👌当下主要学习图形方向,具体细节可以查看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
  1. 这里对于float类型补充一点:
  • float‌:用于表示单个浮点数,常用于颜色通道、位置坐标等。
  • float2‌(Vector2):用于表示二维向量,如二维坐标、UV坐标等。
  • float3‌(Vector3):用于表示三维向量,如位置、法线等。
  • float4‌(Vector4):用于表示四维向量,如颜色、四元数等。
  1. 在unity中,一个模型最多有四套UV

  2. 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默认使用背面剔除(只渲染正面)。

  1. Shader代码控制:直接显示出渲染结果
    SubShader
    {
        Pass
        {
            Cull Off  //1️⃣不剔除
            Cull Back //2️⃣默认剔除背面
            Cull Front//3️⃣剔除正面
            //注意在CGPROGRAM之前
            CGPROGRAM
            ...
            ENDCG
        }
    }
  1. 材质球控制:物体材质球处出现可选项: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在此链接中有提到,所以查了其他教程补充在这里。

三平面映射

  1. **目的:**防止”纹理拉伸”

防止纹理拉伸传统思路可以手动展开uv坐标以贴合模型起伏,但是难以适应实时渲染的要求,因此引入这一方法:三平面映射。

  1. **应用:**普通网格地形、瀑布、程序化建模的地形、水池的焦散效果

  2. 实现步骤

  • 根据顶点的世界空间坐标计算对应的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

参考链接1️⃣2️⃣

输出合并

对于输出合并阶段,可以细分为:

【片元数据】Color,Depth
    ↓
【Alpha测试】
    ↓
【模板测试】Stencil Test
    ↓
【深度测试】Depth test
    ↓
【混合】Blending
    ↓
【帧缓冲区】Color,Depth,Stencil

Alpha-test

**Alpha 测试是拒绝将像素写入屏幕的最后机会。**在计算出最终输出颜色之后,颜色可选择性地将其 Alpha 值与固定值进行比较。如果测试失败,则不会将像素写入显示屏。

当下在Shader中,很少会直接用alpha test这个指令。一般用的是更好用、更可控的指令:clip“裁剪”。

如果clip()括号中的值<0,整个模型将不显示。

  1. 模型的一部分被裁剪掉
            float4 frag(v2f i) : SV_Target {
                float4 gradient=tex2D(_MainTex,i.uv);
                clip(gradient-0.1);
                return gradient.xxx;
            }
  1. 设置一个值实现动态控制
    好神奇😂我这里模型用的球,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);
            }
        }
    }
  1. 对UV进行动画处理
                half gradient=tex2D(_MainTex,i.uv+_Time.y).r;
  1. 对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;
            }
        }
    }
  1. 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

参考链接1️⃣2️⃣

  1. 实现半透明混合
    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;
            }
        }
    }
  1. 关闭ZWrite深度写入
    半透明混合的使用要注意排序问题:ZWrite Off
    SubShader
    {
        Pass{
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
        }
    }
  1. 渲染队列问题
    先前我们的材质球的Render Queue为2000,处于不透明的渲染队列中。使用半透明效果必须记得使用Tags{"Queue"="Transparent"}+关掉ZWrite(部分情况下ZWrite需要开启)
    SubShader
    {
        Tags{"Queue"="Transparent"}
        Pass{
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
        }
    }
  1. 贴图半透效果
  • 当前贴图无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);
            }
        }
    }
  1. 其他混合模式
    常用的柔和叠加模式:Blend SrcAlpha One

PART6 边缘光

  1. 得到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;
            }
        }
    }
  1. 得到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);
            }
  1. 在片元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;
            }
  1. 得到边缘光效果
  • (来自弹幕佬的解释)边缘处的法线和视角方向接近垂直,dot=0,1-dot进行取反,边缘就显示出颜色,其他部分就较为透明。
            half4 frag(v2f i) : SV_Target {
                ...
                float rim=1.0-NdoV;
                return rim.xxxx;
            }
  1. 边缘光改色
  • 也可以用alpha通道调:float alpha=saturate((1.0-NdoV)*_Emiss);
            half4 frag(v2f i) : SV_Target {
                float3 col=_MainColor.xyz*_Emiss;//乘倍增系数,Inspector可调
                ...
                return float4(col,alpha);
            }
  1. 边缘光对比度调节
    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);
            }
        }
    }
  1. 模型透过问题:预先写深度
    经过上面的步骤,模型能够看到内部的透过结构。如何避免这种现象?

☝「打开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