👌当下主要学习图形方向,具体细节可以查看Unity官方文档。
渲染管线
摄像机
光照
模型
网格
纹理
着色器
材质
Visual effects
天空
颜色
图形API
Graphics performance and profiling
虽然自己程序方面更擅长C++而且现在用UE也是个趋势,但是Unity普及度更高而且教程更多,UE蓝图也摒弃了传统的纯程序开发路线,所以打好这方面基础是现在最需要做的。如果有任何建议和想法需要交流欢迎联系我!不断试错不断学习中🤗
笔记内容90%来自视频原话,其他内容个人补充,欢迎勘误。
学习内容大多来自b站佬们的教程(先三连之后看的!)🤟参考链接点击标题跳转
项目实战-Uinty
Shader代码基础
PART1
Shader代码基本架构
Shader"1️⃣"
{
Properties
{
2️⃣
}
SubShader
{
️3️⃣
Pass
{
4️⃣
}
}
}
1️⃣Shader名称
2️⃣定义变量位置
常见的五种变量类型:
- Float
- Range
- Vector
- Color
- Texture
Properties
{
//常用的五种数据类型
_Float("Float",Float)=0.0
_Range("Range",Range(0.0,1.0))=0.0
_Vector("Vector",Vector)=(1,1,1,1)
_Color("Color",Color)=(0.5,0.5,0.5,0.5)
_Texture("Texture",2D)="black"{}
}
3️⃣主要Shader代码部分
可以理解成一个Pass等于一个完整的GPU渲染管线,SubShader里可以写多个Pass,写几个代表会被重复渲染几次,而且每次调用的Shader都不一样。
CGPROGRAM
//中间的任何代码都属于unity cg的范围
ENDCG
- 这里对于
float类型补充一点:
-
float:用于表示单个浮点数,常用于颜色通道、位置坐标等。 -
float2(Vector2):用于表示二维向量,如二维坐标、UV坐标等。 -
float3(Vector3):用于表示三维向量,如位置、法线等。 -
float4(Vector4):用于表示四维向量,如颜色、四元数等。
在unity中,一个模型最多有四套UV
float/half/fixed的区别:
类型 位宽 适用范围 常见用途 精度 现代GPU支持情况 float 32-bit 大 位置、物理计算、PBR 高 强烈推荐 half 16-bit 中 颜色计算、法线、屏幕后处理 中等 推荐(移动端优化) fixed 10-bit 小 颜色计算(老设备) 低 几乎淘汰
GPU渲染管线
基本结构:
【模型数据】 -> 1️⃣【顶点Shader】 -> 2️⃣【图元装配及光栅化】 -> 3️⃣【片元Shader】 -> 4️⃣【输出合并】
|-----------------------------GPU渲染管线---------------------------------------|
1️⃣顶点Shader:
- 将模型数据的模型空间坐标转换到对应的裁剪空间,即输出在裁剪空间下的顶点坐标
- 【模型空间】 -> 世界空间 -> 相机空间 -> 【裁剪空间】,中间经过三个矩阵(Model,View,Projection)操作
2️⃣图元装配及光栅化
- 硬件阶段
- 生成片元并进行光栅化插值
3️⃣片元Shader
- 每个片元调用片元shader给自身着色
- 计算对应的颜色后输出
4️⃣输出合并
- 输出到对应的帧缓冲区
一个完整的Shader通常由顶点Shader和片段Shader共同组成。
完整Shader代码
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
Shader "CS0102/shader"
{
Properties
{
// 常用的五种数据类型
_Float("Float", Float) = 0.0
_Range("Range", Range(0.0, 1.0)) = 0.0
_Vector("Vector", Vector) = (1,1,1,1)
_Color("Color", Color) = (0.5,0.5,0.5,0.5)
//_Texture("Texture", 2D) = "black" {}
_MainTex("MainTex",2D)="black"{}
}
SubShader
{
Pass
{
// Shader 主要代码部分
CGPROGRAM
#pragma vertex vert // 指定一个顶点 Shader:vert
#pragma fragment frag // 指定一个片元 Shader:frag
#include "UnityCG.cginc" // 头文件
// 从 CPU 端获取模型数据
struct appdata {
float4 vertex : POSITION; // 模型顶点坐标
// 第一套uv,共4个可用(TEXCOORD0~TEXCOORD3)
float2 uv : TEXCOORD0;
float3 normal : NORMAL; // 法线
float4 color : COLOR; // 顶点色
};
// 输出结构体定义
struct v2f {
float4 pos : SV_POSITION;
//通用储存器(插值器),共16个可用(TEXCOORD0~TEXCOORD15)
float2 uv:TEXCOORD0;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//动态链接四个参数
// 顶点 Shader
v2f vert(appdata v) {
v2f o;
//float4 pos_world = mul(unity_ObjectToWorld, v.vertex); // 模型空间转世界空间
//float4 pos_clip = mul(UNITY_MATRIX_VP, pos_world); // 世界空间直接变换到裁剪空间
//o.pos = pos_clip;
//或者直接合成一个mvp操作
o.pos=UnityObjectToClipPos(v.vertex);
//输出uv值
//o.uv=v.uv;
o.uv=v.uv*_MainTex_ST.xy+_MainTex_ST.zw;
return o;
}
// 片元 Shader
float4 frag(v2f i) : SV_Target {
//贴图采样
float4 col=tex2D(_MainTex,i.uv);
return col;
}
ENDCG
}
}
}
PART2
背面剔除(Backface Culling)
背面剔除在硬件阶段、NDC空间之后进行,Unity默认使用背面剔除(只渲染正面)。
- Shader代码控制:直接显示出渲染结果
SubShader
{
Pass
{
Cull Off //1️⃣不剔除
Cull Back //2️⃣默认剔除背面
Cull Front//3️⃣剔除正面
//注意在CGPROGRAM之前
CGPROGRAM
...
ENDCG
}
}
- 材质球控制:物体材质球处出现可选项:Off/Front/Back
Properties
{
[Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode",float)=2
}
SubShader
{
Pass
{
Cull [_CullMode]
}
}
PART3 纹理映射
正面纹理采样设定
这里用XY坐标值采样贴图,即uv按模型本身的XY展开。
代码如下:
struct v2f {
...
float2 pos_uv:TEXCOORD1;
};
v2f vert(appdata v) {
...
o.pos_uv=v.vertex.xy*_MainTex_ST.xy+_MainTex_ST.zw;
return o;
}
float4 frag(v2f i) : SV_Target {
float4 col=tex2D(_MainTex,i.pos_uv);
return col;
}
👉️补充:
- 正面 XY平面
- 侧面 ZY平面
- 俯视 XZ平面
总结:纹理映射的来源不一定是单纯的o.uv的uv坐标,也可以是上面写的o.pos_uv(世界坐标pos_world也可)这种自己设定的值。
另,如果想三平面都能够完美覆盖涉及到:三平面映射。up在此链接中有提到,所以查了其他教程补充在这里。
三平面映射
- **目的:**防止”纹理拉伸”
防止纹理拉伸传统思路可以手动展开uv坐标以贴合模型起伏,但是难以适应实时渲染的要求,因此引入这一方法:三平面映射。
**应用:**普通网格地形、瀑布、程序化建模的地形、水池的焦散效果
实现步骤
- 根据顶点的世界空间坐标计算对应的UV坐标
//取每个片元在三个方向的uv坐标
half2 yUV=IN.worldPos.xz/_TextureScale;
half2 xUV=IN.worldPos.yz/_TextureScale;
half2 zUV=IN.worldPos.xy/_TextureScale;
- 从x/y/z三个方向对漫反射贴图进行采样
half3 yDiff=tex2D(_DiffuseMap,yUV);
half3 xDiff=tex2D(_DiffuseMap,xUV);
half3 zDiff=tex2D(_DiffuseMap,zUV);
- 调整三平面边界过渡的锐利度
//用法线方向作为从三个方向采样的权重值
//额外解决纹理接缝问题(法线是连续过渡的)
half3 blendWeights=pow(abs(In.worldNormal),_TriplanarBlendSharpness);
- 将混合权重值单位化
//第三步pow锐化三平面边界后,得到的法线值可能偏小
//重新单位化
blendWeights=blendWeights/(blendWeights.x+blendWeights.y+blendWeights.z);
- 按三个面的混合权重,将采样颜色混合在一起
//根据混合系数混合
p.Albedo=xDiff*blendWeights.x+yDiff*blendWeights.y+zDiff*blendWeights.z;
更多相关内容戳此
渲染扭曲问题
打印uv坐标:
float4 frag(v2f i) : SV_Target {
float4 col=tex2D(_MainTex,i.uv);
return float4(i.uv,0.0,0.0);
}
若uv存在不连续的情况,则会导致贴图采样时引起纹理图像的失真问题。
👉️为什么会有uv不连续的现象?
光栅化过程产生。光栅化时根据三角面的进行,每个三角按照每个三角面进行一个插值。而对于模型而言,其三角网排列就会造成一定程度上的问题。
解决方式:
- uv展开并缩放到0~1之间
- 模型加面
- 手动在片元shader中重新计算uv
PART4 透明度测试 Alpha-test
输出合并
对于输出合并阶段,可以细分为:
【片元数据】Color,Depth
↓
【Alpha测试】
↓
【模板测试】Stencil Test
↓
【深度测试】Depth test
↓
【混合】Blending
↓
【帧缓冲区】Color,Depth,Stencil
Alpha-test
**Alpha 测试是拒绝将像素写入屏幕的最后机会。**在计算出最终输出颜色之后,颜色可选择性地将其 Alpha 值与固定值进行比较。如果测试失败,则不会将像素写入显示屏。
当下在Shader中,很少会直接用alpha test这个指令。一般用的是更好用、更可控的指令:clip“裁剪”。
如果clip()括号中的值<0,整个模型将不显示。
- 模型的一部分被裁剪掉
float4 frag(v2f i) : SV_Target {
float4 gradient=tex2D(_MainTex,i.uv);
clip(gradient-0.1);
return gradient.xxx;
}
- 设置一个值实现动态控制
好神奇😂我这里模型用的球,Cutout调到0.5变成吃豆人了
Properties
{
_MainTex("MainTex",2D)="black"{}
_Cutout("Cutout",Range(-0.1,1.1))=0.0
[Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode",float)=2
}
SubShader
{
Pass
{
half4 frag(v2f i) : SV_Target {
half gradient=tex2D(_MainTex,i.uv).r;
clip(gradient-_Cutout);
return gradient.xxxx;
//gradient.xxxx等价于float4(gradient,gradient,gradient,gradient);
}
}
}
- 对UV进行动画处理
half gradient=tex2D(_MainTex,i.uv+_Time.y).r;
- 对UV进行动画速度的处理
Properties
{
_Speed("Speed",Vector)=(1,1,0,0)
}
SubShader
{
Pass
{
float4 _Speed;
half4 frag(v2f i) : SV_Target {
half gradient=tex2D(_MainTex,i.uv+_Time.y*_Speed.xy).r;
}
}
}
- noise贴图控制
适当的模型+贴图可以实现波形扩散效果
Properties
{
_NoiseTex("Noise Tex",2D)="white"{}
//_MainColor("MainColor",Color)=(1,1,1,1)
}
SubShader
{
Pass
{
float4 _NoiseTex_ST;
//float4 _MainColor;
half4 frag(v2f i) : SV_Target {
half gradient=tex2D(_MainTex,i.uv+_Time.y*_Speed.xy).r;
half noise=tex2D(_NoiseTex,i.uv+_Time.y*_Speed.zw).r;
clip(gradient-noise-_Cutout);
return noise.xxxx;
//return _MainColor;
}
}
}
综上 alpha-test可以用来做溶解相关的效果。
PART5 半透明混合 Blending
- 实现半透明混合
Properties
{
_MainColor("MainColor",Color)=(1,1,1,1)
}saturate
SubShader
{
Blend SrcAlpha OneMinusSrcAlpha//注意此句在CGPROGRAM之前
float4 _MainColor;
Pass
{
half4 frag(v2f i) : SV_Target {
half4 col=tex2D(_MainTex,i.uv)*_MainColor;
return col;
}
}
}
- 关闭ZWrite深度写入
半透明混合的使用要注意排序问题:ZWrite Off
SubShader
{
Pass{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
}
}
- 渲染队列问题
先前我们的材质球的Render Queue为2000,处于不透明的渲染队列中。使用半透明效果必须记得使用Tags{"Queue"="Transparent"}+关掉ZWrite(部分情况下ZWrite需要开启)
SubShader
{
Tags{"Queue"="Transparent"}
Pass{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
}
}
- 贴图半透效果
- 当前贴图无alpha通道,所以只能用其灰度值来做一个半透效果。
half4 frag(v2f i) : SV_Target {
half3 col=_MainColor.xyz;
half alpha=tex2D(_MainTex,i.uv).r*_MainColor.a;
return float4(col,alpha);
}
做出来的球像透明泡泡🤗好好看~
- 增加显示强度
此处要确保alpha值在0~1的范围内,否则开HDR的时候就很容易出问题
Properties
{
_Emiss("Emiss",Float)=1.0
}
SubShader
{
float _Emiss;
Pass
{
half4 frag(v2f i) : SV_Target {
half3 col=_MainColor.xyz*_Emiss;
half alpha=saturate(tex2D(_MainTex,i.uv).r*_MainColor.a*_Emiss);
return float4(col,alpha);
}
}
}
- 其他混合模式
常用的柔和叠加模式:Blend SrcAlpha One
PART6 边缘光
- 得到normal_world
SubShader
{
Pass
{
struct appdata {
float3 normal : NORMAL;
};
struct v2f {
float3 normal_world:TEXCOORD1;
};
v2f vert(appdata v) {
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.normal_world=normalize(mul(float4(v.normal,0.0),unity_WorldToObject).xyz);
//
o.uv=v.uv*_MainTex_ST.xy+_MainTex_ST.zw;
return o;
}
}
}
- 得到view_world
struct v2f {
float3 view_world:TEXCOORD2;
};
v2f vert(appdata v) {
float3 pos_world=mul(unity_ObjectToWorld,v.vertex).xyz;
o.view_world=normalize(_WorldSpaceCameraPos.xyz-pos_world);
}
- 在片元Shader中把数据承接过来
dot(a,b):a向量与b向量进行点乘。
两个向量越重合就越接近1;180°为-1。[-1,1]👉️dot结果在[-1,1]
half4 frag(v2f i) : SV_Target {
float3 normal_world=normalize(i.normal_world);
//光栅化的过程会导致向量的长度变化
float3 view_world=normalize(i.view_world);
float NdoV=saturate(dot(normal_world,view_world));
float rim=1.0-NdoV;
return NdoV.xxxx;
}
- 得到边缘光效果
- (来自弹幕佬的解释)边缘处的法线和视角方向接近垂直,dot=0,1-dot进行取反,边缘就显示出颜色,其他部分就较为透明。
half4 frag(v2f i) : SV_Target {
...
float rim=1.0-NdoV;
return rim.xxxx;
}
- 边缘光改色
- 也可以用alpha通道调:
float alpha=saturate((1.0-NdoV)*_Emiss);
half4 frag(v2f i) : SV_Target {
float3 col=_MainColor.xyz*_Emiss;//乘倍增系数,Inspector可调
...
return float4(col,alpha);
}
- 边缘光对比度调节
Properties
{
_RimPower("_RimPower",Float)=1.0
}
SubShader
{
Pass
{
float _RimPower;
half4 frag(v2f i) : SV_Target {
float NdoV=pow(saturate(dot(normal_world,view_world)),RimPower);
float fresnel=pow((1.0-NdoV),_RimPower);
float alpha=saturate(fresnel*_Emiss);
}
}
}
- 模型透过问题:预先写深度
经过上面的步骤,模型能够看到内部的透过结构。如何避免这种现象?
☝「打开ZWrite」ZWrite On。此时效果其实不算特别完美
✌「再加一个`Pass」预写一遍深度,代码如下:
SubShader
{
Tags{"Queue"="Transparent"}
Pass
{
Cull Of
ZWrite On
ColorMask 0
CGPROGRAM
float4 _Color;
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 veryexPos:POSITION):SV_POSITION
{
return UnityObjectToClipPos(vertexPos);
}
float4 frag(void):COLOR
{
return _Color;
}
ENDCG
}
Pass
{
...
}
}
- 把最靠前的三角形的片元的深度预先
ZWrite On写好 Color Mask的操作:只写深度,不写任何的颜色信息- 到了第二遍pass绘制的时候,这些片元就通过深度测试,默认把背后的像素剔除
👉️ASE中有对应的功能,此处仅为简单演示。
🤯🤯🤯断断续续一个多星期终于把这一小时的视频啃完辣!!!休息休息继续战斗!
其他参考链接
反射探针:
https://zhuanlan.zhihu.com/p/438022045
三大Shader编程语言:
https://blog.csdn.net/weixin_56516170/article/details/135266277
《Shader入门精要》源代码:
https://github.com/candycat1992/Unity_Shaders_Book
法线贴图:
https://blog.csdn.net/weixin_49427945/article/details/136458398
https://docs.unity3d.com/cn/2021.1/Manual/StandardShaderMaterialParameterNormalMap.html
