在URP中移植HDRP的IBL预烘焙图
在URP中移植HDRP的IBL预烘焙图
00 前置知识
URP默认的环境光FDG是用函数拟合的
1
2
3
4
5
6
// Computes the specular term for EnvironmentBRDF
half3 EnvironmentBRDFSpecular(BRDFData brdfData, half fresnelTerm)
{
float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0);
return half3(surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm));
}
而HDRP是使用预积分的LUT图来完成的, 会更准确, 且不会在边缘产生过亮的高光, 这个非常重要.
HDRP中关键的几个文件是
PreIntegratedFGD.cs: 记录了LUT图的声明, 其中比较重要的是1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public enum FGDTexture { Resolution = 64 // LUT分辨率为 64 } ... m_PreIntegratedFGD[(int)index] = new RenderTexture(res, res, 0, GraphicsFormat.A2B10G10R10_UNormPack32) { hideFlags = HideFlags.HideAndDontSave, filterMode = FilterMode.Bilinear, // LUT的FilterMode为 Bilinear wrapMode = TextureWrapMode.Clamp, // LUT的WrapMode为 Clamp name = CoreUtils.GetRenderTargetAutoName(res, res, 1, GraphicsFormat.A2B10G10R10_UNormPack32, $"preIntegrated{index}") // 图片格式为 GraphicsFormat.A2B10G10R10_UNormPack32 };
PreIntegratedFGD.hlsl: 记录了重要的解码函数LUT渲染用着色器
preIntegratedFGD_GGXDisneyDiffuse.shaderpreIntegratedFGD_CharlieFabricLambert.shaderPreIntegratedFGD_Marschner.shader
ImageBasedLighting.hlsl: 记录了生成函数IntegrateGGXAndDisneyDiffuseFGD
01 实施
离线生成, 不考虑实时生成.
制作生成工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
using UnityEngine; using UnityEditor; using UnityEngine.Experimental.Rendering; using System.IO; public class GeneratePreIntegratedFGDEditor : EditorWindow { private const int Resolution = 64; private const GraphicsFormat RtFormat = GraphicsFormat.A2B10G10R10_UNormPack32; private const TextureFormat SaveFormat = TextureFormat.RGBAFloat; private const string ShaderPath = "Hidden/URP/preIntegratedFGD_GGXDisneyDiffuse"; private string assetPath = "Assets/Temp/PreIntegratedFGD_GGXDisneyDiffuse.asset"; [MenuItem("Tools/Generate PreIntegratedFGD LUT")] public static void ShowWindow() { var window = GetWindow<GeneratePreIntegratedFGDEditor>(true, "PreIntegratedFGD Generator"); window.minSize = new Vector2(400, 80); window.Show(); } private void OnGUI() { GUILayout.Label("离线生成 PreIntegratedFGD LUT", EditorStyles.boldLabel); assetPath = EditorGUILayout.TextField("Asset Path", assetPath); if (GUILayout.Button("Generate LUT")) { GenerateLUT(); } } private void GenerateLUT() { // 确保 Shader 路径正确 Shader lutShader = Shader.Find(ShaderPath); if (lutShader == null) { Debug.LogError($"找不到 Shader '{ShaderPath}'"); return; } Material mat = new Material(lutShader); // 创建 RenderTexture var rtDesc = new RenderTextureDescriptor(Resolution, Resolution) { graphicsFormat = RtFormat, depthBufferBits = 0, sRGB = false, msaaSamples = 1 }; RenderTexture rt = new RenderTexture(rtDesc) { filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp }; // 渲染到 RT Graphics.Blit(null, rt, mat); // 释放材质 DestroyImmediate(mat); // 读取 RT 到 Texture2D RenderTexture prev = RenderTexture.active; RenderTexture.active = rt; Texture2D tex = new Texture2D(Resolution, Resolution, SaveFormat, false) { name = Path.GetFileNameWithoutExtension(assetPath) }; tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; tex.ReadPixels(new Rect(0, 0, Resolution, Resolution), 0, 0); tex.Apply(); RenderTexture.active = prev; // 保存成资源 AssetDatabase.StartAssetEditing(); if (File.Exists(assetPath)) { AssetDatabase.DeleteAsset(assetPath); } AssetDatabase.CreateAsset(tex, assetPath); AssetDatabase.StopAssetEditing(); AssetDatabase.SaveAssets(); // 不刷新的话, 会导致看似选中, 实际上没有选中 AssetDatabase.Refresh(); Debug.Log($"已生成 PreIntegratedFGD LUT 并保存到: {assetPath}"); //释放 RT RenderTexture.active = null; // ← 先清空 active, 不清空则会报错 rt.Release(); DestroyImmediate(rt); } }
迁移着色器
注意:
管线修改
"RenderPipeline" = "UniversalPipeline"注释掉
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switchHDRP用的全屏三角, 这里直接使用屏幕顶点和UV就好, 对应的转换
1 2 3 4 5
struct Attributes { float4 position : POSITION; // 裸顶点 float2 texCoord : TEXCOORD0; // 对应 UV };
对应的顶点转换代码
1 2 3
// 直接用 Blit 自带的顶点/UV output.positionCS = TransformWorldToHClip(input.position.xyz); output.texCoord = input.texCoord;
其他着色器类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
Shader "Hidden/URP/preIntegratedFGD_GGXDisneyDiffuse" { SubShader { Tags { "RenderPipeline" = "UniversalPipeline" } Pass { ZTest Always Cull Off ZWrite Off HLSLPROGRAM #pragma editor_sync_compilation #pragma vertex Vert #pragma fragment Frag #pragma target 4.5 // #pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch #define PREFER_HALF 0 #define FGDTEXTURE_RESOLUTION (64) #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Input.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl" // #define SHADER_TARGET 20 #include "ImageBasedLighting.hlsl" // struct Attributes // { // uint vertexID : SV_VertexID; // }; struct Attributes { float4 position : POSITION; // 裸顶点 float2 texCoord : TEXCOORD0; // 对应 UV }; struct Varyings { float4 positionCS : SV_POSITION; float2 texCoord : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; // output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID); // output.texCoord = GetFullScreenTriangleTexCoord(input.vertexID); // 直接用 Blit 自带的顶点/UV output.positionCS = TransformWorldToHClip(input.position.xyz); output.texCoord = input.texCoord; return output; } float4 Frag(Varyings input) : SV_Target { // We want the LUT to contain the entire [0, 1] range, without losing half a texel at each side. float2 coordLUT = RemapHalfTexelCoordTo01(input.texCoord, FGDTEXTURE_RESOLUTION); // The FGD texture is parametrized as follows: // X = sqrt(dot(N, V)) // Y = perceptualRoughness // These coordinate sampling must match the decoding in GetPreIntegratedDFG in Lit.hlsl, // i.e here we use perceptualRoughness, must be the same in shader // Note: with this angular parametrization, the LUT is almost perfectly linear, // except for the grazing angle when (NdotV -> 0). float NdotV = coordLUT.x * coordLUT.x; float perceptualRoughness = coordLUT.y; // Pre integrate GGX with smithJoint visibility as well as DisneyDiffuse float4 preFGD = IntegrateGGXAndDisneyDiffuseFGD( NdotV, PerceptualRoughnessToRoughness(perceptualRoughness)); // 可视化 coord.x(N·V 方向渐变)和 coord.y(roughness 方向渐变) // return float4(input.texCoord.x, input.texCoord.y, 0, 1); // return float4(coordLUT.x, coordLUT.y, 0, 1); return float4(preFGD.xyz, 1.0); } ENDHLSL } } Fallback Off }
02 坑
ImageBasedLighting.hlsl中的IntegrateGGXAndDisneyDiffuseFGD函数中调用了Hammersley2d函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
real VanDerCorputBase2(uint i) { return ReverseBits32(i) * rcp(4294967296.0); // 2^-32 } real2 Hammersley2dSeq(uint i, uint sequenceLength) { return real2(real(i) / real(sequenceLength), VanDerCorputBase2(i)); } // Loads elements from one of the precomputed tables for sample counts of 16, 32, 64, 256. // Computes sample positions at runtime otherwise. real2 Hammersley2d(uint i, uint sampleCount) { switch (sampleCount) { #ifdef HAMMERSLEY_USE_CB case 16: return hammersley2dSeq16[i].xy; case 32: return hammersley2dSeq32[i].xy; case 64: return hammersley2dSeq64[i].xy; case 256: return hammersley2dSeq256[i].xy; #else case 16: return k_Hammersley2dSeq16[i]; case 32: return k_Hammersley2dSeq32[i]; case 64: return k_Hammersley2dSeq64[i]; case 256: return k_Hammersley2dSeq256[i]; #endif default: return Hammersley2dSeq(i, sampleCount); // 因为默认采样数为4096, 所以直接走的这个分支 } }
这个函数在
Hammersley.hlsl文件中1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Ref: http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html uint ReverseBits32(uint bits) { #if (SHADER_TARGET >= 45) // 分支A return reversebits(bits); #else // 分支B bits = (bits << 16) | (bits >> 16); bits = ((bits & 0x00ff00ff) << 8) | ((bits & 0xff00ff00) >> 8); bits = ((bits & 0x0f0f0f0f) << 4) | ((bits & 0xf0f0f0f0) >> 4); bits = ((bits & 0x33333333) << 2) | ((bits & 0xcccccccc) >> 2); bits = ((bits & 0x55555555) << 1) | ((bits & 0xaaaaaaaa) >> 1); return bits; #endif }
如果走分支A, 原本
reversebits会走uint bitfieldReverse(uint x)这个, 因为bits是uint的理论上应该走这个重载. 但Compile and show code会发现, 实际走的是int bitfieldReverse(int x)这个重载, 因为编出来的代码是bitfieldReverse((int) x), 走了一次转义, 然后会报错error C7011: implicit cast from "int" to "uint", 导致无法渲染.如果走分支B(在
#include ImageBasedLighting.hlsl前强制#define SHADER_TARGET 30), 则会发现, 编出来的代码有类似这样的代码u_xlatu27 = uint(int(bitfieldInsert(int(u_xlatu19.x), int(u_xlatu_loop_1), 16 & 0x1F, 16)));, 而这里实际走的也是int bitfieldInsert(int a, int b, int c, int d)的重载, 而理论上应该走uint bitfieldInsert(uint a, uint b, int c, int d). 这个虽然不会报错, 但是会渲染错误的图片.原因猜测
在编辑器的Android平台环境下, ShaderCompiler会不支持uint的函数形式, 同时位移操作也会因为不支持uint而出错.
解决方案
因为位移操作的本质是提升渲染效率, HDRP是实时每帧生成这张图, 所以会在意. 而这次的方案是离线渲染, 预生成无需考虑效率, 那么抛弃位移操作, 直接硬算即可, 其他的不用改动.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// ——— 纯浮点版 Hammersley,不用任何位运算 ——— float RadicalInverse2_PURE(int bits) { float invBase = 0.5; float result = 0.0; float fBits = float(bits); UNITY_UNROLL while (fBits > 0.0) { // 等价于 bits % 2 float b = floor(frac(fBits * 0.5) * 2.0); result += b * invBase; // 相当于 bits >>= 1 fBits *= 0.5; fBits = floor(fBits); invBase *= 0.5; } return result; } float2 Hammersley2d_PURE(int i, int N) { float phi = RadicalInverse2_PURE(i); float theta = float(i) / float(N); return float2(phi, theta); } // ————————————————————————————————————————————————
最后成图如下, 左图是离线运算结果, 右图是HDRP生成的图

