基于物理的渲染PBR(二):挑战手写pbr和IBL环境光部分的见解

手写PBR

在又经过一个星期左右的学习下,我对PBR的了解也有了一定的进展,所以我尝试了一下手写一套较为基础的pbr,不过对比于Unity自带的Standard来说,自己所写的肯定是略显粗糙的。Unity平台自带的pbr里处理了很多更细节的东西,诸如平台问题、优化问题、贴图烘焙问题等等,下面便简单说说自己写的一套pbr,记录一下。

首先,我们先回顾一下在第一章的时候,我们提到了一个反射方程,里面的BRDF函数是基于物理来计算的,具体的公式这里就不多阐述了,这里提供了链接方便回忆。

我们把需要计算的光源分为三部分,直接光照、间接光照、自发光。在第一章里,我们所介绍的是直接光照,而间接光照基本没有提及,下面先介绍一下间接光照的概念。

间接光照也可以理解为全局光照GI,顾名思义,即指物体受到间接的光照,它一般分为物体经过反射后的光和环境光,这里面可以大体地细分为间接漫反射间接镜面反射。对于间接漫反射来说,如果物体是静态的,则可以使用光照贴图lightmap来烘焙上去,如果是动态的,那么一般会使用一个光照探针。而对于镜面反射,我们一般会使用反射探针进行采样实现,这也可以被称为IBL的部分。

简单的介绍了一下间接光照的概念,所以在shader里,我们主要需要计算的便是五大部分,分别是直接光的漫反射直接光的高光反射间接光的漫反射间接光的高光反射自发光。在本文中,自发光暂不做考虑,考虑到在实时渲染下计算半球积分基本是无法实现的,所以我们把反射方程替换为以下的公式:

在这里插入图片描述
如果存在多个光源,我们最后在把光照的计算效果进行累加,下面为代码的编写。

首先我们定义了一些需要的基本的材质属性

Properties
    {    
        _Albedo("Albedo",2D) = "white"{} //主贴图
        _MainColor("Main Color",Color) = (1,1,1,1) //主色调
        _Metallic("Metallic",2D) = "white"{} // 金属度贴图
        _MetallicScale("MetallicScale",Range(0,1)) = 1 // 金属强度
        _NormalMap("Normal Map",2D) = "white"{} // 法线贴图
        _NormalScale("Scale",Float) = 1.0  // 凹凸程度
        _Occlusion("Occlusion",2D) = "white"{} // Ao贴图
        _Smoothness("Smothness",Range(0,1)) = 1 // 光滑度
        _LUT("LUT",2D) = "white"{} // Lut贴图
    }

输入结构体和输出结构体

//输入结构体用UnityCG.cginc里定义的,引入头文件即可
struct appdata_tan 
{
       float4 vertex : POSITION;
       float4 tangent : TANGENT;
       float3 normal : NORMAL;
       float4 texcoord : TEXCOORD0;
       UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
       float4 pos : SV_POSITION;
       float2 uv : TEXCOORD0;
       float4 m0 : TEXCOORD1;
       float4 m1 : TEXCOORD2;
       float4 m2 : TEXCOORD3;//xyz 存储着从切线空间到世界空间的变换矩阵,w存储着世界坐标
       SHADOW_COORDS(4)
};

顶点着色器

 v2f vv(appdata_tan v)
         {
             v2f o;
             o.pos = UnityObjectToClipPos(v.vertex);//把顶点转换到裁剪空间
             o.uv = TRANSFORM_TEX(v.texcoord,_Albedo);//计算uv偏移量
             float3 worldpos = mul(unity_ObjectToWorld,v.vertex).xyz;//在世界坐标的顶点
             float3 worldnormal = UnityObjectToWorldNormal(v.normal);//在世界坐标的法线
             float3 worldtangent = UnityObjectToWorldDir(v.tangent.xyz);//在世界坐标的切线
             float3 worldbinormal = cross(worldnormal,worldtangent) * v.tangent.w;//在世界坐标的副法线
             o.m0 = float4(worldtangent.x,worldbinormal.x,worldnormal.x,worldpos.x);//存储变换矩阵
             o.m1 = float4(worldtangent.y,worldbinormal.y,worldnormal.y,worldpos.y);
             o.m2 = float4(worldtangent.z,worldbinormal.z,worldnormal.z,worldpos.z);
             TRANSFER_SHADOW(o);//阴影计算
             return o;
         }

顶点函数里需要计算的与平时所写的shader差别不多,关键在于片元函数的光照计算

片元着色器

我们把需要计算的光照拆分成上述四项,我们首先计算光照需要用到的所有变量

            float3 worldpos = float3(i.m0.w,i.m1.w,i.m2.w);//世界坐标下的顶点
            UNITY_LIGHT_ATTENUATION(atten,i,worldpos);//通过内置宏计算光照衰减
            float3 L = normalize(UnityWorldSpaceLightDir(worldpos));//光照方向
            float3 V = normalize(UnityWorldSpaceViewDir(worldpos));//视角方向
            float3 N = UnpackNormal(tex2D(_NormalMap,i.uv));
            N.xy *= _NormalScale;
            N.z = sqrt(1.0 - saturate(dot(N.xy,N.xy)));
            N = normalize(half3(dot(i.m0.xyz,N),dot(i.m1.xyz,N),dot(i.m2.xyz,N)));//经过法线贴图采样转换到世界坐标的法线
            float4 Metal = tex2D(_Metallic,i.uv);//金属度贴图采样
            Metallic = Metal.r * _MetallicScale;//取贴图的r通道作为金属度
            Roughness = (1 - Metal.a * _Smoothness);//取贴图的a通道作为光滑度,但我们需要的是粗糙度,所以需要1 - 光滑度

            float3 H = normalize(V + L);//半角向量
            float NV = saturate(dot(N,V));//分别对应的点乘项
            float NL = saturate(dot(N,L));
            float NH = saturate(dot(N,H));
            float LV = saturate(dot(L,V));
            float LH = saturate(dot(L,H));

然后我们定义所有需要用到的模型,诸如D、F、G项等

            //Schlick菲涅耳近似等式F
            float3 fresnelSchlick(float cosTheta,float3 F0)
            {
                return F0 + (1 - F0) * pow(1.0 - cosTheta,5.0);
            }
            //引入粗糙度的菲涅耳公式
            float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
            {
                return F0 + (max(float3(1.0 - roughness,1.0 - roughness,1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
            }   
            //法线分布函数D 用到的是GGX模型
            float DistributionGGX(float NdotH,float roughness)
            {
                float a = pow(roughness,2);
                float a2 = pow(a,2);
                float fenmu = UNITY_PI * pow(pow(NdotH,2) * (a2 - 1) + 1,2);
                return a2 / max(fenmu,0.001);
            }
            //几何函数G 用到的是GGX模型
            float GeometrySchlickGGX(float NdotV, float roughness)
            {
                float r = pow(roughness,2);
                float k = pow(r + 1,2) / 8;
                float fenmu = NdotV * (1 - k) + k;
                return NdotV / max(fenmu,0.001);
            }
            //阴影-遮蔽的混合函数
            float GeometrySmith(float NdotV,float NdotL,float roughness)
            {
                float ggx1 = GeometrySchlickGGX(NdotV,roughness);
                float ggx2 = GeometrySchlickGGX(NdotL,roughness);
                return ggx1 * ggx2;
            }
            //基于物理的BRDF公式的高光反射项
            float3 BRDFspecular(float HdotL,float NdotH,float NdotV,float NdotL,float F0)
            {
                float3 F = fresnelSchlick(HdotL,F0);
                float NDF = DistributionGGX(NdotH,Roughness);
                float G = GeometrySmith(NdotV,NdotL,Roughness);
                float3 specular = NDF * F * G / max((4 * NdotV * NdotL),0.001);
                return specular;
            }

★直接光漫反射

这里用到的是基础的兰伯特模型,在漫反射项中基本为定值,所以我们可以很快的可以算出

Albedo = tex2D(_Albedo,i.uv) * _MainColor;//对主帖图进行采样
float3 F0 = float3(0.04,0.04,0.04);//定义物体的基础反射率,大部分都为0.04
F0 = lerp(F0,Albedo,Metallic);//考虑到如果为金属的话,基础反射率需要带有颜色信息,所以使用lerp进行处理

这里我在网上查阅到资料发现,在漫反射系数上,可以使用两种方法得到近似的值,第一种即使用通过菲涅耳计算得到的F作为高光反射率,而漫反射率则为1 - F,而这样计算可以保证出射光的比率永远不大于1,遵循了能量守恒定律

float3 Ks = fresnelSchlick(max(dot(H,V),0.0),F0);
float3 Kd = (float3(1.0,1.0,1.0) - Ks) * (1 - Metallic);

第二种则为使用Unity内置宏OneMinusReflectivityFromMetallic,经过两个系数效果的对比和资料的查询,对于菲涅耳作为系数来说,它从公式上更遵循物理,但效果上并没有使用内置宏好,这里读者可以自行取舍,因为PBR的变式数不胜数,正如图形学那句经典老话,“如果它看上去是对的,那么它就是对的”

float3 Kd = OneMinusReflectivityFromMetallic(Metallic);

另外别忘了在最后乘上1 - Metallic,因为金属几乎没有漫反射,而万物基本都有高光反射,所以在漫反射系数上我们需要做处理,让金属度参与进计算。

最后可以算出直接光的漫反射项,至于函数里的分母π,因为在上述公式展开后就被消掉了,所以不需要考虑

float3 diffuse = Kd * Albedo;

★直接光高光反射项

对于高光反射项,我们直接使用BRDFspecualar函数,即可得到

float3 specular = BRDFspecular(LH,NH,NV,NL,F0);

另外对于菲涅耳项的参数cosTheta有一个小小的拓展,对于传统的菲涅耳公式上来说,我们使用的是NV,即法线视角方向做点乘。但在PBR里,因为我们引入了微平面理论,而微平面是从微观角度上看待的,所以我们使用了半角向量H,用H代替了N,而在效果上,我们也会发现使用VH做点乘的效果会更好。而Unity使用的是LH,经查阅资料后,其实这是一种对GGX shader渲染效果的优化方法,可以在这里找到答案。

最后整合得到直接光的部分

float3 Lo = (diffuse + specular * UNITY_PI) * _LightColor0.rgb * NL;

★IBL部分

接下来便是刚刚介绍过的间接光照部分,即IBL。我们一样把它分为两个部分计算,间接光漫反射间接光镜面反射

首先,我们先从IBL部分说起,IBL全名即Image-Based Lighting 意思是基于纹理的光照。顾名思义,我们是从贴图上得到它们的光照信息。如果我们按照直接光的算法,我们是不可能计算所有不同的光线的各种反射和光路,当然在目前的光线追踪技术上,虽然我也不太理解,但是理论上是可以实现的,但是也需要极好的硬件支持。

★间接光漫反射

对于漫反射的计算,我们把反射公式拆解成两部分,BRDF的漫反射项是一个定值,我们可以直接算出。而剩下的积分项,我们可以把它近似的看成对CubeMap(环境贴图) 的采样,但这里我们并不需要做这个操作,因为Unity通过内置的一个宏,把相关的采样数据计算在了宏里。这个就是球谐函数,我们使用不同的球谐基底,便可以得到天空盒或光照探针的数据,对光照进行还原。最后再乘以漫反射系数,即为间接光的漫反射,在Unity里,漫反射可能还涉及到光照贴图的计算,这里暂不作考虑,代码和公式如下:

在这里插入图片描述

float3 ambient = 0.03 * Albedo; //计算基础的环境光
float3 sh = ShadeSH9(float4(N,1));//内置宏ShadeSH9计算相应的采样数据
float3 iblDiffuse = max(float3(0,0,0),sh + ambient.rgb);
float3 Flast = fresnelSchlickRoughness(max(dot(N,V), 0.0), F0, Roughness);//引入了粗糙度的菲涅耳项计算高光反射比例 反推出漫反射比例
float kd = (1 - Flast) * (1 - Metallic);//这里也可以使用直接光计算时使用的内置宏
iblDiffuse = iblDiffuse * Kd * Albedo;//最后乘上漫反射系数和原颜色 
//注意这里同样跟直接光一样把反射方程简化了出来 通过采样代替积分计算,所以不需要除系数π

★间接光镜面反射

对于镜面反射部分,因为数值不是恒定的,它依赖于入射光的方向和视角方向,所以同样很难做到实时计算。所以我们这里借鉴了UE4引擎里的近似算法split sum,并把它简化成两项LiBRDF。对于Li即光照信息部分(左边的部分),我们同样使用采样CubeMap来计算,但与漫反射不同的是,因为镜面反射受到粗糙度影响,所以我们根据粗糙度而使用对应的多个模糊程度不同的mipmap进行采样,公式如下:

在这里插入图片描述
对于左边的Li括号部分,代码如下:

float3 reflectDir = normalize(reflect(-V,N));//计算反射向量 使用该方向对CubeMap进行取样
float percetualRoughness = Roughness * (1.7 - 0.7 * Roughness);//因为粗糙度和mipmap的等级关系不是线性的 所以我们需要进行处理
float mip = percetualRoughness * 6;// 把数值范围映射到0到6之间,Unity默认的mip层级为6,也可以改为内置宏UNITY_SPECCUBE_LOD_STEPS
float4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflectDir,mip);//对CubeMap进行采样,unity_SpecCube0为天空盒或最近的反射探针的数据
float4 iblSpecular = float4(DecodeHDR(rgbm,unity_SpecCube0_HDR),1);//最后把采样到的颜色进行HDR解码处理

这样我们就得到了左边括号的值,对于右边括号的BRDF项,我们有两种做法可以进行模拟。首先第一种是传统的做法,即把值放进一张LUT查找图中,然后通过采样得到,采样坐标分别是NV粗糙度,代码如下:

在这里插入图片描述

float2 envBDRF = tex2D(_LUT, float2(lerp(0, 0.99, NV), lerp(0, 0.99, Roughness))).rg;//对LUT进行采样
float3 iblSpecularResult = iblSpecular * (Flast * envBDRF.r + envBDRF.g);//最后通过使用采样得到的r值进行缩放和g值进行偏移得到结果

但是我们都知道,采样操作的开销跟普通函数的计算是要大不少,而Unity内部的Standard也不是使用LUT图进行采样,而是使用了一个叫surfaceReduction的系数,以及一个在F0grazingTerm之间插值的菲涅耳系数,可以理解为上述公式的拟合函数,具体代码如下:

float grazingTerm = saturate(1 - Roughness + OneMinusReflectivityFromMetallic(Metallic.r));
float surfaceReduction = 1 / (pow(Roughness,2) + 1);
float3 iblSpecular= surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1.0),grazingTerm,NV);

这里我们还是使用常规的LUT采样,最后把四个计算好的光照进行叠加,返回颜色值

float ao = tex2D(_Occlusion,i.uv).r;//计算Ao环境光遮罩效果
float3 color = Lo + (iblSpecular + iblDiffuse) * ao;//Lo为直接计算的直接光部分,后面为IBL间接光部分,需要注意的是要乘上Ao贴图的系数
return fixed4(color,1.0); 

最后贴一下整体的代码:

Shader "Unlit/PBR"
{
    Properties
    {    
        _Albedo("Albedo",2D) = "white"{} //主贴图
        _MainColor("Main Color",Color) = (1,1,1,1) //主色调
        _Metallic("Metallic",2D) = "white"{} // 金属度贴图
        _MetallicScale("MetallicScale",Range(0,1)) = 1 // 金属强度
        _NormalMap("Normal Map",2D) = "white"{} // 法线贴图
        _NormalScale("Scale",Float) = 1.0  // 凹凸程度
        _Occlusion("Occlusion",2D) = "white"{} // Ao贴图
        _Smoothness("Smothness",Range(0,1)) = 1 // 光滑度
        _LUT("LUT",2D) = "white"{} // Lut贴图
    }
    SubShader
    {

        Pass
        {
            Tags{"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vv
            #pragma fragment ff
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 m0 : TEXCOORD1;
                float4 m1 : TEXCOORD2;
                float4 m2 : TEXCOORD3;
                SHADOW_COORDS(4)
            };
            
            sampler2D _Albedo;
            float4 _Albedo_ST;
            sampler2D _Metallic;
            sampler2D _NormalMap;
            sampler2D _Occlusion;
            sampler2D _LUT;
            float _NormalScale;
            float _Smoothness;
            float4 _MainColor;
            float _MetallicScale;
            uniform float3 Albedo;
            uniform float Metallic;
            uniform float Roughness;

            v2f vv(appdata_tan v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord,_Albedo);
                float3 worldpos = mul(unity_ObjectToWorld,v.vertex).xyz;
                float3 worldnormal = UnityObjectToWorldNormal(v.normal);
                float3 worldtangent = UnityObjectToWorldDir(v.tangent.xyz);
                float3 worldbinormal = cross(worldnormal,worldtangent) * v.tangent.w;
                o.m0 = float4(worldtangent.x,worldbinormal.x,worldnormal.x,worldpos.x);
                o.m1 = float4(worldtangent.y,worldbinormal.y,worldnormal.y,worldpos.y);
                o.m2 = float4(worldtangent.z,worldbinormal.z,worldnormal.z,worldpos.z);
                TRANSFER_SHADOW(o);
                return o;
            }
            float3 fresnelSchlick(float cosTheta,float3 F0)
            {
                return F0 + (1 - F0) * pow(1.0 - cosTheta,5.0);
            }
            float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
            {
                return F0 + (max(float3(1.0 - roughness,1.0 - roughness,1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
            }   
            float DistributionGGX(float NdotH,float roughness)
            {
                float a = pow(roughness,2);
                float a2 = pow(a,2);
                float fenmu = UNITY_PI * pow(pow(NdotH,2) * (a2 - 1) + 1,2);
                return a2 / max(fenmu,0.001);
            }
            float GeometrySchlickGGX(float NdotV, float roughness)
            {
                float r = pow(roughness,2);
                float k = pow(r + 1,2) / 8;
                float fenmu = NdotV * (1 - k) + k;
                return NdotV / max(fenmu,0.001);
            }
            float GeometrySmith(float NdotV,float NdotL,float roughness)
            {
                float ggx1 = GeometrySchlickGGX(NdotV,roughness);
                float ggx2 = GeometrySchlickGGX(NdotL,roughness);
                return ggx1 * ggx2;
            }
            float3 BRDFspecular(float HdotL,float NdotH,float NdotV,float NdotL,float F0)
            {
                float3 F = fresnelSchlick(HdotL,F0);
                float NDF = DistributionGGX(NdotH,Roughness);
                float G = GeometrySmith(NdotV,NdotL,Roughness);
                float3 specular = NDF * F * G / max((4 * NdotV * NdotL),0.001);
                return specular;
            }
            fixed4 ff(v2f i) : SV_TARGET
            {
                float3 worldpos = float3(i.m0.w,i.m1.w,i.m2.w);
                UNITY_LIGHT_ATTENUATION(atten,i,worldpos);
                float3 L = normalize(UnityWorldSpaceLightDir(worldpos));
                float3 V = normalize(UnityWorldSpaceViewDir(worldpos));
                float3 N = UnpackNormal(tex2D(_NormalMap,i.uv));
                N.xy *= _NormalScale;
                N.z = sqrt(1.0 - saturate(dot(N.xy,N.xy)));
                N = normalize(half3(dot(i.m0.xyz,N),dot(i.m1.xyz,N),dot(i.m2.xyz,N)));
                float4 Metal = tex2D(_Metallic,i.uv);
                Metallic = Metal.r * _MetallicScale;
                Roughness = (1 - Metal.a * _Smoothness);

                float3 H = normalize(V + L);
                float NV = saturate(dot(N,V));
                float NL = saturate(dot(N,L));
                float NH = saturate(dot(N,H));
                float LV = saturate(dot(L,V));
                float LH = saturate(dot(L,H));

                Albedo = tex2D(_Albedo,i.uv) * _MainColor;//对主帖图进行采样
                float3 F0 = float3(0.04,0.04,0.04);//定义物体的基础反射率,大部分都为0.04
                F0 = lerp(F0,Albedo,Metallic);//考虑到如果为金属的话,基础反射率需要带有颜色信息,所以使用lerp进行处理
                
                float3 Ks = fresnelSchlick(max(dot(H,V),0.0),F0);
                float3 Kd = (float3(1.0,1.0,1.0) - Ks) * (1 - Metallic);
                //另一种漫反射系数的算法
                //float3 Kd = OneMinusReflectivityFromMetallic(Metallic);
               
                float3 diffuse = Kd * Albedo;
                float3 specular = BRDFspecular(LH,NH,NV,NL,F0);
                float3 Lo = (diffuse + specular * UNITY_PI) * _LightColor0.rgb * NL;

                float3 ambient = 0.03 * Albedo;//计算基础的环境光
                float3 sh = ShadeSH9(float4(N,1));//内置宏ShadeSH9计算相应的采样数据
                float3 iblDiffuse = max(float3(0,0,0),sh + ambient.rgb);
                float3 Flast = fresnelSchlickRoughness(max(dot(N,V), 0.0), F0, Roughness);//引入了粗糙度的菲涅耳项计算高光反射比例 反推出漫反射比例
				float kd = (1 - Flast) * (1 - Metallic);//这里也可以使用直接光计算时使用的内置宏
                iblDiffuse *= Kd * Albedo;//最后乘上漫反射系数和兰伯特定值
                //注意这里同样跟直接光一样把反射方程简化了出来 通过采样代替积分计算,所以不需要除系数π

                float3 reflectDir = normalize(reflect(-V,N));//计算反射向量 使用该方向对CubeMap进行取样
                float percetualRoughness = Roughness * (1.7 - 0.7 * Roughness);//因为粗糙度和mipmap的等级关系不是线性的 所以我们需要进行处理
                float mip = percetualRoughness * 6;// 把数值范围映射到0到6之间,Unity默认的mip层级为6,也可以改为内置宏UNITY_SPECCUBE_LOD_STEPS
                float4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflectDir,mip);//对CubeMap进行采样,unity_SpecCube0为天空盒或最近的反射探针的数据
                float3 iblSpecular = DecodeHDR(rgbm,unity_SpecCube0_HDR);//最后把采样到的颜色进行HDR解码处理

                
                float2 envBDRF = tex2D(_LUT, float2(lerp(0, 0.99, NV), lerp(0, 0.99, Roughness))).rg; // LUT采样
                iblSpecular *= (Flast * envBDRF.r + envBDRF.g);//最后通过使用采样得到的r值进行缩放和g值进行偏移得到结果

                //instead for LUT
                // float grazingTerm = saturate(1 - Roughness + OneMinusReflectivityFromMetallic(Metallic.r));
                // float surfaceReduction = 1 / (pow(Roughness,2) + 1);
                // iblSpecular = surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1.0),grazingTerm,NV);

                float ao = tex2D(_Occlusion,i.uv).r;//计算Ao环境光遮罩效果
                float3 color = Lo + (iblSpecular + iblDiffuse) * ao;//Lo为直接计算的直接光部分,后面为IBL间接光部分,需要注意的是要乘上Ao贴图的系数
                
                return fixed4(color,1.0); 

            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

最后也是基本上把一个pbr的雏形给写出来了,当然对比Untiy自带的standard,还有很多不足的地方,而且pbr的变式也非常非常的多,光是我阅读查阅资料时,就发现了好多不同的写法和模型公式,这里只是用了最为常用的一种写法。接下来还要继续深入学习pbr,做到可以灵活运用修改在不同的项目中。

最后附上这篇文章参考过和阅读过的资料:

1.如何在Unity中造一个PBR Shader轮子

2.猴子都能看懂的PBR

3.从零开始在Unity中写一个PBR着色器

4.Unity PBR Standard Shader 实现详解 (三)全局光照函数计算

5.由浅入深学习PBR的原理和实现

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值