Unity-Shader详解-其一
今天我们来介绍Unity的一大核心组件:shader。
Shader
Shader就是我们的着色器,用于控制图形的渲染的计算和生成。
对于不同的引擎,具体实现渲染的方法也不一样,也就是我们俗称的不同的图形引擎API,比如OpenGL,DirectX,Vulkan,而我们的Unity的图形引擎则是封装了多个API来组成的。
我们在Unity项目里创建->着色器->无光着色器,就可以得到下述代码:
Shader "Unlit/NewUnlitShader"
{Properties{_MainTex ("Texture", 2D) = "white" {}}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag// make fog work#pragma multi_compile_fog#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// sample the texturefixed4 col = tex2D(_MainTex, i.uv);// apply fogUNITY_APPLY_FOG(i.fogCoord, col);return col;}ENDCG}}
}
当然这里我们针对所谓的无光照着色器还是得先说明一下,我们可以使用的Unity着色器分为:
无光照着色器(Unlit Shader)、图像特效着色器(Image Effect Shader)、表面着色器(Surface Shader)、计算着色器(Compute Shader)和光线追踪着色器(Ray Tracing Shader)等。
后续我们可能会一个个介绍,但是在最开始,我们把目光放在最基础的无光照着色器上。
既然都叫无光照着色器了,显然我们不考虑在这个着色器上计算光照。我们会直接把颜色或者纹理在着色器渲染的物体上生成,没有复杂的光照计算。
回到代码本身,我们可以看到着色器总的由两个大块组成:Properties和SubShader。
Properties
Properties{_MainTex ("Texture", 2D) = "white" {}}
_MainTex,懂英语的应该都知道这是主纹理的意思,参数里有一个"Texture",代表纹理类型。
Properties就是属性的意思,比如我们定义了一个纹理是白色的,他的固定句式是:
针对数字或者滑动条:
name ("display name", Range (min, max)) = number
name ("display name", Float) = number
name ("display name", Int) = number
针对颜色或者矢量(颜色本质上是一个三维(四维)矢量):
name ("display name", Color) = (number,number,number,number)
name ("display name", Vector) = (number,number,number,number)
针对纹理:
name ("display name", 2D) = "defaulttexture" {}
name ("display name", Cube) = "defaulttexture" {}
name ("display name", 3D) = "defaulttexture" {}
在着色器的代码中,我们用name处的名称使用变量,但是在材质(Material)处,看到的是display name。
Subshader
然后是我们的SubShader:
Tags { "RenderType"="Opaque" }LOD 100
对于每一个Unity的着色器来说,其中都必须包含子着色器,我们真正调用的也是子着色器。
Tags,翻译为标签。在着色器中每一个Tags就是对应的一系列子着色器的一些标注,比如这里是规定渲染类型为不透明物体,LOD(Level of Details)细节层级为100,当计算机性能不够时可能会自动降低LOD。
一些其他常见的Tags如下:
Pass,则是一个具体定义渲染流程的部分,在这里我们会展开具体的子着色器写法。
CGPROGRAM#pragma vertex vert#pragma fragment frag// make fog work#pragma multi_compile_fog#include "UnityCG.cginc"
CGPROGRAM一般和ENDCG一起使用,中间的部分表明是可编程的渲染部分,有图形学学习经验的小伙伴应该都知道,整个渲染流程我们真正可以用代码控制的部分其实主要就是顶点着色器和片元着色器这两部分,一般这两个着色器的代码我们也放在CGPROGRAM和ENDCG之间(CG是Unity书写着色器的语言) 。
#pragma则是一个编译命令,主要作用就是声明顶点着色器和片元着色器的函数:
#include 就不介绍了,如果不认识include建议是重修一下大一程序语言课。
struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_ST;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// sample the texturefixed4 col = tex2D(_MainTex, i.uv);// apply fogUNITY_APPLY_FOG(i.fogCoord, col);return col;}ENDCG
这里就是具体的着色器的内容了,首先是两个结构体:appdata和v2f,我们看到顶点的定义后面有一个:POSITION,这是什么意思呢?
说白了就是一个程序告诉计算机我们的变量具体存储的是模型空间里的什么内容的语法糖。
appdata结构体定义了我们具体着色器要操作的对象:顶点坐标和纹理坐标。
v2f结构体则是告诉我们顶点着色器的输出来作为片元着色器的输入的对象:依然是顶点坐标和纹理坐标,以及一个奇怪的东西:
struct v2f{float2 uv : TEXCOORD0;UNITY_FOG_COORDS(1)float4 vertex : SV_POSITION;};
没有如下,简单地说这就是一个宏,作用是帮你生成一个烟雾特效,对应的参数是对应的作为纹理坐标的索引(我们定义了第一个纹理坐标索引为0,这里相当于新增了一个Unity自带的烟雾效果的纹理坐标索引为1)。
还有我们的顶点坐标似乎多了一个SV的前缀,这是什么意思呢?其实就是不同空间坐标的顶点坐标,SV对应的是裁剪空间。
sampler2D _MainTex;float4 _MainTex_ST;v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);UNITY_TRANSFER_FOG(o,o.vertex);return o;}fixed4 frag (v2f i) : SV_Target{// sample the texturefixed4 col = tex2D(_MainTex, i.uv);// apply fogUNITY_APPLY_FOG(i.fogCoord, col);return col;}
这里就是具体的顶点着色器和片元着色器的定义了,顶点着色器把appdata类型变量作为输入而把v2f类型变量作为返回值,代码中声明一个v2f实例o,o的顶点坐标和纹理坐标分别用了两个不同的函数来得到:UnityObjectToClipPos和TRANSFORM_TEX。
不展开细聊的话,我们知道一般从模型空间得到裁剪空间的顶点坐标,然后允许动态调整纹理坐标就是通过这两个函数得到的即可。
UNITY_TRANSFER_FOG(o,o.vertex)则又是一个内置宏,含义就是给o的顶点坐标施加之前声明的FOG特效。
片元着色器这边:以v2f作为输入而输出fixed4类型的变量,这里后续跟的:SV_Target是:
我们从i的UV坐标上采样纹理的颜色值,然后与Unity自带的烟雾颜色插值混合,最后返回。
概念补充:
虽然很短的几十行代码,其背后蕴含的原理非常的多,我来稍微做一些补充和介绍。
语义:
系统值语义往往代表特殊的关键数据,比如SV_POSITION就是裁剪空间的坐标,SV_TARGET则是片元着色器的最终输出颜色。
不同的空间坐标:
Unity总的来说有多种空间坐标系,在我们的顶点着色器运作的时候,大致的流程和变化情况是这样的:
模型→世界→视图→裁剪
模型坐标指的就是以模型自我为中心的局部坐标系,在最开始时,场景中没有物体,我们要现根据一个个模型空间的坐标创建出模型之后才加入场景;世界坐标则是针对场景来说,此时所有场景中的模型会共享一个同样的世界坐标;视图坐标则是针对摄像机的坐标系,以摄像机为原点,分别计算不同物体相对于摄像机的位置;最后的裁剪空间就是根据摄像机的参数设置(视锥体大小,远近平面)从视图空间中裁剪出来的空间,最后经过透视除法和视口变化后就得到了我们摄像机中最后可以看到的场景。
关于颜色插值:
插值往往作为渐变或者平滑过渡的手段,尤其在颜色、纹理中有用。
基础光照
兰伯特
简单地说,兰伯特光照模型就是理想漫反射光照模型,我们只用考虑光源方向和法线的夹角来计算反射光线是否存在即可。
逐顶点和逐像素?
根本差别就在于我们的光照的计算是在顶点着色器还是在片元着色器中完成。
让我们分别实现这样的光照模型。
Shader "Chapter3/chapter3_1"
{// Shader 属性(Properties)部分:定义了外部可以调整的参数。Properties{// _MainTex:纹理贴图,默认值为白色纹理_MainTex ("Texture", 2D) = "white" {}}SubShader{// SubShader 标签(Tags):定义了渲染模式与渲染顺序Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }LOD 100 // 设置最低的细节等级(LOD)// Pass 说明渲染过程的一个阶段。每个 Pass 都会执行顶点着色器和片段着色器。Pass{CGPROGRAM// 声明使用的着色器语言#pragma vertex vert#pragma fragment frag// 包含 Unity 的常用着色器代码库#include "UnityCG.cginc"// 包含 Unity 的光照模型代码库#include "Lighting.cginc"// 定义输入结构体,传入顶点着色器的数据struct appdata{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:顶点位置float4 vertex : POSITION;// normal:表面法线float3 normal : NORMAL;};// 定义输出结构体,传递数据到片段着色器struct v2f{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:变换后的顶点位置(裁剪空间位置)float4 vertex : SV_POSITION;// color:计算得到的漫反射光照颜色fixed3 color : COLOR;};// 定义 uniform 变量,用于存储外部传入的数据uniform sampler2D _MainTex; // 纹理采样器uniform float4 _MainTex_ST; // 纹理的平移和缩放参数// 顶点着色器:负责处理顶点数据并进行必要的变换v2f vert(appdata v){v2f o;// 将物体空间的顶点转换为裁剪空间的顶点o.vertex = UnityObjectToClipPos(v.vertex);// 变换纹理坐标o.uv = TRANSFORM_TEX(v.uv, _MainTex);// 计算世界空间的法线:将物体空间法线转换为世界空间fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));// 计算世界空间的光照方向:获取光源方向并标准化fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// 计算漫反射光照强度(Dot product):计算法线与光源方向的点积并应用光源的颜色fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));// 将计算的漫反射颜色传递给输出o.color = diffuse;// 返回处理后的数据return o;}// 片段着色器:负责计算每个像素的颜色fixed4 frag(v2f i) : SV_Target{// 采样纹理颜色fixed4 color = tex2D(_MainTex, i.uv);// 将纹理颜色与漫反射光照强度相乘,得到最终的颜色color.rgb = color.rgb * i.color;// 返回最终颜色return color;}ENDCG}}
}
首先依然是定义好基础的纹理。
定义渲染物体为非透明物体,同时光照模式为前向渲染。
作为顶点着色器输入的数据有顶点坐标,纹理坐标和法线向量,顶点着色器传入片元着色器的数据有顶点坐标,纹理坐标和经由顶点着色器计算得到的光照颜色值。
// 顶点着色器:负责处理顶点数据并进行必要的变换v2f vert(appdata v){v2f o;// 将物体空间的顶点转换为裁剪空间的顶点o.vertex = UnityObjectToClipPos(v.vertex);// 变换纹理坐标o.uv = TRANSFORM_TEX(v.uv, _MainTex);// 计算世界空间的法线:将物体空间法线转换为世界空间fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));// 计算世界空间的光照方向:获取光源方向并标准化fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// 计算漫反射光照强度(Dot product):计算法线与光源方向的点积并应用光源的颜色fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));// 将计算的漫反射颜色传递给输出o.color = diffuse;// 返回处理后的数据return o;}
顶点着色器中把顶点转换成裁剪空间的顶点传入,纹理坐标设置为可变换,然后计算世界空间中的法线方向和光照方向 ,最后计算总的漫反射光照强度。
最后计算的代码中:saturate代表[0,1],最后得到一个[0,1]的颜色值。
效果如图:
逐像素的话:
Shader "Chapter3/chapter3_1_frag"
{Properties{// _MainTex: 纹理贴图,默认值为白色纹理_MainTex ("Texture", 2D) = "white" {}}SubShader{// 设置渲染类型为“不透明”,并指定光照模式为“ForwardBase”。Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }LOD 100 // 设置最低的细节等级(LOD)// Pass阶段定义了渲染的一次完整过程。每个Pass包含顶点着色器和片段着色器的执行。Pass{CGPROGRAM// 声明顶点着色器和片段着色器#pragma vertex vert#pragma fragment frag// 引入Unity的常用着色器代码库#include "UnityCG.cginc"// 引入Unity的光照模型代码库#include "Lighting.cginc"// 顶点输入结构体,包含从模型传入的顶点数据struct appdata{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:物体空间中的顶点位置float4 vertex : POSITION;// normal:物体空间中的法线float3 normal : NORMAL;};// 顶点输出结构体,将数据传递到片段着色器struct v2f{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:裁剪空间中的顶点位置float4 vertex : SV_POSITION;// worldNormal:转换到世界空间的法线float3 worldNormal : TEXCOORD1;};// uniform变量:用于从外部传入的数据uniform sampler2D _MainTex; // 纹理采样器uniform float4 _MainTex_ST; // 纹理的平移和缩放参数// 顶点着色器:将顶点从物体空间转换到裁剪空间,并计算法线的世界空间表示v2f vert(appdata v){v2f o;// 将物体空间的顶点转换为裁剪空间o.vertex = UnityObjectToClipPos(v.vertex);// 变换纹理坐标o.uv = TRANSFORM_TEX(v.uv, _MainTex);// 将法线从物体空间转换到世界空间o.worldNormal = UnityObjectToWorldNormal(v.normal);return o;}// 片段着色器:根据纹理和光照计算每个像素的最终颜色fixed4 frag(v2f i) : SV_Target{// 从纹理中采样颜色fixed4 color = tex2D(_MainTex, i.uv);// 规范化法线(确保长度为1)fixed3 worldNormal = normalize(i.worldNormal);// 获取世界空间的光源方向并规范化fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// 计算漫反射光照强度:点积计算法线与光源方向的夹角fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));// 将纹理颜色与漫反射光照强度相乘,得到最终颜色color.rgb *= diffuse;// 返回最终颜色return color;}ENDCG}}
}
本质区别只是把原来放在顶点着色器中的光照计算流程放置在了片元着色器中。
额,看起来好像没啥区别,不过大家知道是怎么回事就可以。
半兰伯特
可以看到对于兰伯特光照模型而言,计算公式中对于背光面就是简单粗暴的抹零,这显然不太符合现实规律,我们在此基础上可以加些东西。
我们直接来看代码怎么写,同样分逐顶点和逐像素:
逐顶点:
Shader "Chapter3/chapter3_2_vertex_half"
{Properties{// _MainTex:纹理属性,默认值为白色纹理_MainTex ("Texture", 2D) = "white" {}}SubShader{// Tags:设置渲染类型为“Opaque”(不透明),光照模式为“ForwardBase”(前向渲染基础光照)Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }LOD 100 // 设置最低的细节等级(LOD)// Pass阶段定义了渲染的一次完整过程。每个Pass包含顶点着色器和片段着色器的执行Pass{CGPROGRAM// 声明顶点着色器和片段着色器#pragma vertex vert#pragma fragment frag// 引入Unity常用的着色器代码库#include "UnityCG.cginc"// 引入Unity光照模型代码库#include "Lighting.cginc"// 顶点输入结构体:传入从模型中来的数据struct appdata{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:物体空间中的顶点位置float4 vertex : POSITION;// normal:物体空间中的法线float3 normal : NORMAL;};// 顶点输出结构体:传递数据给片段着色器struct v2f{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:裁剪空间中的顶点位置float4 vertex : SV_POSITION;// color:在顶点着色器中计算的颜色,传递给片段着色器fixed3 color : COLOR;};// uniform变量:用于从外部传入的数据uniform sampler2D _MainTex; // 纹理采样器uniform float4 _MainTex_ST; // 纹理的平移和缩放参数// 顶点着色器:将顶点从物体空间转换为裁剪空间,并计算颜色v2f vert(appdata v){v2f o;// 将物体空间中的顶点转换为裁剪空间中的顶点o.vertex = UnityObjectToClipPos(v.vertex);// 将纹理坐标进行变换o.uv = TRANSFORM_TEX(v.uv, _MainTex);// 将法线从物体空间转换到世界空间,并规范化(使长度为1)fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));// 获取光源的世界空间位置并规范化fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// 计算漫反射光照:将法线与光源的方向计算点积,并进行平滑处理// (dot(worldNormal, worldLight) * 0.5 + 0.5) 用于让点积的范围变为 [0, 1],以适应颜色计算fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);// 将计算出的颜色传递给输出结构体o.color = diffuse;return o;}// 片段着色器:根据纹理和计算的光照颜色来生成最终像素的颜色fixed4 frag(v2f i) : SV_Target{// 从纹理中采样颜色fixed4 color = tex2D(_MainTex, i.uv);// 将采样到的颜色与漫反射光照强度相乘,得到最终的颜色color.rgb = color.rgb * i.color;// 返回最终颜色return color;}ENDCG}}
}
其中的核心变化:
fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
效果如图:
逐像素:
Shader "Chapter3/chapter3_2_frag_half"
{Properties{// _MainTex:纹理属性,默认值为白色纹理_MainTex ("Texture", 2D) = "white" {}}SubShader{// Tags:渲染类型为“不透明”,光照模式为“ForwardBase”(前向渲染基础光照)Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }LOD 100 // 设置着色器的最低细节等级(LOD)// Pass阶段定义了渲染的具体过程,包括顶点着色器和片段着色器Pass{CGPROGRAM// 声明顶点着色器(vert)和片段着色器(frag)#pragma vertex vert#pragma fragment frag// 引入Unity常用的着色器代码库#include "UnityCG.cginc"// 引入Unity的光照模型代码库#include "Lighting.cginc"// 顶点输入结构体:传入从模型中来的数据struct appdata{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:物体空间中的顶点位置float4 vertex : POSITION;// normal:物体空间中的法线float3 normal : NORMAL;};// 顶点输出结构体:传递数据给片段着色器struct v2f{// uv:纹理坐标float2 uv : TEXCOORD0;// vertex:裁剪空间中的顶点位置float4 vertex : SV_POSITION;// worldNormal:世界空间中的法线float3 worldNormal : TEXCOORD1;};// uniform变量:用于从外部传入的数据uniform sampler2D _MainTex; // 纹理采样器uniform float4 _MainTex_ST; // 纹理的平移和缩放参数// 顶点着色器:将顶点从物体空间转换为裁剪空间,并计算世界空间中的法线v2f vert(appdata v){v2f o;// 将物体空间中的顶点转换为裁剪空间中的顶点o.vertex = UnityObjectToClipPos(v.vertex);// 将纹理坐标进行变换o.uv = TRANSFORM_TEX(v.uv, _MainTex);// 将法线从物体空间转换到世界空间,并规范化(确保法线长度为1)o.worldNormal = UnityObjectToWorldNormal(v.normal);return o;}// 片段着色器:根据纹理和计算的光照颜色来生成最终像素的颜色fixed4 frag(v2f i) : SV_Target{// 从纹理中采样颜色fixed4 color = tex2D(_MainTex, i.uv);// 将世界空间中的法线规范化fixed3 worldNormal = normalize(i.worldNormal);// 获取光源的世界空间位置,并规范化fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);// 计算漫反射光照,计算法线与光源方向的点积并进行平滑处理// (dot(worldNormal, worldLight) * 0.5 + 0.5) 用于让点积的范围变为 [0, 1]fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);// 将纹理的颜色与漫反射光照颜色相乘,得到最终的颜色color.rgb *= diffuse;return color;}ENDCG}}
}
效果如图:
冯模型
兰伯特模型是只考虑漫反射的光照模型,但是显然真正的光照模型不仅仅只有这么一个考量。
我们在漫反射的基础上加入一个作为环境光的常量和一个反射光组成我们的冯光照模型。
显然这众多参数之中,N,I,V向量都是可以直接拿到的,但是R(反射方向)需要我们去进行计算。
于是我们得到了所有计算的方法:
Shader "Chapter3/chapter3_3_phong"
{Properties{// 漫反射颜色_Diffuse("Diffuse", Color) = (1,1,1,1)// 镜面反射颜色_Specular("Specular", Color) = (1, 1, 1, 1)// 光泽度(控制镜面高光的锐利程度)_Gloss("Gloss", Range(8.0, 256)) = 20}SubShader{Pass{Tags{// 设置光照模式为ForwardBase,适用于前向渲染"LightMode" = "ForwardBase"}CGPROGRAM// 声明顶点着色器和片段着色器的函数#pragma vertex vert#pragma fragment frag// 引入Unity的光照计算库#include "Lighting.cginc"// 声明传递给着色器的属性fixed4 _Diffuse; // 漫反射颜色fixed4 _Specular; // 镜面反射颜色float _Gloss; // 光泽度// 顶点数据结构struct a2v{float4 vertex : POSITION; // 顶点位置float3 normal : NORMAL; // 顶点法线};// 传递给片段着色器的数据结构struct v2f{float4 pos : SV_POSITION; // 顶点位置,最终转换为裁剪空间坐标float3 worldNormal : TEXCOORD0; // 世界空间中的法线float3 worldPos : TEXCOORD1; // 世界空间中的位置};// 顶点着色器v2f vert(a2v v){v2f o;// 将物体空间的顶点坐标转换为裁剪空间坐标o.pos = UnityObjectToClipPos(v.vertex);// 将物体空间中的法线转换为世界空间法线o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// 获取物体在世界空间中的位置o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}// 片段着色器fixed4 frag(v2f i) : SV_Target{// 归一化世界空间中的法线fixed3 worldNormal = normalize(i.worldNormal);// 归一化光源方向fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// 计算反射方向fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));// 计算观察方向fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);// 环境光(可以在Lighting.cginc中找到计算)fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// 漫反射光照(根据法线和光源方向的点积计算)fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));// 镜面反射光照(反射方向与视线方向的点积,经过光泽度指数计算)fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);// 返回最终的颜色,包含环境光、漫反射光和镜面反射光return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Diffuse" // 如果无法使用此着色器,则使用内建的Diffuse着色器作为回退
}
效果如图:
布林-冯模型
布林-冯模型可以看作是冯模型的优化版本:
代码如下:
Shader "Chapter3/chapter3_3_blinn_phong"
{Properties{// 漫反射颜色_Diffuse("Diffuse", Color) = (1,1,1,1)// 镜面反射颜色_Specular("Specular", Color) = (1, 1, 1, 1)// 光泽度(控制镜面高光的锐利程度)_Gloss("Gloss", Range(8.0, 256)) = 20}SubShader{Pass{Tags{// 设置光照模式为ForwardBase,适用于前向渲染"LightMode" = "ForwardBase"}CGPROGRAM// 声明顶点着色器和片段着色器的函数#pragma vertex vert#pragma fragment frag// 引入Unity的光照计算库#include "Lighting.cginc"// 声明传递给着色器的属性fixed4 _Diffuse; // 漫反射颜色fixed4 _Specular; // 镜面反射颜色float _Gloss; // 光泽度// 顶点数据结构struct a2v{float4 vertex : POSITION; // 顶点位置float3 normal : NORMAL; // 顶点法线};// 传递给片段着色器的数据结构struct v2f{float4 pos : SV_POSITION; // 顶点位置,最终转换为裁剪空间坐标float3 worldNormal : TEXCOORD0; // 世界空间中的法线float3 worldPos : TEXCOORD1; // 世界空间中的位置};// 顶点着色器v2f vert(a2v v){v2f o;// 将物体空间的顶点坐标转换为裁剪空间坐标o.pos = UnityObjectToClipPos(v.vertex);// 将物体空间中的法线转换为世界空间法线o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// 获取物体在世界空间中的位置o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;return o;}// 片段着色器fixed4 frag(v2f i) : SV_Target{// 归一化世界空间中的法线fixed3 worldNormal = normalize(i.worldNormal);// 归一化光源方向fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);// 计算观察方向(从世界空间中的物体位置到摄像机位置)fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);// 计算半程向量(Blinn-Phong模型的关键)fixed3 halfDir = normalize(worldLightDir + viewDir);// 环境光(从Lighting.cginc中获取)fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;// 漫反射光照(根据法线和光源方向的点积计算)fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));// 镜面反射光照(Blinn-Phong模型,使用半程向量和法线的点积,经过光泽度指数计算)fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);// 返回最终的颜色,包含环境光、漫反射光和镜面反射光return fixed4(ambient + diffuse + specular, 1.0);}ENDCG}}FallBack "Diffuse" // 如果无法使用此着色器,则使用内建的Diffuse着色器作为回退
}
和布林-冯模型最大的区别:
// 计算半程向量(Blinn-Phong模型的关键)
fixed3 halfDir = normalize(worldLightDir + viewDir);
...
// 镜面反射光照(Blinn-Phong模型,使用半程向量和法线的点积,经过光泽度指数计算)
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
多了一个半程向量的计算并更改了高光反射计算的方法。
效果如下:
可以看到高光的范围更大了,且这样减少了计算量。
相关文章:
Unity-Shader详解-其一
今天我们来介绍Unity的一大核心组件:shader。 Shader Shader就是我们的着色器,用于控制图形的渲染的计算和生成。 对于不同的引擎,具体实现渲染的方法也不一样,也就是我们俗称的不同的图形引擎API,比如OpenGL,Direct…...
WPF与C++ 动态库交互
WPF与C++动态库交互技术详解 一、基本交互方式概述 WPF应用程序与C++动态库交互主要有以下几种方式: P/Invoke调用(平台调用)COM互操作C++/CLI桥接层内存映射文件命名管道/Socket通信本文将重点介绍最常用的P/Invoke和C++/CLI两种…...
自动化测试实战篇
文章目录 目录1. 自动化实施步骤1.1 编写web测试用例1.2 自动化测试脚本开发1.3 测试报告 目录 自动化实施步骤 1. 自动化实施步骤 1.1 编写web测试用例 注: 因为这里仅作为演示,所以设计的用例并不是非常完整 1.2 自动化测试脚本开发 # common/Util…...
基于pandoc的MarkDown格式与word相互转换小工具开发(pyqt5)
这里写目录标题 开发目标准备工作源代码程序打包其他事项命令行使用pandoc关于pandoc默认表格无边框的说明 开发目标 采用word格式模板,实现高级定制样式。具备配置保存功能,方便快捷。自定义转换选项、pandoc路径。 准备工作 开发环境:Wi…...
JVM知识点(一)---内存管理
一、JVM概念 什么是JVM? 定义: Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境) 好处: 一次编写,到处运行自动内存管理,垃圾回收功能数组下标越界越界检查多态 比较jvm jre jdk区别 学习路…...
Apache NetBeans 25 发布
Apache NetBeans 25 已于 2025 年 2 月 20 日发布3。NetBeans 是一个主要面向 Java 的集成开发环境,同时支持 C/C、PHP、JavaScript 和其他编程语言1。以下是一些主要的更新内容: Gradle 的优化与增强:优化单文件测试功能,即使测试…...
【设计模式区别】装饰器模式和适配器模式区别
装饰器模式(Decorator Pattern)和适配器模式(Adapter Pattern)都是 结构型设计模式 或者说 包装模式 (Wrapper),用于解决对象的组合和扩展问题,但它们的核心目的、结构和使用场景有显…...
矫平机终极指南:特殊材料处理、工艺链协同与全球供应链管理
一、特殊材料矫平:挑战与创新解决方案 1. 高温合金(如Inconel 718)处理 技术难点: 屈服强度高达1100 MPa,传统矫平力不足 高温下易氧化,需惰性气体保护环境 解决方案: 采用双伺服电机驱动&a…...
stm32进入睡眠模式的几个注意点
(1)关闭systick (2)先关闭外设时钟,再屏蔽中断,避免先屏蔽中断再关闭外设时钟导致中断挂起无法进入睡眠模式(立即被唤醒)。 参考: 注:图片截自《RM0433参考手…...
深入理解网络安全中的加密技术
1 引言 在当今数字化的世界中,网络安全已经成为个人隐私保护、企业数据安全乃至国家安全的重要组成部分。随着网络攻击的复杂性和频率不断增加,保护敏感信息不被未授权访问变得尤为关键。加密技术作为保障信息安全的核心手段,通过将信息转换为…...
学习设计模式《六》——抽象工厂方法模式
一、基础概念 抽象工厂模式的本质是【选择产品簇(系列)的实现】; 抽象工厂模式定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类; 抽象工厂模式功能:抽象工厂的功能是为一系列相关对象或相互依…...
MySQL 数据类型
文章目录 数据类型数据类型分类数据类型tinyint类型(整型)总结bit类型(字节) 浮点类型float类型decimal类型 字符串类型char类型varchar(变长字符串) char 和 varchar的对比日期类型enum和set类型ÿ…...
基于Tcp协议的应用层协议定制
前言:本文默认读者已掌握 TCP 协议相关网络接口知识,将聚焦于应用层协议的设计与剖析,有关底层通信机制及业务逻辑部分仅作简要概述,不再展开详述。 目录 服务器 一、通信 二、协议 1.序列化与反序列化 2. 封包与解包 三、业…...
Flink反压问题解析
一、什么是反压(Backpressure)? 反压(Backpressure) 是流处理系统中的一种流量控制机制。当下游算子处理速度低于上游数据生产速度时,系统会向上游传递压力信号,迫使上游降低数据发送速率,避免数据堆积和系统崩溃。 Flink 通过动态反压机制实现这一过程,但其副作用是…...
C语言中结构体的字节对齐的应用
一、字节对齐的基本原理 计算机的内存访问通常以固定大小的块(如 4 字节、8 字节)为单位。若数据的内存地址是块大小的整数倍,称为 自然对齐。例如: int(4 字节)的地址应为 4 的倍数。 double(…...
大规模数据同步后数据总条数对不上的系统性解决方案:从字段映射到全链路一致性保障
一、引言 在数据同步(如系统重构、分库分表、多源整合)场景中,“本地数据一致,生产环境条数对不上”是典型痛点。问题常源于并发处理失控、数据库性能瓶颈、字段映射错误、缓存脏数据等多维度缺陷。本文结合实战经验,…...
美团Java后端二面面经!
场景题是面试的大头,建议好好准备 Q. [美团]如何设计一个外卖订单的并发扣减库存系统? Q.[美团]为啥初始标记和重新标记需要STW? Q.[美团]骑手位置实时更新,如何保证高并发写入? Q.[美团]订单表数据量过大导致查询…...
35-疫苗预约管理系统(微服务)
技术: RuoYi框架 后端: SpringBootMySQLspringCloudnacosRedis 前端: vue3 环境: Idea mysql maven jdk1.8 用户端功能 1.首页:展示疫苗接种须知标语、快速预约模块 2.疫苗列表:展示可接种的疫苗 3.预约接种: 用户可进行疫苗预约接种 修改预约时间 …...
Ext JS模拟后端数据之SimManager
Ext.ux.ajax.SimManager 是 Ext JS 框架中用于拦截 Ajax 请求并返回模拟数据的核心工具,适用于前后端分离开发、原型验证或独立测试场景。它通过配置灵活的规则和模拟处理器(Simlet),帮助开发者在不依赖真实后端的情况下完成前端功能开发。 simlets 是simulated servers的…...
BT169-ASEMI无人机专用功率器件BT169
编辑:ll BT169-ASEMI无人机专用功率器件BT169 型号:BT169 品牌:ASEMI 封装:SOT-23 批号:最新 引脚数量:3 特性:单向可控硅 工作温度:-40℃~150℃ BT169单向可控硅ÿ…...
4月26日星期六今日早报简报微语报早读
4月26日星期六,农历三月廿九,早报#微语早读。 1、广州多条BRT相关线路将停运,全市BRT客运量较高峰时大幅下降; 2、国务院批复:同意在海南全岛等15地设立跨境电商综合试验区; 3、我国首次实现地月距离尺度…...
如何将 sNp 文件导入并绘制到 AEDT (HFSS)
导入 sNp 文件 打开您的项目,右键单击 “Result” 绘制结果 导入后,用户可以选择它进行打印。请参阅下面的示例。要点:确保从 Solution 中选择它。...
Shell脚本-for循环应用案例
在Shell脚本编程中,for循环是一种强大的工具,用于处理重复性任务。无论是批量处理文件、遍历目录内容还是简单的计数任务,for循环都能提供简洁而有效的解决方案。本文将通过几个实际的应用案例来展示如何使用for循环解决具体的编程问题。 案…...
MATLAB基础应用精讲-【基础知识篇】发布和共享 MATLAB 代码
目录 MATLAB发布代码---生成文档pdf 分节符对发布文件的分节 实时脚本 Matlab workspace与m脚本数据共享 发布和共享 MATLAB 代码 在实时编辑器中创建和共享实时脚本 发布 MATLAB 代码文件 (.m) 添加帮助和创建文档 发布 MATLAB 代码文件 (.m) 可创建包括您的代码、注释…...
Shell脚本-while循环语法结构
在Shell脚本编程中,while循环是一种重要的流程控制语句,它允许我们重复执行一段代码,直到指定的条件不再满足为止。与for循环不同,while循环通常用于条件驱动的迭代,而不是基于列表或范围的迭代。本文将详细介绍Shell脚…...
Java基础第四章、面向对象
一、成员变量 示例: 二、JVM内存模型 类变量就是静态变量 三、构造方法 默认构造方法、定义的构造方法(不含参数、含参数) 构造方法重载: this关键字 this关键字应用:对构造方法进行复用,必须放在第一行 四、面向对象的三大特征 1…...
【基础IO上】复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux系统下,一切皆文件 | 重定向原理
1.关于文件的预备知识 1.1 文件的宏观理解 广义上理解,键盘、显示器等都是文件,因为我们说过“Linux下,一切皆文件”,当然我们现在对于这句话的理解是片面的;狭义上理解,文件在磁盘上,磁盘是一…...
linux离线部署open-metadata
OpenMetadata 环境及离线资源关闭防火墙禁止防火墙关闭 SELinux 创建用户安装JDK安装mysql安装Elasticsearch安装open-metadata 环境及离线资源 系统:CentOS Linux release 7.9.2009 (Core) JDK:17 Mysql: 8.0 OpenMetadata:1.6.…...
Exposure Adjusted Incidence Rate (EAIR) 暴露调整发病率:精准量化疾病风险
1. 核心概念 1.1 传统发病率的局限性 1.1.1 公式与定义 传统发病率公式为新发病例数除以总人口数乘以观察时间。例如在某社区观察1年,有10例新发病例,总人口1000人,发病率即为10/10001=0.01。 此公式假设所有个体暴露时间和风险相同,但实际中个体差异大,如部分人暴露时间…...
信令与流程分析
WebRTC是h5支持的重要特征之一,有了它,不再需要借助音视频相关的客户端,直接通过浏览器的Web页面就可以实现音视频聊天功能。 WebRTC项目是开源的,我们可以借助WebRTC,构建自己的音视频聊缇娜功能。无论是前端JS的Web…...
声音分离人声和配乐base,vocals,drums -从头设计数字生命第6课, demucs——仙盟创梦IDE
demucs -n htdemucs --two-stemsvocals 未来之窗.mp3 demucs -n htdemucs --shifts5 之.mp3demucs -n htdemucs --shifts5 -o wlzcoutspl 未来之窗.mp3 伴奏提取人声分离技术具有多方面的重大意义,主要体现在以下几个领域: 音乐创作与制作 创作便利…...
Chrmo手动同步数据
地址栏输入 chrome://sync-internals分别点击这2个按钮即可触发手动同步...
【Dify系列教程重置精品版】第1课 相关概念介绍
文章目录 一、Dify是什么二、Dify有什么用三、如何玩转Dify?从螺丝刀到机甲战士的进阶指南官方网站:https://dify.ai github地址:https://github.com/langgenius/dify 一、Dify是什么 Dify(Define + Implement + For You)。这是一款开源的大…...
【HTTP通信:生活中的邮局之旅】
HTTP通信:生活中的邮局之旅 HTTP通信就像是现代社会的邮政系统,让信息能够在互联网的城市间穿梭。下面我将用邮局比喻和图表来解释这个过程,以及它在现代应用中的重要性。 HTTP通信的旅程图解 #mermaid-svg-gC3zCsPpsFcq3sy3 {font-family:…...
Operating System 实验二 内存管理实验
目录 实验目标: 实验设备: 实验内容: (1)验证FIFO和Stack LRU页面置换算法 【代码(注释率不低于30%)】 【实验过程(截图)】 【结论】 (2)分别用FIFO和Stack LRU页置换算法,自己设定一个页面引用序列,绘制页错误次数和可用页帧总数的曲线并对比(可用Excel绘…...
深入解析YOLO v1:实时目标检测的开山之作
目录 YOLO v1 算法详解 1. 核心思想 2. 算法优势 3. 网络结构(Unified Detection) 4. 关键创新 5. 结构示意图(Fig1) Confidence Score 的计算 类别概率与 Bounding Box 的关系 后处理&…...
windows作业job介绍
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、作业job是什么?二、使用步骤1.代码示例 总结 前言 提示:这里可以添加本文要记录的大概内容: winapi网站: h…...
POLARIS土壤相关数据集
POLARIS相关数据集属于杜克大学(Duke University)土木与环境工程系(CEE)的水文学研究团队。该团队有三个总体主题:1) 改善地球系统模型中地表异质性的表示,2) 利用环境数据来描述在陆…...
【Harmony OS】组件
目录 组件概述 组件常用属性 系统内置组件 Text TextArea 多行文本输入框组件 TextInput 文本输入框 Button Image 图片组件,支持本地图片和网络图片 Radio 单选框 Checkbox 复选框 Blank 空白填充组件 Divider 分隔符 PatternLock 图案密码锁组件 Prog…...
找出字符串中第一个匹配项的下标
题目:28. 找出字符串中第一个匹配项的下标 给你两个字符串 haystack 和 needle,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1。 …...
专家系统的知识获取、检测与组织管理——基于《人工智能原理与方法》的深度解析
前文我们已经了解了专家系统的基本概念和一般结构,系统中有专业的知识才是专家系统的关键,接下来对专家系统中的知识是如何获取、检测、组织和管理的进行探讨。 1.专家系统的基本概念:专家系统的基本概念解析——基于《人工智能原理与方法》…...
BUUCTF-[GWCTF 2019]re3
[GWCTF 2019]re3 查壳,64位无壳 然后进去发现主函数也比较简单,主要是一个长度校验,然后有一个mprotect函数,说明应该又是Smc,然后我们用脚本还原sub_402219函数处的代码 import idc addr0x00402219 size224 for …...
基准指数选股策略思路
一种基于Python和聚宽平台的量化交易策略,主要包含以下内容: 1. 导入必要的库 - 导入jqdata和jqfactor库用于数据获取和因子计算。 - 导入numpy和pandas库用于数据处理。 2. 初始化函数 - 设置基准指数为沪深300指数。 - 配置交易参数,如使用…...
【阿里云大模型高级工程师ACP习题集】2.5 优化RAG应用提升问答准确度(⭐️⭐️⭐️ 重点章节!!!)
习题集 【单选题】在RAG应用的文档解析与切片阶段,若遇到文档类型不统一,部分格式的文档不支持解析的问题,以下哪种解决方式不可行?( ) A. 开发对应格式的解析器 B. 转换文档格式 C. 直接忽略该类型文档 D. 改进现有解析器以支持更多格式 【多选题】在选择向量数据库时,…...
【torch\huggingface默认下载路径修改】.cache/torch/ 或 .cache/huggingface
问题 服务器的硬盘空间是有限的,系统上的固态硬盘空间又比较小,在跑深度学习模型的时候经常有默认下载权重的操作,不管是torch或者huggingface,如果不加管理,所有的权重都放在home/user/.cache 里面,迟早会…...
SpringBoot 常用注解大全
SpringBoot 常用注解大全 一、核心注解 1. 启动类注解 SpringBootApplication:组合注解,包含以下三个注解 Configuration:标记该类为配置类EnableAutoConfiguration:启用自动配置ComponentScan:组件扫描 2. 配置相…...
【器件专题1——IGBT第2讲】IGBT 基本工作原理:从结构到特性,一文解析 “电力电子心脏” 的核心机制
IGBT(绝缘栅双极型晶体管,Insulated Gate Bipolar Transistor)作为现代电力电子领域的核心器件,其工作原理融合了 MOSFET 的高效控制优势与 BJT 的大功率处理能力。本文从物理结构、导通 / 关断机制、核心特性等维度,深…...
再谈String
1、字符串常量池 1.1 创建对象的思考 下面是两种创建字符串对象的代码 public static void main1(String[] args) {String s1 "hello";String s2 "hello";System.out.println(s1 s2);//trueString s3 new String("hello");String s4 new …...
语音合成之五语音合成中的“一对多”问题主流模型解决方案分析
语音合成中的“一对多”问题主流模型解决方案分析 引言“一对多”指的是什么?优秀开源模型的方法CosyvoiceSparkTTSLlaSA TTSVITS 引言 TTS系统旨在模仿人类的自然语音,但其核心面临着一个固有的挑战,即“一对多”问题 。这意味着对于给定的…...
嵌入式:Linux系统应用程序(APP)启动参数及其规则详解
在 systemd 的服务单元文件中,[Service] 部分用于定义服务的启动、停止、重启等操作,以及服务的运行环境和参数。以下是 [Service] 部分常见参数及其规则的详细介绍: 服务类型相关参数 **Type** **作用**:指定服务的启动类型&…...