Unity TA 学习笔记系列(3) - 《Unity Shader 入门精要》基础PBR光照(二)
一、前言
上一篇文章对PBR中直接光的高光反射项和漫反射项进行了整理,但显然那样的光照模型仍然不够完整,那这篇文章就要来讲一讲PBR中环境光的处理。对于环境光,计算上没有直接光这么简单,但使用的BRDF模型是一致的,所以下面会以计算处理中的不同为主,不会再去关注具体使用了什么算法模型。
二、IBL高光反射BRDF
在Unity中,我们通常会使用IBL(基于图像的光照)来模拟环境光照,在进行IBL的BRDF计算时,我们会采用LUT(查找表)的形式,来获取BRDF的计算结果。在学习中,我曾有过疑问,为什么直接光能够计算的BRDF,换成IBL以后就需要去查表了,两者的本质区别在哪。
首先给出结论,IBL的BRDF实时计算成本远高于直接光。我们需要知道,光线可以认为是在表面某点的球形任意方向上照射到该点,而只有该点表面法线同侧的上半球光线能够对该点产生影响,因此BRDF实际上是在计算一个半球积分,表达式如下:
其中为出射光,
为光源位置,
为出射方向,
为半球面积分,
为表面反射规律的函数,
为入射方向,
为法向。
直接光和IBL两者的区别在于直接光的BRDF计算是单方向点积分,由于直接光只在单个方向上有非零辐射度,因此BRDF的积分操作会直接塌缩为单点求值,塌缩后的表达式如下:
而对于IBL的BRDF积分,由于IBL的本质是物体表面接收到来自整个半球所有方向的间接光线,此时在所有方向上都有非零辐射度,积分无法塌缩,必须对整个半球进行连续积分,同时考虑到表面对于IBL的间接光线采样质量,计算次数会成倍增长,同时BRDF积分是同时与入射和出射方向相关的,也就是相机移动和物体旋转都会影响积分结果,此时需要在保证性能的情况下实时计算IBL积分几乎是不现实的。
因此通过LUT来缓存预计算的BRDF积分结果是目前最优的解决方案,但这个方案有一定局限,它要求使用微表面BRDF且菲涅尔项需要能够线性分离。这个方案来源于Epic在2013年提出的Split Sum近似,以Cook-Torrance BRDF为例,它对积分运算做了如下拆分:
Cook-Torrance镜面BRDF完整表示为:
其中 为半角向量。
将菲涅尔项 用Schlick近似展开:
代入原积分,我们可以将整个半球积分拆成两项:
通过上式,可以发现积分内部实际只与法线、出射方向和粗糙度相关,与物体本身金属度颜色无关。于是我们可以将整个计算拆成两张独立的预计算纹理,预过滤环境贴图和BRDF积分LUT图。
1. 预过滤环境贴图
预过滤环境贴图是对环境光L及BRDF中的D、G项预卷积,生成带有mipmap的立方体环境贴图,其中每个mip级别对应一个粗糙度值,粗糙度越高,对应的mip级别越模糊,且这张图只与环境光相关,和材质视角无关。
2. BRDF积分LUT图
积分中剩下的两个系数,我们可以将其预计算至一张2D纹理中作为积分运算的LUT,通过将作为UV坐标对LUT图进行采样,我们可以获得积分的两项系数输出。值得一提的是,这张积分LUT图只与你采用的BRDF相关,与其他因素完全无关,因此只要采用的是同一套BRDF,所有材质均可以使用这张LUT图。
3. IBL高光反射计算
有了环境光贴图和积分LUT图,我们能够对IBL高光反射进行最终计算。首先我们根据粗糙度,采样预过滤环境贴图对应的mip级别,得到光照影响;通过
对BRDF LUT图进行采样,得到积分系数
;最后对高光反射颜色进行计算,算式如下:
由此,仅需通过纹理采样和简单运算,就可以获得整个半球的间接光照。
三、IBL漫反射BRDF
IBL漫反射相对于高光反射计算要简单,以Lambert漫反射BRDF为例,此时,积分可以简化为如下形式:
其中积分项就是简单根据法线对环境光辐照度进行采样,而这个采样在引擎中是由对应调用的,非常简便。
如果需要使用上一篇文章中提到的Disney改进型BRDF进行漫反射计算,将对应的BRDF带入积分中,可以发现并没有Lambert这么简单,积分区域较为复杂,甚至可能还需要一张LUT图来帮助计算。
鉴于环境光的漫反射对最终表现没有太大的影响,在我的实现中就偷了个小懒,在Lambert BRDF的基础上引入菲涅尔项及金属度对漫反射能量的影响来近似,我的实现表达式如下:
至此,结合上一篇文章,我的PBR实现结构就基本完整了。
四、代码实现
1. IBL高光反射
float mipLevel = linearRoughness * (1.7 - 0.7 * linearRoughness) * UNITY_SPECCUBE_LOD_STEPS;
float4 encodeSpecularRadiance = unity_SpecCube0.SampleLevel(samplerunity_SpecCube0, reflectDir, mipLevel);
float3 specularRadiance = DecodeHDREnvironment(encodeSpecularRadiance, unity_SpecCube0_HDR);
float NdotV = max(0, dot(normalWS, V));
float2 brdfSample = float2(max(NdotV, 0), linearRoughness);
float2 brdfValue = tex2D(_BRDF_LUT, brdfSample).rg;
float3 specularIBL = specularRadiance * (F0 * brdfValue.x + (1 - F0) * brdfValue.y);
2. IBL漫反射
float3 diffuseRadiance = SampleSH(normalWS);
float3 F = Fresnel_Schlick(F0, NdotV);
float3 kD = (1 - F) * (1 - metallic);
float3 diffuseIBL = albedo * kD * diffuseRadiance / PI;
3. PBR Shader
注:在我的Shader中,BRDF相关函数全放在了另一个hlsl里,上一篇文章都贴过相关实现,这里就不重复放了,可以结合进行补全。
Shader "MyPBR/BasicPBR"
{
Properties
{
_AlbedoMap("Albedo Map", 2D) = "white" {}
_AlbedoColor("Albedo Color", Color) = (1,1,1,1)
_MetallicMap("Metallic Map", 2D) = "white" {}
_Metallic("Metallic", Range(0,1)) = 0
[Toggle(_IS_SMOOTHNESS)] _IsSmoothness("Is Smoothness", float) = 0
_RoughnessSmoothnessMap("Roughtness/Smoothness Map", 2D) = "white" {}
_RoughnessSmoothness("Roughness/Smoothness", Range(0,1)) = 0.5
_NormalMap("Normal Map", 2D) = "bump" {}
_NormalIntensity("Normal Intensity", float) = 1
_OcclusionMap("Occlusion Map", 2D) = "white" {}
_OcclusionIntensity("Occlusion Intensity", float) = 1
_EmissionMap("Emission Map", 2D) = "black" {}
[HDR] _EmissionColor("Emission Color", Color) = (1,1,1,1)
_BRDF_LUT("BRDF LUT", 2D) = "white" {}
}
SubShader
{
HLSLINCLUDE
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile _ _LIGHTMAP_ON
#pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_PROJECTION
#pragma multi_compile_fragment _ _REFLECTION_BOX_PROJECTION
#pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
#pragma shader_feature _ _IS_SMOOTHNESS
#pragma shader_feature _ _IS_ANISTROPY
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/GlobalIllumination.hlsl"
#include "../BRDF/BRDFUtils.hlsl"
struct a2v
{
float3 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
};
struct v2f
{
float4 positionCS_SV : SV_POSITION;
float2 uv : TEXCOORD0;
float3 positionWS : TEXCOORD1;
float3 normalWS : NORMAL;
float3 tangentWS : TANGENT;
float3 bitangentWS : TEXCOORD2;
};
sampler2D _AlbedoMap;
float4 _AlbedoColor;
sampler2D _MetallicMap;
float _Metallic;
sampler2D _RoughnessSmoothnessMap;
float _RoughnessSmoothness;
sampler2D _NormalMap;
float _NormalIntensity;
sampler2D _OcclusionMap;
float _OcclusionIntensity;
sampler2D _EmissionMap;
float4 _EmissionColor;
sampler2D _BRDF_LUT;
ENDHLSL
Pass
{
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
v2f vert(a2v v)
{
v2f o;
o.positionCS_SV = TransformObjectToHClip(v.positionOS);
o.positionWS = TransformObjectToWorld(v.positionOS);
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
o.tangentWS = normalize(TransformObjectToWorldDir(v.tangentOS.xyz));
o.bitangentWS = normalize(cross(o.normalWS, v.tangentOS.xyz) * v.tangentOS.w);
o.uv = v.uv;
return o;
}
float4 frag(v2f i) : SV_Target
{
float3x3 TBN = float3x3(normalize(i.tangentWS), normalize(i.bitangentWS), normalize(i.normalWS));
float3x3 tangentToWorld = transpose(TBN);
float3 albedo = tex2D(_AlbedoMap, i.uv).rgb * _AlbedoColor.rgb;
float metallic = tex2D(_MetallicMap, i.uv).r * _Metallic;
#ifdef _IS_SMOOTHNESS
float smoothness = tex2D(_RoughnessSmoothnessMap, i.uv).b * _RoughnessSmoothness;
float perceptualRoughness = 1 - smoothness;
#else
float perceptualRoughness = tex2D(_RoughnessSmoothnessMap, i.uv).b * _RoughnessSmoothness;
#endif
perceptualRoughness = clamp(perceptualRoughness, 0.04, 0.98);
float3 normalTS = UnpackNormal(tex2D(_NormalMap, i.uv));
normalTS.xy *= _NormalIntensity;
normalTS.z = sqrt(1 - saturate(dot(normalTS.xy, normalTS.xy)));
float3 normalWS = normalize(mul(tangentToWorld, normalTS));
float3 occlusion = tex2D(_OcclusionMap, i.uv).g * _OcclusionIntensity;
float3 emission = tex2D(_EmissionMap, i.uv).rgb * _EmissionColor.rgb;
float roughness = max(HALF_MIN_SQRT, perceptualRoughness * perceptualRoughness);
float roughness2 = roughness * roughness;
float dielectricSpecular = 0.04;
float3 F0 = lerp(dielectricSpecular, albedo, metallic);
Light lights[1+8];
lights[0] = GetMainLight(TransformWorldToShadowCoord(i.positionWS), i.positionWS, 1);
int lightCount = 1 + GetAdditionalLightsCount();
for(int j = 1; j < lightCount; j++)
{
lights[j] = GetAdditionalLight(j - 1, i.positionWS, 1);
}
float3 diffuse = 0;
float3 specular = 0;
float3 ambient = 0;
float3 finalColor = 0;
float3 V = normalize(GetCameraPositionWS() - i.positionWS);
float3 reflectDir = reflect(-V, normalWS);
for(int k = 0; k < lightCount; k++)
{
Light light = lights[k];
float3 L = normalize(light.direction);
float3 H = normalize(V + L);
float NdotL = max(0,dot(normalWS, L));
float NdotV = max(0,dot(normalWS, V));
float NdotH = max(0,dot(normalWS, H));
float LdotH = dot(L, H);
float VdotH = dot(V, H);
float3 F = Fresnel_Schlick(F0, VdotH);
float NDF = NDF_GGX(NdotH, roughness);
float k = (1 + roughness) * (1 + roughness) / 8;
float G = Geometry_Smith(NdotV, NdotL, k);
//Cook-Torrance
float3 nominator = F * NDF * G;
float denominator = 4 * NdotV * NdotL + 0.001;
float3 specularBRDF = nominator / denominator;
float diffuseFactor = DisneyDiffuseFrostbite(NdotV, NdotL, LdotH, roughness);
float3 kD = (1 - F) * (1 - metallic) * diffuseFactor;
float3 diffuseBRDF = kD * albedo / PI;
float3 radiance = light.color * NdotL;
diffuse += diffuseBRDF * radiance;
specular += specularBRDF * radiance;
}
float mipLevel = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness) * UNITY_SPECCUBE_LOD_STEPS;
float4 encodeSpecularRadiance = unity_SpecCube0.SampleLevel(samplerunity_SpecCube0, reflectDir, mipLevel);
float3 specularRadiance = DecodeHDREnvironment(encodeSpecularRadiance, unity_SpecCube0_HDR);
float NdotV = max(0, dot(normalWS, V));
float2 brdfSample = float2(max(NdotV, 0), perceptualRoughness);
float2 brdfValue = tex2D(_BRDF_LUT, brdfSample).rg;
float3 specularIBL = specularRadiance * (F0 * brdfValue.x + (1 - F0) * brdfValue.y);
float3 diffuseRadiance = SampleSH(normalWS);
float3 F = Fresnel_Schlick(F0, NdotV);
float3 kD = (1 - F) * (1 - metallic);
float3 diffuseIBL = albedo * kD * diffuseRadiance / PI;
ambient = (diffuseIBL + specularIBL) * occlusion;
finalColor = diffuse + specular + ambient + emission;
return float4(finalColor, 1);
}
ENDHLSL
}
UsePass "Universal Render Pipeline/Lit/DepthOnly"
UsePass "Universal Render Pipeline/Lit/DepthNormals"
}
Fallback "Universal Render Pipeline/Lit"
}
4. LUT图生成
注:我使用的BRDF后续有过修改和优化,但没有同步更新LUT生成,所以两边BRDF可能会有部分偏差,但采用的模型大差不差,最后效果上也不会有特别大的问题。
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
public class BRDFLUTGenerator : EditorWindow
{
private int lutSize = 512;
private string savePath = "Assets/BRDF_LUT.png";
private Texture2D previewTexture;
private bool isGenerating = false;
[MenuItem("Tools/PBR/Generate BRDF LUT")]
public static void ShowWindow()
{
GetWindow<BRDFLUTGenerator>("BRDF LUT Generator");
}
void OnGUI()
{
GUILayout.Label("BRDF积分查找贴图生成工具", EditorStyles.boldLabel);
lutSize = EditorGUILayout.IntSlider("LUT尺寸", lutSize, 32, 2048);
savePath = EditorGUILayout.TextField("保存路径", savePath);
EditorGUILayout.Space();
if (GUILayout.Button("生成BRDF LUT") && !isGenerating)
{
GenerateBRDFLUT();
}
EditorGUILayout.Space();
if (previewTexture != null)
{
GUILayout.Label("预览:");
Rect rect = GUILayoutUtility.GetRect(256, 256);
EditorGUI.DrawPreviewTexture(rect, previewTexture);
}
EditorGUILayout.HelpBox(
"此工具将生成BRDF积分查找贴图,用于基于图像的照明(Irradiance)计算。\n" +
"贴图的横坐标对应N·V(法线·视线),纵坐标对应粗糙度。\n" +
"R通道存储缩放因子,G通道存储偏移因子。",
MessageType.Info);
}
private void GenerateBRDFLUT()
{
isGenerating = true;
try
{
// 创建纹理
Texture2D lut = new Texture2D(lutSize, lutSize, TextureFormat.RGBAFloat, false);
lut.wrapMode = TextureWrapMode.Clamp;
lut.filterMode = FilterMode.Bilinear;
// 计算进度
float totalSteps = lutSize * lutSize;
float currentStep = 0;
// 蒙特卡洛采样数
const int sampleCount = 1024;
// 为每个像素计算BRDF积分
for (int y = 0; y < lutSize; y++)
{
for (int x = 0; x < lutSize; x++)
{
currentStep++;
// 报告进度
if (currentStep % 1000 == 0)
{
float progress = currentStep / totalSteps;
EditorUtility.DisplayProgressBar("生成BRDF LUT",
$"计算中... {(int)(progress * 100)}%", progress);
}
// 将像素坐标映射到参数空间
// x: N·V [0, 1]
// y: 粗糙度 [0, 1]
float NdotV = (x + 0.5f) / lutSize;
float roughness = (y + 0.5f) / lutSize;
// 计算BRDF积分
Vector2 brdf = IntegrateBRDF(NdotV, roughness, sampleCount);
// 存储到纹理
Color color = new Color(brdf.x, brdf.y, 0, 1);
lut.SetPixel(x, y, color);
}
}
EditorUtility.ClearProgressBar();
// 应用纹理更改
lut.Apply();
// 保存为PNG
byte[] bytes = EncodeToPNG(lut);
File.WriteAllBytes(savePath, bytes);
// 导入设置
AssetDatabase.ImportAsset(savePath);
TextureImporter importer = AssetImporter.GetAtPath(savePath) as TextureImporter;
if (importer != null)
{
importer.textureType = TextureImporterType.Default;
importer.sRGBTexture = false; // 线性空间
importer.wrapMode = TextureWrapMode.Clamp;
importer.filterMode = FilterMode.Bilinear;
importer.mipmapEnabled = false;
importer.maxTextureSize = 512;
TextureImporterPlatformSettings settings = importer.GetDefaultPlatformTextureSettings();
settings.format = TextureImporterFormat.RGBA32;
settings.maxTextureSize = 512;
importer.SetPlatformTextureSettings(settings);
importer.SaveAndReimport();
}
// 预览
previewTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(savePath);
Debug.Log($"BRDF LUT生成完成,保存至: {savePath}");
// 选中生成的文件
Selection.activeObject = AssetDatabase.LoadAssetAtPath<Texture2D>(savePath);
}
catch (System.Exception e)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"生成BRDF LUT时出错: {e.Message}");
}
finally
{
isGenerating = false;
}
}
// 积分BRDF函数 - 基于你的BRDF模型
private Vector2 IntegrateBRDF(float NdotV, float roughness, int sampleCount)
{
Vector3 V = new Vector3(
Mathf.Sqrt(1.0f - NdotV * NdotV), // sin(theta)
0.0f,
NdotV); // cos(theta)
float A = 0.0f;
float B = 0.0f;
Vector3 N = new Vector3(0.0f, 0.0f, 1.0f);
float roughnessSq = roughness * roughness;
for (int i = 0; i < sampleCount; i++)
{
// 重要性采样 - 基于GGX分布
Vector2 xi = Hammersley(i, sampleCount);
Vector3 H = SchlickGGX_Sample(xi, N, roughnessSq);
Vector3 L = (2 * Vector3.Dot(V, H) * H - V).normalized;
float NdotL = Mathf.Max(L.z, 0.0f);
float NdotH = Mathf.Max(H.z, 0.0f);
float VdotH = Mathf.Max(Vector3.Dot(V, H), 0.0f);
if (NdotL > 0.0f)
{
// 基于你的BRDF模型计算几何项
//float k = (roughnessSq + 1.0f) * (roughnessSq + 1.0f) / 8.0f;
float k = roughnessSq * roughnessSq / 2f;
float G = G_SmithCustom(NdotV, NdotL, k);
float G_Vis = (G * VdotH) / (NdotH * NdotV);
// 菲涅尔项
float Fc = Mathf.Pow(1.0f - VdotH, 5.0f);
// 计算权重
A += (1.0f - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
return new Vector2(A / sampleCount, B / sampleCount);
}
// GGX法线分布函数
private float D_GGX(float NdotH, float roughnessSq)
{
float a = NdotH * roughnessSq;
float k = roughnessSq / (1.0f - NdotH * NdotH + a * a);
return k * k * (1.0f / Mathf.PI);
}
// 重要性采样 - GGX分布
private Vector3 SchlickGGX_Sample(Vector2 xi, Vector3 norm, float roughness)
{
float a = roughness * roughness;
float phi = 2.0f * Mathf.PI * xi.x;
float cosTheta = Mathf.Sqrt((1.0f - xi.y) / (1.0f + (a * a - 1.0f) * xi.y));
float sinTheta = Mathf.Sqrt(1.0f - cosTheta * cosTheta);
Vector3 H = new Vector3(
Mathf.Cos(phi) * sinTheta,
Mathf.Sin(phi) * sinTheta,
cosTheta);
Vector4 rot = Quat_ZTo(norm);
return Quat_Rotate(rot, H);
}
private float G_SchlickGGXCustom(float NdotV, float k)
{
return NdotV / (NdotV * (1 - k) + k);
}
private float G_SmithCustom(float NdotV, float NdotL, float k)
{
float ggx1 = G_SchlickGGXCustom(NdotV, k);
float ggx2 = G_SchlickGGXCustom(NdotL, k);
return ggx1 * ggx2;
}
private Vector4 Quat_ZTo(Vector3 to)
{
float cosHalfTheta = Mathf.Sqrt(Mathf.Max(0, (to.z + 1) * 0.5f));
float twoCosHalfTheta = 2.0f * cosHalfTheta;
return new Vector4(-to.y / twoCosHalfTheta, to.x / twoCosHalfTheta, 0, cosHalfTheta);
}
private Vector3 Quat_Rotate(Vector4 q, Vector3 p)
{
Vector3 q_xyz = new Vector3(q.x, q.y, q.z);
Vector3 temp = q.w * p + Vector3.Cross(q_xyz, p);
Vector4 qp = new Vector4(temp.x, temp.y, temp.z, -Vector3.Dot(q_xyz, p));
Vector4 invQ = Quat_Inverse(q);
Vector3 invQ_xyz = new Vector3(invQ.x, invQ.y, invQ.z);
Vector3 qp_xyz = new Vector3(qp.x, qp.y, qp.z);
Vector3 qpInQ = qp.w * invQ_xyz + invQ.w * qp_xyz + Vector3.Cross(qp_xyz, invQ_xyz);
return qpInQ;
}
private Vector4 Quat_Inverse(Vector4 q)
{
return new Vector4(-q.x, -q.y, -q.z, q.w);
}
// Hammersley序列,用于低差异采样
private Vector2 Hammersley(int i, int N)
{
uint bits = (uint)i;
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
return new Vector2((float)i / N, (float)bits / 0xFFFFFFFF);
}
// 将浮点纹理编码为PNG(需要特殊处理浮点数据)
private byte[] EncodeToPNG(Texture2D texture)
{
int width = texture.width;
int height = texture.height;
// 将浮点数据转换为字节数据
Texture2D byteTexture = new Texture2D(width, height, TextureFormat.RGBA32, false);
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
Color floatColor = texture.GetPixel(x, y);
Color32 byteColor = new Color32(
(byte)(Mathf.Clamp01(floatColor.r) * 255),
(byte)(Mathf.Clamp01(floatColor.g) * 255),
(byte)(Mathf.Clamp01(floatColor.b) * 255),
(byte)(Mathf.Clamp01(floatColor.a) * 255)
);
byteTexture.SetPixel(x, y, byteColor);
}
}
byteTexture.Apply();
return byteTexture.EncodeToPNG();
}
// 另一种方法:使用RenderTexture和Shader生成BRDF LUT
[MenuItem("Tools/PBR/Generate BRDF LUT via Shader")]
public static void GenerateBRDFLUTViaShader()
{
int lutSize = 512;
// 创建渲染纹理
RenderTexture rt = new RenderTexture(lutSize, lutSize, 0,
RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
rt.enableRandomWrite = true;
rt.Create();
// 创建计算着色器或材质
Shader shader = Shader.Find("Hidden/BRDFLUTGenerator");
if (shader == null)
{
Debug.LogError("未找到Hidden/BRDFLUTGenerator着色器");
return;
}
Material material = new Material(shader);
// 渲染到纹理
RenderTexture previous = RenderTexture.active;
RenderTexture.active = rt;
GL.Clear(true, true, Color.black);
GL.PushMatrix();
GL.LoadOrtho();
material.SetPass(0);
GL.Begin(GL.QUADS);
GL.TexCoord2(0, 0);
GL.Vertex3(0, 0, 0);
GL.TexCoord2(1, 0);
GL.Vertex3(1, 0, 0);
GL.TexCoord2(1, 1);
GL.Vertex3(1, 1, 0);
GL.TexCoord2(0, 1);
GL.Vertex3(0, 1, 0);
GL.End();
GL.PopMatrix();
RenderTexture.active = previous;
// 保存纹理
Texture2D tex = new Texture2D(lutSize, lutSize, TextureFormat.RGBAFloat, false);
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0, 0, lutSize, lutSize), 0, 0);
tex.Apply();
RenderTexture.active = null;
byte[] bytes = tex.EncodeToPNG();
string path = "Assets/BRDF_LUT_Shader.png";
File.WriteAllBytes(path, bytes);
// 清理
rt.Release();
DestroyImmediate(material);
DestroyImmediate(tex);
AssetDatabase.ImportAsset(path);
Debug.Log($"BRDF LUT已生成: {path}");
}
}
五、最终效果
*不知道为啥图片还是放不上来,后面再补充更新吧
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)