仅源码 / 静态预览 材质 原始语言: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:
- 采样基础贴图与基础色,得到 albedo;
- 用
_TilingA/_TilingB与_ScrollSpeed计算两组随_Time.y滚动的 UV; - 分别用
UnpackNormalScale解出两张切线空间法线,按_BlendWeight做加权混合(UDN 混合); - 用顶点着色器传下来的切线与副切线构造
half3x3 tbn,把切线空间法线变换到世界空间; - 填充
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"
}