跳到主要内容
← 全部 Shader
仅源码 / 静态预览 材质 原始语言:HLSL (Unity URP) 浏览器:无浏览器实现

URP 双层滚动法线(仅源码)

一个使用 URP PBR 光照与两张滚动法线贴图的 Unity 着色器。由于依赖贴图资源与 URP 每帧光照数据,目前没有浏览器实时预览,页面只提供原始 HLSL 与静态参考图。

本条目没有浏览器实时预览

这个着色器依赖两张法线贴图资源、切线空间到世界空间的 TBN 变换,以及 URP 的 UniversalFragmentPBR(包含 GGX BRDF、光照探针、附加光源循环与屏幕空间环境光遮蔽)。 要在浏览器里如实还原,需要引入贴图资源与 IBL 环境贴图,并重新实现 URP 的光照数据供给, 属于后续工作。在实现完成之前,本条目不提供实时预览,也不会显示“浏览器实时运行”的标记。

示意图:双层滚动法线混合的预期效果,尚未在浏览器中实现

静态预览图 / 参考画面。

本页目录

为什么这个条目没有实时预览

任务书要求「未实现实时预览的项目不会显示虚假的实时运行状态」。这一条就是那个反例样本:它的 runtime.status 是 static-only,languages.browser 是 none,因此

  • 列表页与详情页的徽标显示「仅源码 / 静态预览」,而不是「浏览器实时运行」;
  • 页面不会渲染 <canvas>,也不会动态加载 three.js;
  • 明确写出无法实时运行的原因,而不是留一个空白区域让人猜。

原始 HLSL 做了什么

ScrollingNormalLit.shader 是一个 URP 的 ForwardLit Pass:

  1. 采样基础贴图与基础色,得到 albedo;
  2. 用 _TilingA / _TilingB 与 _ScrollSpeed 计算两组随 _Time.y 滚动的 UV;
  3. 分别用 UnpackNormalScale 解出两张切线空间法线,按 _BlendWeight 做加权混合(UDN 混合);
  4. 用顶点着色器传下来的切线与副切线构造 half3x3 tbn,把切线空间法线变换到世界空间;
  5. 填充 SurfaceData / InputData,交给 UniversalFragmentPBR 完成 GGX BRDF、IBL 与附加光照计算。

关键代码:

float2 scrollA = IN.uv * _TilingA.xy + _Time.y * _ScrollSpeed.xy;
float2 scrollB = IN.uv * _TilingB.xy + _Time.y * _ScrollSpeed.zw;

half3 nA = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, scrollA), _NormalScale);
half3 nB = UnpackNormalScale(SAMPLE_TEXTURE2D(_SecondNormalMap, sampler_SecondNormalMap, scrollB), _SecondNormalScale);

half sgn = IN.tangentWS.w;
float3 bitangent = sgn * cross(IN.normalWS, IN.tangentWS.xyz);
half3x3 tbn = half3x3(IN.tangentWS.xyz, bitangent, IN.normalWS);
half3 normalWS = normalize(mul(normalTS, tbn));

移植到浏览器需要解决的问题

如果将来要做浏览器版本,至少要处理下面几件事,这里先记录下来:

依赖浏览器中的对应方案
_NormalMap / _SecondNormalMap 贴图资源需要自备可自由使用的贴图,或改用程序化生成的法线(例如解析式波纹法线)
UnpackNormalScale在 GLSL 中手写:vec3 n = texture(t, uv).xyz * 2.0 - 1.0; n.xy *= scale;
TBN 构造three.js 的几何体自带 tangent 属性,或在着色器里用偏导数 dFdx/dFdy 现算
UniversalFragmentPBR需要自己实现 GGX 的 D/F/G 项,或者接受一个简化的 Blinn-Phong 近似并如实标注差异
光照探针与 IBL需要提供一张环境贴图并做预滤波,否则金属度大于 0 时结果会明显不同
_ADDITIONAL_LIGHTS 附加光源循环浏览器预览只有单一方向光,附加光源需要省略
_Time.y映射到预览的可暂停时钟 uTime

验证状态

  • HLSL:静态审查。本仓库没有 Unity 安装,未经过 Unity 着色器编译器验证。
  • GLSL:不存在,因此没有任何浏览器端验证结果。

源码

浏览器 GLSL 与原始 HLSL 分开存放、分开展示。HLSL 仅作参考,浏览器无法直接执行 Unity URP 的 HLSL。

ShaderLab + HLSLPROGRAM 完整源码 scrolling-normal/ScrollingNormalLit.shader hlsl 原始 HLSL · 不在浏览器中执行 168 行
// ---------------------------------------------------------------------------
// Portfolio/ScrollingNormalLit
//
// Unity URP shader: two scrolling normal maps blended in tangent space on top
// of URP's physically based lighting. Used on this site as an example of a
// shader that is NOT reproducible in the browser preview yet, so its content
// entry is marked `runtime.status: static-only` and the page shows sources plus
// a static reference image instead of claiming a live WebGL 2 run.
//
// Verified with: static review only. This repository has no Unity installation.
// ---------------------------------------------------------------------------
Shader "Portfolio/ScrollingNormalLit"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor]   _BaseColor("Base Color", Color) = (1, 1, 1, 1)

        [Normal] _NormalMap("Normal Map A", 2D) = "bump" {}
        _NormalScale("Normal Scale A", Range(0, 2)) = 1.0
        _TilingA("Tiling A", Vector) = (1, 1, 0, 0)

        [Normal] _SecondNormalMap("Normal Map B", 2D) = "bump" {}
        _SecondNormalScale("Normal Scale B", Range(0, 2)) = 0.6
        _TilingB("Tiling B", Vector) = (2.3, 1.7, 0, 0)

        _ScrollSpeed("Scroll Speed (UV per second)", Vector) = (0.05, 0.03, -0.02, 0.04)
        _BlendWeight("Blend Weight (A vs B)", Range(0, 1)) = 0.5

        _Smoothness("Smoothness", Range(0, 1)) = 0.5
        _Metallic("Metallic", Range(0, 1)) = 0.0
        _AmbientOcclusion("Ambient Occlusion", Range(0, 1)) = 1.0
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Geometry"
        }
        LOD 300

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex scrollVert
            #pragma fragment scrollFrag
            #pragma target 3.5

            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _SHADOWS_SOFT
            #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
            #pragma multi_compile _ _FORWARD_PLUS

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            TEXTURE2D(_BaseMap);            SAMPLER(sampler_BaseMap);
            TEXTURE2D(_NormalMap);          SAMPLER(sampler_NormalMap);
            TEXTURE2D(_SecondNormalMap);    SAMPLER(sampler_SecondNormalMap);

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseMap_ST;
                half4 _BaseColor;
                half4 _TilingA;
                half4 _TilingB;
                half4 _ScrollSpeed;
                half  _NormalScale;
                half  _SecondNormalScale;
                half  _BlendWeight;
                half  _Smoothness;
                half  _Metallic;
                half  _AmbientOcclusion;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float4 tangentOS  : TANGENT;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS  : SV_POSITION;
                float3 positionWS  : TEXCOORD0;
                float3 normalWS    : TEXCOORD1;
                float4 tangentWS   : TEXCOORD2; // xyz: tangent, w: sign
                float2 uv          : TEXCOORD3;
            };

            Varyings scrollVert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs vertexInput = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normalInput = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);

                OUT.positionCS = vertexInput.positionCS;
                OUT.positionWS = vertexInput.positionWS;
                OUT.normalWS = normalInput.normalWS;
                OUT.tangentWS = float4(normalInput.tangentWS, IN.tangentOS.w * GetOddNegativeScale());
                OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
                return OUT;
            }

            // Blend two tangent-space normals; equivalent to a weighted
            // UDN blend, which is what most stylised water/surface shaders use.
            half3 BlendScrollNormals(half3 a, half3 b, half weight)
            {
                return normalize(lerp(a, b, saturate(weight)));
            }

            half4 scrollFrag(Varyings IN) : SV_Target
            {
                half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                half4 albedo = baseMap * _BaseColor;

                float2 scrollA = IN.uv * _TilingA.xy + _Time.y * _ScrollSpeed.xy;
                float2 scrollB = IN.uv * _TilingB.xy + _Time.y * _ScrollSpeed.zw;

                half3 nA = UnpackNormalScale(
                    SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, scrollA), _NormalScale);
                half3 nB = UnpackNormalScale(
                    SAMPLE_TEXTURE2D(_SecondNormalMap, sampler_SecondNormalMap, scrollB), _SecondNormalScale);

                half3 normalTS = BlendScrollNormals(nA, nB, _BlendWeight);

                half sgn = IN.tangentWS.w;
                float3 bitangent = sgn * cross(IN.normalWS, IN.tangentWS.xyz);
                half3x3 tbn = half3x3(IN.tangentWS.xyz, bitangent, IN.normalWS);
                half3 normalWS = normalize(mul(normalTS, tbn));

                float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
                Light mainLight = GetMainLight(shadowCoord);

                SurfaceData surfaceData = (SurfaceData)0;
                surfaceData.albedo = albedo.rgb;
                surfaceData.metallic = _Metallic;
                surfaceData.specular = half3(0, 0, 0);
                surfaceData.smoothness = _Smoothness;
                surfaceData.normal = normalWS;
                surfaceData.occlusion = _AmbientOcclusion;
                surfaceData.alpha = albedo.a;

                InputData inputData = (InputData)0;
                inputData.positionWS = IN.positionWS;
                inputData.normalWS = normalWS;
                inputData.viewDirectionWS = SafeNormalize(GetWorldSpaceViewDir(IN.positionWS));
                inputData.shadowCoord = shadowCoord;
                inputData.shadowMask = half4(1, 1, 1, 1);
                inputData.fogCoord = 0.0;

                half4 color = UniversalFragmentPBR(inputData, surfaceData);
                return color;
            }
            ENDHLSL
        }
    }

    FallBack "Universal Render Pipeline/Lit"
}