写在前面:
这个系列来源于书籍冯乐乐老师的《unity shader 入门精要》,
参考的代码库“https://github.com/candycat1992/Unity_Shaders_Book”
我自己跟着写了一遍代码,并在代码块里给shader代码加上了比较详细的注释
更为仔细的解释和unity shader原理和知识书里都有,本blog不做详细解释,推荐买书来看。
本项目的所有代码在https://github.com/takashiwangbh/Unity-shader-effect-reproduction/tree/main
在这一节主要演示了3个较为复杂的纹理,第一个是使用立方体纹理实现环境映射,第二个是使用渲染纹理,也就是摄像机视角来做纹理,这里会有两个使用例子。
反射
在进行物体的shader之前,我们首先对背景做一个设置
这是原始背景
创建一个材质,将shader改为skybox中的6 sided,然后附上贴图
然后点击windows中的lghiting
在environment中将skybox material改为我们的材质
就可以看到背景的天空盒子边成了贴图的场景
反射
使用反射的物体通常看起来像表面渡了层金属,反射的实现原理是通过入射光线的方向和表面法线的方向计算反射方向,再利用反射方向对立方体纹理进行采样
shader code
Shader "Unity Shaders Book/Chapter 10/Reflection" {
// 定义属性,供用户在 Unity Inspector 面板中调整参数
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 基础颜色
_ReflectColor ("Reflection Color", Color) = (1, 1, 1, 1) // 反射颜色
_ReflectAmount ("Reflect Amount", Range(0, 1)) = 1 // 反射强度
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {} // 用于反射的立方体贴图(Cubemap)
}
SubShader {
// 定义渲染队列和类型
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" } // 基本光照模式,Forward 渲染管线中基础光照通道
CGPROGRAM
#pragma multi_compile_fwdbase // 支持前向渲染的基础光照
#pragma vertex vert // 顶点着色器
#pragma fragment frag // 片元着色器
#include "Lighting.cginc" // 包含光照计算的库文件
#include "AutoLight.cginc" // 包含自动光照相关的定义
// 声明属性变量
fixed4 _Color; // 基础颜色
fixed4 _ReflectColor; // 反射颜色
fixed _ReflectAmount; // 反射强度
samplerCUBE _Cubemap; // 反射用的立方体贴图(Cubemap)
// 定义顶点着色器输入结构体
struct a2v {
float4 vertex : POSITION; // 输入的顶点位置
float3 normal : NORMAL; // 输入的顶点法线
};
// 定义顶点着色器输出结构体
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间坐标
float3 worldPos : TEXCOORD0; // 世界坐标
fixed3 worldNormal : TEXCOORD1; // 世界法线
fixed3 worldViewDir : TEXCOORD2; // 世界空间的视角方向
fixed3 worldRefl : TEXCOORD3; // 世界空间的反射方向
SHADOW_COORDS(4) // 阴影坐标(宏定义,自动处理阴影)
};
// 顶点着色器
v2f vert(a2v v) {
v2f o;
// 将物体坐标转换为裁剪空间坐标
o.pos = UnityObjectToClipPos(v.vertex);
// 将法线从物体空间转换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算顶点的世界坐标
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 计算从顶点到相机的方向向量(世界空间)
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 计算反射向量:基于观察方向和法线,得到世界空间反射方向
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
// 传输阴影坐标(用于阴影计算)
TRANSFER_SHADOW(o);
return o;
}
// 片元着色器
fixed4 frag(v2f i) : SV_Target {
// 归一化世界法线
fixed3 worldNormal = normalize(i.worldNormal);
// 归一化世界光照方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 归一化世界观察方向
fixed3 worldViewDir = normalize(i.worldViewDir);
// 获取环境光颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算漫反射颜色:光照颜色 * 基础颜色 * 法线与光照方向的点积
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 使用反射向量采样立方体贴图,实现环境反射效果
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
// 计算光照衰减(阴影计算)
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 使用反射颜色与漫反射颜色进行混合:根据反射强度 _ReflectAmount 混合漫反射与反射效果
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
// 返回最终颜色,alpha 为 1(完全不透明)
return fixed4(color, 1.0);
}
ENDCG
}
}
// 备用 Shader,当目标平台不支持当前 Shader 时使用的回退选项
FallBack "Reflective/VertexLit"
}
折射
当光线从一种介质(空气)斜射到另一种介质(如玻璃),传播方向一般就会发生改变,当给定入射角的时候我们可以使用斯涅尔定律来计算反射角。
shader code
Shader "Unity Shaders Book/Chapter 10/Refraction" {
// 定义属性,用于在材质面板中调整参数
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 基础颜色
_RefractColor ("Refraction Color", Color) = (1, 1, 1, 1) // 折射颜色
_RefractAmount ("Refraction Amount", Range(0, 1)) = 1 // 折射混合程度
_RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5 // 折射率
_Cubemap ("Refraction Cubemap", Cube) = "_Skybox" {} // 折射所使用的 Cubemap 贴图
}
// 定义 SubShader,指定渲染标签
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass {
Tags { "LightMode"="ForwardBase" } // 前向渲染模式
CGPROGRAM
// 编译选项
#pragma multi_compile_fwdbase
#pragma vertex vert // 指定顶点着色器
#pragma fragment frag // 指定片元着色器
// 引入常用的光照函数
#include "Lighting.cginc"
#include "AutoLight.cginc"
// 声明属性变量
fixed4 _Color; // 基础颜色
fixed4 _RefractColor; // 折射颜色
float _RefractAmount; // 折射的混合程度(0 到 1)
fixed _RefractRatio; // 折射率(控制折射角度)
samplerCUBE _Cubemap; // 折射使用的立方体贴图(Cubemap)
// 输入结构体 a2v(从顶点数据传入)
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 法线方向
};
// 输出结构体 v2f(从顶点着色器传递到片元着色器)
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间中的顶点位置
float3 worldPos : TEXCOORD0; // 世界坐标中的顶点位置
fixed3 worldNormal : TEXCOORD1; // 世界坐标中的法线方向
fixed3 worldViewDir : TEXCOORD2;// 从观察者到顶点的方向(世界空间)
fixed3 worldRefr : TEXCOORD3; // 折射方向(世界空间)
SHADOW_COORDS(4) // 用于阴影计算的坐标
};
// 顶点着色器:计算所需的中间数据
v2f vert(a2v v) {
v2f o;
// 将顶点转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 计算世界空间的法线方向
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算世界空间的顶点位置
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 计算从观察者到顶点的方向(世界空间)
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 使用 refract 函数计算折射方向
// refract(入射向量, 法线向量, 折射率)
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
// 传递阴影坐标
TRANSFER_SHADOW(o);
return o;
}
// 片元着色器:计算最终的像素颜色
fixed4 frag(v2f i) : SV_Target {
// 归一化法线、光照方向和观察方向
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);
// 环境光颜色
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 漫反射光照计算:光源颜色 * 物体颜色 * 法线与光照方向的点积
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 使用折射方向从 Cubemap 中采样颜色
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
// 计算光照衰减
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 将漫反射和折射颜色按比例混合
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
// 返回最终颜色(带 alpha 通道)
return fixed4(color, 1.0);
}
ENDCG
}
}
// 当不支持该 Shader 时回退到其他 Shader
FallBack "Reflective/VertexLit"
}
菲涅尔反射
当光线照射到物体的表面,会有一部分发生反射,一部分进入物体内部发生折射,使用菲涅尔近似等式,可以在边界模拟反射光强和折射光强之间的变化。
shader code
Shader "Unity Shaders Book/Chapter 10/Fresnel" {
// 定义可调节的属性
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1) // 物体基础颜色
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5 // Fresnel 效应的强度系数
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {} // 反射所用的环境贴图(立方体贴图)
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"} // 设置渲染类型和渲染队列
Pass {
Tags { "LightMode"="ForwardBase" } // 定义为 ForwardBase 光照模式
CGPROGRAM
#pragma multi_compile_fwdbase // 支持前向渲染的多光源
#pragma vertex vert // 顶点着色器入口
#pragma fragment frag // 片元着色器入口
#include "Lighting.cginc" // 包含基础光照计算函数
#include "AutoLight.cginc" // 包含自动阴影坐标计算函数
// 定义外部属性
fixed4 _Color; // 基础颜色
fixed _FresnelScale; // Fresnel 效应强度
samplerCUBE _Cubemap; // 立方体贴图(反射环境)
// 定义顶点输入结构
struct a2v {
float4 vertex : POSITION; // 模型空间的顶点位置
float3 normal : NORMAL; // 模型空间的法线
};
// 顶点输出结构
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float3 worldPos : TEXCOORD0; // 世界空间顶点位置
fixed3 worldNormal : TEXCOORD1; // 世界空间法线
fixed3 worldViewDir : TEXCOORD2; // 世界空间视角方向
fixed3 worldRefl : TEXCOORD3; // 世界空间反射向量
SHADOW_COORDS(4) // 存储阴影坐标
};
// 顶点着色器
v2f vert(a2v v) {
v2f o;
// 将顶点从对象空间转换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
// 将法线从对象空间转换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 计算世界空间顶点位置
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 计算世界空间的视角方向(从顶点指向相机)
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 计算反射向量(视角方向反射到法线上)
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
// 传递阴影坐标(用于阴影计算)
TRANSFER_SHADOW(o);
return o;
}
// 片元着色器
fixed4 frag(v2f i) : SV_Target {
// 归一化世界空间的法线
fixed3 worldNormal = normalize(i.worldNormal);
// 归一化世界空间的光照方向
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 归一化世界空间的视角方向
fixed3 worldViewDir = normalize(i.worldViewDir);
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 计算阴影衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 从立方体贴图中采样反射颜色
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;
// 计算 Fresnel 系数
// Fresnel = 基础强度 + (1 - 基础强度) * (1 - cos(视角方向与法线夹角))^5
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);
// 计算漫反射光照
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 混合漫反射颜色与反射颜色,根据 Fresnel 系数调整
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
// 返回最终颜色,alpha 设为 1.0
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit" // 回退到低级别的反射 VertexLit Shader
}
效果展示
镜子效果
这里创建一个平面作为镜子的载体。然后再创造一些方块胶囊球体等作为道具。
这里要引入一个纹理叫渲染纹理(Render Texture),这是一种允许将摄像机的渲染结果直接输出到一个 纹理(Texture) 上的纹理,而不是直接渲染到屏幕上。简而言之就是将相机的视角渲染到纹理上,不用我们设置纹理。所以我们需要在镜子的子物体下加一个相机,再将相机的纹理指定为创建的这个渲染纹理。
然后为镜子添加有shader的材质,然后在贴图那里选择我们创建的渲染纹理。
这个时候我们调节之后加的那个相机,他就会把他是视角的物体渲染到纹理里从而渲染到镜子载体上。
最后就能够得到镜子的效果了。
shader code
Shader "Unity Shaders Book/Chapter 10/Mirror" {
// 定义可调属性,允许用户在材质面板中设置纹理
Properties {
_MainTex ("Main Tex", 2D) = "white" {} // 主纹理贴图,默认值为白色
}
SubShader {
Tags {
"RenderType"="Opaque" // 定义渲染类型为不透明物体
"Queue"="Geometry" // 渲染队列为默认的不透明物体队列
}
Pass {
// 开始编写 GPU 程序
CGPROGRAM
// 定义顶点着色器和片元着色器的入口函数
#pragma vertex vert
#pragma fragment frag
// 声明一个 2D 纹理采样器,名称与 Properties 中的 _MainTex 对应
sampler2D _MainTex;
// 定义输入结构 a2v(从模型数据传入顶点着色器)
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 texcoord : TEXCOORD0; // 顶点的纹理坐标
};
// 定义输出结构 v2f(从顶点着色器传递到片元着色器)
struct v2f {
float4 pos : SV_POSITION; // 顶点位置,投影到裁剪空间
float2 uv : TEXCOORD0; // 传递给片元着色器的纹理坐标
};
// 顶点着色器:处理顶点数据
v2f vert(a2v v) {
v2f o;
// 将模型空间的顶点位置转换为裁剪空间,用于显示在屏幕上
o.pos = UnityObjectToClipPos(v.vertex);
// 获取纹理坐标并进行镜像操作(翻转 x 轴)
o.uv = v.texcoord;
o.uv.x = 1 - o.uv.x; // 这里实现了镜像效果,将 x 纹理坐标反转
return o; // 返回给片元着色器
}
// 片元着色器:处理每个像素的颜色
fixed4 frag(v2f i) : SV_Target {
// 采样主纹理中的颜色,使用翻转后的纹理坐标
return tex2D(_MainTex, i.uv);
}
ENDCG // 结束 GPU 程序
}
}
// 关闭 Fallback,这个 Shader 不会回退到其他 Shader
FallBack Off
}
效果展示
玻璃效果
shader中会使用GradPass来实现像玻璃等透明效果,GradPass能够对物体后面的图形进行更复杂的处理。
首先创建一个shader,再创建一个材质应用这个shader,再创建一个cube应用这个材质。
我们需要一个脚本把当前位置观察到的图像渲染到制动的立方体纹理,这个脚本在文件里已经有了,详细可以下载来看,这里也附上脚本
using UnityEngine;
using UnityEditor;
using System.Collections;
public class RenderCubemapWizard : ScriptableWizard {
public Transform renderFromPosition;
public Cubemap cubemap;
void OnWizardUpdate () {
helpString = "Select transform to render from and cubemap to render into";
isValid = (renderFromPosition != null) && (cubemap != null);
}
void OnWizardCreate () {
// create temporary camera for rendering
GameObject go = new GameObject( "CubemapCamera");
go.AddComponent<Camera>();
// place it on the object
go.transform.position = renderFromPosition.position;
// render into cubemap
go.GetComponent<Camera>().RenderToCubemap(cubemap);
// destroy temporary camera
DestroyImmediate( go );
}
[MenuItem("GameObject/Render into Cubemap")]
static void RenderCubemap () {
ScriptableWizard.DisplayWizard<RenderCubemapWizard>(
"Render cubemap", "Render!");
}
}
然后创建一个空的GameObject对象,为了使用这个GameObject的位置信息来渲染立方体纹理。
然后创建一个用于存储的立方体纹理。如下图
在这个纹理中勾选Readable选项。
对于GameObject选择Render into Cubemap
Render From Position选择这个GameObject,Cubemap选择创建的Cubemap,再点击Render,这样我们就把这个gameobject的视角渲染到材质里了。
最后把这个材质添加到shader的属性里就能看到有玻璃的样子了,如果想改变玻璃的外观可以再加材质
shader code
Shader "Unity Shaders Book/Chapter 10/Glass Refraction" {
Properties {
// 定义用于着色器的属性
_MainTex ("主纹理", 2D) = "white" {} // 主纹理,用于显示表面颜色
_BumpMap ("法线贴图", 2D) = "bump" {} // 法线贴图,用于模拟表面细节
_Cubemap ("环境立方体贴图", Cube) = "_Skybox" {} // 立方体贴图,用于环境反射
_Distortion ("扭曲强度", Range(0, 100)) = 10 // 控制扭曲效果的强度
_RefractAmount ("折射比例", Range(0.0, 1.0)) = 1.0 // 控制折射与反射的比例
}
SubShader {
// 透明队列,确保其他对象先于当前对象渲染
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
// 此 Pass 抓取屏幕后方的内容并保存为纹理
// 抓取的结果将在下一个 Pass 中以 _RefractionTex 访问
GrabPass { "_RefractionTex" }
Pass {
CGPROGRAM
#pragma vertex vert // 定义顶点着色器
#pragma fragment frag // 定义片段着色器
#include "UnityCG.cginc" // 引入 Unity 的常用函数库
// 声明纹理和属性
sampler2D _MainTex; // 主纹理
float4 _MainTex_ST; // 主纹理的缩放和平移
sampler2D _BumpMap; // 法线贴图
float4 _BumpMap_ST; // 法线贴图的缩放和平移
samplerCUBE _Cubemap; // 环境立方体贴图
float _Distortion; // 扭曲强度
fixed _RefractAmount; // 折射比例
sampler2D _RefractionTex; // 折射纹理
float4 _RefractionTex_TexelSize; // 折射纹理的像素大小
// 输入的顶点数据
struct a2v {
float4 vertex : POSITION; // 顶点位置
float3 normal : NORMAL; // 顶点法线
float4 tangent : TANGENT; // 顶点切线
float2 texcoord : TEXCOORD0; // 顶点纹理坐标
};
// 顶点着色器的输出
struct v2f {
float4 pos : SV_POSITION; // 裁剪空间位置
float4 scrPos : TEXCOORD0; // 屏幕空间位置
float4 uv : TEXCOORD1; // 主纹理和法线贴图的坐标
float4 TtoW0 : TEXCOORD2; // 切线空间到世界空间的第一行
float4 TtoW1 : TEXCOORD3; // 切线空间到世界空间的第二行
float4 TtoW2 : TEXCOORD4; // 切线空间到世界空间的第三行
};
// 顶点着色器
v2f vert (a2v v) {
v2f o;
// 计算顶点位置的裁剪空间坐标
o.pos = UnityObjectToClipPos(v.vertex);
// 计算屏幕空间坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
// 计算主纹理和法线贴图的坐标
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
// 将顶点位置从对象空间转换到世界空间
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 计算世界空间的法线、切线和副切线
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
// 构建切线空间到世界空间的变换矩阵
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
// 片段着色器
fixed4 frag (v2f i) : SV_Target {
// 从切线空间到世界空间获取法线
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 获取法线贴图的法线值(切线空间)
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
// 在切线空间计算偏移
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
// 将法线从切线空间转换到世界空间
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
// 计算反射方向
fixed3 reflDir = reflect(-worldViewDir, bump);
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
// 根据折射比例混合反射和折射颜色
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1); // 返回最终颜色
}
ENDCG
}
}
// 回退到简单的 Diffuse 着色器
FallBack "Diffuse"
}