变换矩阵

模型>世界

设模型的x轴y轴z轴在世界的方向是vX、vY、vX,模型的世界坐标是(xM,yM,zM)。变换矩阵是

vXx        vYx        vZx        xM

vXy        vYy        vZy        yM

vXz        vYz        vZz        zM

0        0        0        1

面法向和顶点法向

面法向是垂直面的向量。顶点法向是由顶点参与的面的法向加权平均(通常是面积加权)算出的向量。模型文件里存的是顶点法向,因为面法向通过2个边叉乘就能算出。

切线空间

首先切线空间是一个顶点在它参与的一个具体三角形里的概念。就是说一个顶点参与了3个三角形,那么它有3个各自不同的切线空间。

原点在顶点,z轴是面法线,x轴是切线,y轴是副切线。切线方向是 纹理U坐标增加的方向。副切线方向是 纹理V坐标增加的方向

对于每个三角形,可以通过顶点位置和UV坐标计算:

// 三角形的三个顶点:v0, v1, v2
// 对应的UV坐标:uv0, uv1, uv2

// 计算边向量
float3 edge1 = v1 - v0;
float3 edge2 = v2 - v0;

// 计算UV差值
float2 deltaUV1 = uv1 - uv0;
float2 deltaUV2 = uv2 - uv0;
//设:
edge1= deltaUV1.u*T+ deltaUV1.v*B;
edge2= deltaUV2.u*T+ deltaUV2.v*B;
//其中T、B是切线、副切线,三维向量

// 计算切线(T)和副切线(B)
float f = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

float3 tangent = f * (deltaUV2.y * edge1 - deltaUV1.y * edge2);
float3 bitangent = f * (-deltaUV2.x * edge1 + deltaUV1.x * edge2);

// 确保正交性(Gram-Schmidt正交化)
tangent = normalize(tangent - normal * dot(normal, tangent));

看不懂。我们可以想象blender里的uv展开:

这里的u和v方向,朝右朝上,把这两个方向映射回3D模型空间,就是切线、副切线。不过注意,模型三角面和3个顶点在贴图上组成的三角形不一定相似。我们可以把一个顶点拉走,让2个三角形形状明显不同:

此时我们想知道左图的u方向映射到右边空间的向量。我想到可以用v0v1、v0v2作基底表示u向量,得到基底坐标值(x,y),然后到三维空间还用(x,y)带入三维空间的v0v1、v0v2。

Phong高光和Blinn Phong高光对比

假设光和法向的夹角是a,视线和法向的夹角是b,则phong高光反射的夹角是abs(a-b),Blinn Phong高光反射的夹角是abs((a-b)/2),同样情况下Blinn Phong高光反射更亮。

纹理采样、漫反射、高光反射、环境光几个成分是相乘还是相加的问题

如果全相乘,我们知道高光反射大部分区域是黑的,乘其他成分也都是黑的,高光反射应该只对光斑附近有影响,所以+高光反射。

漫反射应该使纹理背光面变暗变淡,向光面不受影响,所以纹理*漫反射。(如果纹理+漫反射,很明显是背光面是纹理,向光面发白)

环境光呢?乘环境光,会变暗,加环境光又会加上一层白雾。所以环境光不应该在最终的公式中单独成一项,它可以给漫反射打底。或者说如果作为加法项,应该乘贴图采样,避免”白雾“效果。

最终的公式是:纹理采样*(漫反射+环境光)+高光反射

渲染管线的流程

前向渲染

顶点坐标变换(模型空间>世界空间>相机空间>裁剪空间)得到顶点在屏幕的坐标和深度>光栅化决定每个三角形包含的像素(三次叉乘算法)>着色(根据颜色或采样贴图得到基础颜色,然后计算漫反射、高光反射、环境光决定颜色深度)>模板测试>深度测试>透明度混合>写入帧缓冲。

延迟渲染

顶点坐标变换(模型空间>世界空间>相机空间>裁剪空间)得到顶点在屏幕的坐标和深度>光栅化决定每个三角形包含的像素(三次叉乘算法)>深度测试>模板测试>写入G Buffer(像素的位置、法线)>着色>写入帧缓冲。

裁剪空间

范围(-1,-1,-1)到(1,1,1).

正交相机:原点是远裁剪面、近裁剪面中间。顶点的相机空间(x/相机半宽,y/相机半高,(2z-N-F)/(N-F))

光栅化

不偷懒的情况下,对视口里的每个三角形遍历每个像素,使用三次叉乘看符号决定像素在不在三角形里。但很明显可以取三角形3个顶点x、y坐标的最小最大值,得到包围盒,只计算包围盒里的像素。

计算颜色时颜色大于1怎么办?

根据是否启用了HDR(High Dynamic Range高动态范围),如果启用了,把颜色使用形如1-exp(-color)的公式从0-无穷大映射到0-1,没有启用则大于1的直接等于1.

.shader(ShaderLab)脚本的结构

参考文章

2024-02-01 Unity Shader 开发入门4 —— ShaderLab 语法-CSDN博客

官方文档

ShaderLab - Unity 手册

.shader是Unity定义的文件类型。它的基本结构为:

Shader "自定义着色器名字"
{
    Properties
    {
        _变量名("面板显示名",类型)=默认值{选项}
        ...
        //类型有2D:贴图、Int、Float、Range(min,max)、Color、Vector:四维向量
    }
    SubShader
    {
        Tags{"键1"="值1"...}//标签有Queue、RenderType、DisableBatching、ForceNoShadowCasting、PreviewType等
        渲染状态//有Cull (Back/Front/Off)、ZWrite(On/Off)、LOD xxx、Blend等
        Pass
        {
            Name "通道名字"//其他着色器可以通过UsePass "自定义着色器名称/通道名字"使用这个通道
            Tags{"键1"="值1"...}//标签有LightMode(常用ForwardBase、ShadowCaster)
            YYYPROGRAM//YYY可选CG或HLSL
            YYY语言代码...
            ENDYYY
        }
        Pass
        {
            YYYPROGRAM//YYY可选CG或HLSL
            YYY语言代码...
            ENDYYY
        }
    }
    Fallback "备用着色器"
}

两种着色器语言对比

CG HLSL
全称 C for graphics High level shader language
适配的渲染管线 Builtin RP URP

其他区别:cg有fixed4数据类型,hlsl没有; 

Properties

_MainTex("主贴图",2D)="white"{}
_MyFloat("浮点型参数",Float)=1
_MyRange("范围浮点型参数",Range(0,4))=1
_MyColor("颜色参数",Color)=(1,1,1,1)
_MyVec("向量",Vector)=(0,0,0,0)
_MyInt("整形参数",Int)=1
[Enum(UnityEngine.Rendering.CullMode)] _CullMode ("Cull Mode", Float) = 2

SubShader Tags

Tags{
    "RenderType"="Opaque"//Transparent、TransparentCutout(树叶)、Background、Overlay
    "Queue"="Geometry"//Background、Geometry、AlphaTest、Transparent、Overlay
    "DisableBatching"="True"//False、LODFading
    "ForceNoShadowCasting"="False"//"True"
    "IgnoreProjector"="True"
}

AlphaTest和Transparent的区别:AlphaTest根据Alpha是否达到阈值,要么显示,要么不显示(用于显示形状不规则的树叶,树叶外面的区域都是低Alpha),Transparent在物体和它后面的像素之间混合。

Queue和RenderType是对应的

Queue RenderType
Geometry Opaque
Transparent Transparent
Background Background
AlphaTest TransparentCutout
Overlay Overlay

渲染状态

Cull Back//Off、Front、[_CullMode]材质面板控制
ZWrite On//Off
ZTest Less//Greater、LEqual、GEqual、Equal、NotEqual、Always
Blend SrcAlpha OneMinusSrcAlpha//One One、OneMinusDstColor One、DstColor Zero
//DstColor SrcColor、One OneMinusSrcAlpha
LOD 100

不同Blend对比

SrcAlpha OneMinusSrcAlpha

One One:颜色相加

OneMinusDstColor One

DstColor Zero:颜色相乘

DstColor SrcColor:二倍颜色相乘

Pass Tags

Tags{"LightMode"="ForwardBase"
//Always、ForwardAdd、Deferred、ShadowCaster、……
"RequireOptions"="SoftVegetation"
"PassFlags"="OnlyDirectional"
}

GrabPass

GrabPass
{
    "_BackgroundTexture"
}
//把当前屏幕输出到_BackgroundTexture

Properties和Pass变量对应

Color,Vector float4,half4,fixed4
Range,Float,Int float, half, fixed
2D sampler2D

顶点着色器和片元着色器

着色器分为两个阶段:顶点着色器Vertex shader和片元着色器Fragment shader。通过

#pragma vertex xxx

#pragma fragment yyy

指定顶点着色器函数为xxx,片元着色器函数为yyy。着色器脚本里还会定义两个结构体:顶点着色器的输入结构体(一般叫appdata)、顶点着色器传递到片元着色器的结构体(一般叫v2f)。顶点着色器的输出类型和片元着色器的输入类型虽然一样,但中间发生了从逐顶点到逐像素的插值,frag的输入结构体数量等于像素数量,远大于vert的输出结构体数量(等于顶点数量)。

语义

就是着色器脚本里全大写的量:POSITION, TEXCOORD0等,对应cpu或gpu里的寄存器。在顶点着色器的输入结构体的字段后面加:POSITION意为从cpu的这个寄存器取数据,取出的是顶点在模型本地的坐标。其他语义类似。

在顶点到片元结构体的字段后面加:XXX意为写入到gpu的这个寄存器。

对TEXCOORD0的理解

TEXCOORD里面存的是和模型顶点数量相同的float2。appdata里的:TEXCOORD0是从模型顶点数据取数据。而在v2f里的:TEXCOORD0的意思是:

在 v2f 结构体(vertex-to-fragment)中,TEXCOORD0 的含义与 appdata 中不同,它不是模型数据,而是插值寄存器,用于在顶点着色器和片元着色器之间传递数据。

struct v2f
{
    float2 uv : TEXCOORD0;  // ← 插值寄存器(用于传递数据)
};

TEXCOORD0 在这里是一个语义标签,告诉GPU:"请把这段数据在光栅化时进行插值,然后传递给片元着色器"。

在 v2f 结构中:

  • TEXCOORD0 是插值寄存器语义

  • 用于顶点→片元的数据传递

  • GPU会在光栅化时自动插值

  • 可以传递任意数据(不限于UV坐标)

  • 数字编号只是标识符,没有特殊含义

系统值System value

命名为SV_XXX的语义叫系统值,是gpu拿去渲染用的数据。常见的有SV_POSITION是顶点在裁剪空间的坐标,SV_TARGET是顶点的颜色值。

顶点位置空间转换和向量(比如法线)空间转换不能用一个转换矩阵吗?

我们这样想,模型空间比世界空间平移了(3,0,0),旋转、缩放都不变化,那么一个向量,它在这两个坐标系的方向是一样的,但是一个点在两个坐标系的位置不一样。

所以顶点位置空间转换矩阵是4x4,向量空间转换矩阵是3x3。

向量的空间转换相当于两个顶点空间转换后相减。

编译预处理指令#pragma

#pragma的作用很多,无法用一句话概括。它的具体作用取决于后面的第一个参数。如上面指定顶点和片元着色器。

对#pragma multi_compile的理解

参考文章

【unity shader变体之#pragma multi_compile 和 #pragma shader_feature -  CSDN App】https://blog.csdn.net/qq_17347313/article/details/106872268?sharetype=blogdetail&shareId=106872268&sharerefer=APP&sharesource=Jesui&sharefrom=link

和#define类似,都是条件编译,而#pragma multi_compile只是声明一组“候选”宏,后面的宏也不一定启用,需要shader.EnableKeyWord()或material.EnabledKeyWord()才相当于#define了这个宏。条件编译时也是用#if defined()。相当于用非编译预处理指令定义宏。

LOD

官方介绍

ShaderLab:为子着色器指定 LOD 值 - Unity 手册

首先着色器LOD和模型LOD是两个概念。通过设置shader.maximumLOD,在一列subshader里使用第一个遇到的不大于maximumLOD的subshader(如果想使用尽量高品质的着色器就把LOD降序排列)。它就是一个着色器选择器。

HLSL的语法和API

CBUFFER_START()和CBUFFER_END

这是hlsl语言用的,用来把Properties里不是贴图(对所有顶点一样,也就是uniform)的参数搬运过来,在程序里使用。

Shader "自定义着色器"
{
    Properties
    {
        _AAA("哈哈哈",Float)=0
    }
    SubShader
    {
        
        Pass
        {
            Name "通道1"
            HLSLPROGRAM
            CBUFFER_START(UnityPerMaterial)
            float _AAA;
            ENDHLSL
        }
    }
    Fallback "备用着色器"
}

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);

VertexPositionInputs是一个结构体,里面包含顶点在各个空间下的坐标。GetVertexPositionInputs(float3 positionOS)用于从模型空间得到各个空间的坐标。

阴影

产生阴影的原理是光源每帧根据相机的位置、方向,确定一个“阴影相机”的覆盖范围,记录“阴影相机”画面各位置的深度,生成一个阴影纹理。在v2f中使用SHADOW_COORDS(idx)声明阴影坐标变量。在顶点着色器,TRANSFER_SHADOW(o)计算此顶点在阴影纹理的坐标,存在_ShadowCoord。片元着色器里SHADOW_ATTENUATION(i)根据i._ShadowCoord和阴影纹理存的深度比较,大于存的深度就是阴影。

阴影Pass,阴影投射者和接收者都需要应用这种材质。

Pass
{
    Tags{"LightMode" = "ShadowCaster"}

    Blend One One

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdadd_fullshadows // 支持多重编译,包含完整阴影
    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"

    // 定义顶点到片段的数据结构,与基础Pass一致
    struct v2f
    {
        float4 pos : SV_POSITION;
        float4 vertex : TEXCOORD1;
        SHADOW_COORDS(2)
    };

    fixed4 _MainColor;

    // 顶点着色器
    v2f vert (appdata_base v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.vertex = v.vertex;
        TRANSFER_SHADOW(o)
        return o;
    }
    fixed4 frag (v2f i) : SV_Target
    {
        // 转换顶点到世界空间
        float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
        // 使用阴影宏计算阴影系数
        UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
        fixed4 color = _LightColor0 * _MainColor;
        // 应用阴影到颜色
        color.rgb *= shadowmask;

        return color; // 返回最终颜色
    }
    ENDCG
}

内置代码

按常用程度。变量类宏定义一般以_开头,常量类宏定义全大写。

UnityObjectToClipPos(float4 v)

tex2D(贴图,uv)

uv范围是0-1,对应贴图从左到右,从下到上。当uv超过(0,1)时根据贴图的Wrap Mode决定是就采样0、1还是循环采样。

那么下面这一段:

fixed4 col=tex2D(_MainTex,i.vertex.xy/100);

就把此材质的物体变成了一个“窗口”,贴图以一定缩放平铺在屏幕里,在模型显示的区域能看见贴图。

float3 UnityObjectToWorldNormal(float3 norm)

_WorldSpaceLightPos0

从物体表面指向光源的方向。类型为float4,分量w为0表示平行光,1表示点光源。

_LightColor0

主光源颜色。需要包含"Lighting.cginc"

_WorldSpaceCameraPos

相机世界位置。

UNITY_LIGHTMODEL_AMBIENT

类型float4,表示环境光的颜色。

unity_ObjectToWorld

模型转世界空间的矩阵。

reflect(i,n)

计算反射方向。

float3 WorldSpaceViewDir(float4 v)

float3 ObjSpaceViewDir(float4 v)

float3 WorldSpaceLightDir(float4 v)

flaot3 ObjSpaceLightDir(float4 v)

模型空间,顶点指向光源的方向。对平行光,所有顶点相同。

float3 UnityObjectToWorldDir(in float3 dir)

float3 UnityWorldToObjectDir(float3 dir)

UnityObjectToWorldNormal和UnityObjectToWorldDir的内部有什么区别?

主要在于如何处理非均匀缩放

特性 UnityObjectToWorldNormal UnityObjectToWorldDir
用途 专门用于法线向量 用于一般方向向量(切线、视线、光源方向等)
缩放处理 使用逆转置矩阵处理非均匀缩放 使用普通旋转矩阵,忽略缩放或假设均匀缩放
数学原理 乘以模型矩阵的逆转置 乘以模型矩阵的旋转部分
归一化 自动重新归一化 通常不重新归一化(除非输入已归一化)
典型用途 法线贴图、光照计算 切线方向、向量运算

_MainTex_TexelSize

贴图像素尺寸。

具体来说,这个变量是一个四元数(Vector4),包含以下四个分量值:

  • x‌:1 / 纹理宽度(每个像素在UV空间的宽度)
  • y‌:1 / 纹理高度(每个像素在UV空间的高度)
  • z‌:纹理宽度(像素数量)
  • w‌:纹理高度(像素数量)

假设你的纹理分辨率是512×512像素,那么_MainTex_TexelSize的值就是:
Vector4(1/512, 1/512, 512, 512)

采样一个像素的上下左右像素:

half2 destUv = half2(_MainTex_TexelSize.x, _MainTex_TexelSize.y);
half spriteLeft = tex2D(_MainTex, i.uv + half2(destUv.x, 0)).a;
half spriteRight = tex2D(_MainTex, i.uv - half2(destUv.x, 0)).a;
half spriteBottom = tex2D(_MainTex, i.uv + half2(0, destUv.y)).a;
half spriteTop = tex2D(_MainTex, i.uv - half2(0, destUv.y)).a;

TRANSFORM_TEX(uv,tex)

TRANSFORM_TEX(v.uv,_MainTex)应用贴图的拉伸、偏移。相当于

v.uv.xy*_MainTex##_ST.xy+_MainTex##_ST.zw

_Time

_Time 是一个float4类型的变量,包含四个分量:

  • x分量‌:t/20(时间除以20)
  • y分量‌:t(原始时间)
  • z分量‌:t×2(时间乘以2)
  • w分量‌:t×3(时间乘以3)

其中t是从当前场景加载开始所经过的时间(以秒为单位)

着色器应该怎么学

写着色器有几个特点

  1. 没有打印功能,难调试;
  2. 没有代码提示;
  3. 大量用到UnityCG.cginc、AutoLight.cginc的宏定义;

着色器代码调试

直接输出查看:

return fixed4(result,result,result,result);

实战

逐顶点Phong

这里没有加单独的漫反射颜色,不想让参数太多。

Shader "逐顶点Phong"
{
    Properties
    {
        _Color ("Color", Color) =(1,1,1,1)
        _SpeColor("高光颜色",Color)=(1,1,1,1)
        _Gloss("高光范围",Range(0.1,10))=5
        _MainTex("MainTex",2D)="white"{}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv:TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                fixed3 spec:TEXCOORD0;
                fixed3 albedo:TEXCOORD2;
                float2 uv:TEXCOORD1;
            };
            fixed4 _Color;
            fixed4 _SpeColor;
            float _Gloss;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float3 worldNormal=normalize(UnityObjectToWorldNormal(v.normal));
                float3 lightPos=normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse=_LightColor0.rgb*_Color*max(0,dot(worldNormal,lightPos));
                
                float3 lightDir=-normalize(_WorldSpaceLightPos0.xyz);
                float3 reflDir=reflect(lightDir,worldNormal);
                float3 worldVer=mul(unity_ObjectToWorld,v.vertex);
                float3 viewDir=normalize(_WorldSpaceCameraPos-worldVer);
                fixed4 spec=_LightColor0*_SpeColor*pow(max(0,dot(reflDir,viewDir)),_Gloss);

                o.albedo=diffuse+UNITY_LIGHTMODEL_AMBIENT;
                o.spec=spec;
                o.uv=v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed3 color=tex2D(_MainTex,i.uv)* i.albedo+ i.spec;
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

透明度随时间变化

注意要支持透明必须写

Tags{
    "Queue"="Transparent"
    }

Blend SrcAlpha OneMinusSrcAlpha

Shader "透明度随时间变换"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _RandomSeed("_MaxYUV", Range(0, 10000)) = 0.0
    }
    SubShader
    {
        Tags{
            "Queue"="Transparent"
            }
        LOD 100
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            UNITY_DEFINE_INSTANCED_PROP(float, _RandomSeed)
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                half randomSeed = UNITY_ACCESS_INSTANCED_PROP(Props, _RandomSeed); // 获取随机种子值
                fixed4 col = tex2D(_MainTex, i.uv);
                col.w=(_Time.y + randomSeed) % 1;
                return col;
            }
            ENDCG
        }
    }
}

裁剪空间坐标z的含义

用这个代码测试:

fixed4 frag (v2f i) : SV_Target
{
    fixed col=i.vertex.z;
    return col;
}

看起来相机处是1,远离相机快速变成0.

描边

参考:

Quick Outline | Particles/Effects | Unity Asset Store

它是由2个材质Mask和Fill组成,Mask的队列值小于Fill,比Fill先渲染。Fill负责把物体渲染成纯色且比真实形状大一些,通过

output.position = UnityViewToClipPos(viewPosition + viewNormal * -viewPosition.z * _OutlineWidth / 1000.0);

把顶点向外移一些。而Mask先通过模板测试,使用

Stencil {
  Ref 1
  Pass Replace
}

把模板缓冲区写成1,然后Fill的模板设置

Stencil {
  Ref 1
  Comp NotEqual
}

凡是Mask渲染过的像素,模板缓冲区是1,Fill通不过测试,不渲染,显示之前的像素。这需要Mask在Fill之前渲染,如果把Mask的队列改成大于3110,它就在Fill后面渲染,就会失效,物体变成纯色。

我没有搞懂的是Mask里没有顶点着色器,没有输出到SV_Position,怎么知道Mask覆盖的区域的?

漫反射逐顶点和逐像素的对比

逐顶点:v2f里记录颜色;vert里计算漫反射颜色;frag里乘颜色;

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    fixed3 color:COLOR;
};
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);
    fixed3 diffuse=_LightColor0.rgb*saturate(dot(worldNormal,worldLight));
    o.color=diffuse;
    return o;
}
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    col.rgb=col.rgb*i.color;
    return col;
}

逐像素:v2f里记录法线;vert里计算法线;frag里计算漫反射颜色,应用;

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    float3 worldNormal:TEXCOORD1;
};
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 col = tex2D(_MainTex, i.uv);
    fixed3 worldNormal=normalize(i.worldNormal);
    fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
    fixed3 diffuse=_LightColor0.rgb*saturate(dot(worldNormal,worldLight));
    col.rgb*=diffuse;
    return col;
}

UnityURPToonLitShaderExample着色器有时候会显示在Failed to compile里:

重新导入后会不再显示Failed to compile

UnityURPToonLitShaderExample着色器应用后,关闭项目再打开,妮露和甘雨的着色出现黑色:

把人物删掉,重新导入,应用Shader,人物显示正常:

关闭项目重新打开后又出问题。

妮露的头发的渲染由两个材料完成,“前蝴蝶结”渲染大部分颜色:

“后发”渲染头发上的光泽,大部分是黑的:(可以看到出问题的时候头发光泽还在)

推测原因是把“后发”这个材料覆盖在了“前蝴蝶结”上。

解决方法:后发材料勾选Alpha Clipping。这个选项的意思是把贴图里不透明度alpha值不大于Cutoff的部分设置为透明,让其他材料渲染。

关于下面的Cutoff:为0时预览材质球跟之前一样,大部分是黑的;稍微调大一点黑色消失,只剩下光泽;再调大一点光泽也消失。但是调这个对场景里的效果没影响。

但是甘雨的后背材料“肌”勾选Alpha Clipping没用。给衣服的材料“裙摆”勾选Alpha Clipping有用:

同时发现“裙摆”预览材质球的一些部分由黑色变透明,所以是“裙摆”该设置透明的部分没有透明,渲染成黑色,盖在了后背的材料“肌”上。

总之,所有预览材质球里有这种黑色部分的材料都应该勾选Alpha Clipping。如果没有,勾不勾选就没有区别。

从顶点着色器到片元着色器的结构体里的一个变量没有写语义

系统会自动加上这一段:

给着色器加阴影效果

解决方法:在Shader最后加Fallback "Diffuse",GPU用自定义的着色器渲染完会调用Diffuse着色器,Diffuse着色器里面有LightMode=ShadowCaster的Pass,用于投射阴影。

投射阴影需要在shader脚本里定义LightMode=ShadowCaster的pass并在里面写相应代码。使用了Fallback Diffuse后如果自己写的脚本没有LightMode=ShadowCaster的pass就会使用Diffuse的。

背面剔除

背面剔除后从模型内部看不到模型。

URP内置的Lit和Unlit着色器的背面剔除叫Render Face。

自己写代码是声明一个[Enum(UnityEngine.Rendering.CullMode)]类型的变量,再在Pass开头写Cull[变量名]

某些人的衣服颜色不对

着色器是SimpleURPToonLitExample(With Outline)和URP/Lit会有此问题。URP/Unlit没问题。

更奇怪的是改变BlendShapes衣服会变色:

改成别的着色器如URP/Unlit,好了。所以认为是着色器不完善,细节无法深究。

Instancing: Property 'unity_RenderingLayer' shares the same constant buffer offset with 'unity_LODFade'. Ignoring.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

移动相机时场景里的树明暗突变,可能是这个报错的结果。

后来我想起我导入了URP的例子,删除例子,场景里的东西变成淡蓝色,重新渲染了。然后问题消失了。不过具体机制还是不懂。


场景里没有阴影时可以检查哪些地方

Project settings-Graphics-Scriptable render pipeline settings点进URP Asset的检查器,在Lighting部分检查Cast Shadows

在场景里的光源检查器的Shadows部分检查Shadow Type。

物体的Mesh renderer或Skinned mesh renderer的Lighting部分的Cast shadows

总之,检查Asset和Component的设置。

试图写接收阴影的着色器,失败

按照

Unity中Shader阴影的接收_shader 阴影-CSDN博客

写的,没有看到其他物体的阴影在人物上造成明暗差异的效果。v2f中定义了SHADOW_COORDS()而非UNITY_SHADOW_COORDS(),因为查阅AutoLight.cginc已经改成这个名字了。

Shader "学习/贴图加漫反射"//名称
{
    Properties
    {
        _MainTex ("贴图", 2D) = "white" {}
        [Enum(UnityEngine.Rendering.CullMode)]_CullMode("剔除模式",float)=2
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass//一次渲染过程
        {
            Cull [_CullMode]//关闭背面剔除Cull Back背面剔除
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM

            #pragma vertex vert//顶点着色器声明,类型 名字
            #pragma fragment frag//片元着色器声明,类型 名字
            #pragma multi_compile DIRECTIONAL POINT SHADOWS_SCREEN
            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            struct appdata//从CPU拿的数据结构,名字可自定义
            {
                float4 vertex : POSITION;//顶点在模型空间下的坐标,vertex是变量名,可自定义;POSITION特定语义词
                float2 uv : TEXCOORD0;//第一套uv
                float3 normal : NORMAL;
            };

            struct v2f//从顶点着色器传递到片元着色器的数据结构,名字可自定义
            {
                float2 uv : TEXCOORD0;//储存器
                float4 vertex : SV_POSITION;
                float3 normalInWorld:NORMAL;
                float4 worldPos :TEXCOORD2;
                SHADOW_COORDS(3)
            };

            sampler2D _MainTex;//把Properties里的同名变量传进SubShader
            float4 _MainTex_ST;//贴图拉伸和偏移

            v2f vert (appdata v)//顶点着色器
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);//应用贴图拉伸和偏移
                o.normalInWorld=mul((float3x3)unity_ObjectToWorld,v.normal);
                TRANSFER_SHADOW(o)
                o.worldPos = mul(unity_ObjectToWorld,v.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target//片元着色器。冒号后面是渲染目标
            {
                //贴图采样
                fixed4 col = tex2D(_MainTex, i.uv);//fixed精度8位,half精度16位
                //下面是计算漫反射
                fixed3 worldNormal=normalize(i.normalInWorld);
                //获得直射光的方向
                fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
                //漫反射
                fixed3 diffuseColor=_LightColor0.rgb*col.rgb*(dot(worldNormal,worldLightDir)*0.5+0.5);
                //增加环境光颜色
                fixed3 color=col.rgb;
                color*=UNITY_LIGHTMODEL_AMBIENT.xyz;
                color+=diffuseColor;
                //接收阴影
                // float attenuation=1;
                // attenuation= SHADOW_ATTENUATION(i);
                UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
                color*=attenuation;
                return fixed4(color,1);
            }
            ENDCG
        }

又尝试把UNITY_LIGHT_ATTENUATION输出的值返回:

UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
                color*=attenuation;
                return fixed4(attenuation,attenuation,attenuation,1);

人物显示为全白色,也就是全1:

开头加入

#pragma multi_compile_fwdbase

则TRANSFER_SHADOW(o)的位置报错

让SimpleURPToonLitOutlineExample着色器接收阴影

接收阴影的原理:

  1. 使用一个世界坐标转阴影贴图坐标的函数(HLSL里是TransformWorldToShadowCoord())得到模型顶点的阴影坐标;
  2. 把阴影坐标输入一个计算阴影衰减的函数(HLSL里是MainLightRealtimeShadow(float4 shadowCoord))得到衰减值,就是一个浮点数;
  3. 应用阴影衰减,通常是把输出的颜色乘上衰减。

SimpleURPToonLitOutlineExample里的修改方法:

1.开启关键字法

1.选中要接收阴影的材质,在右上角三个点开启Debug;

2.在Valid Keywords下加一个_MAIN_LIGHT_SHADOWS;

注意加关键字的时候可能在下面的Invalid Keywords加了一个元素,不用管,把关键字粘贴进去,如果正确就会进入Valid Keywords里面。

效果:

2.修改代码法

1.在SimpleURPToonLitOutlineExample_Shared.hlsl的InitializeLightingData()函数里加上lightingData.shadowCoord=TransformWorldToShadowCoord(input.positionWSAndFogFactor.xyz);加上之后这个函数变成:

ToonLightingData InitializeLightingData(Varyings input)
{
    ToonLightingData lightingData;
    lightingData.positionWS = input.positionWSAndFogFactor.xyz;
    lightingData.viewDirectionWS = SafeNormalize(GetCameraPositionWS() - lightingData.positionWS);  
    lightingData.normalWS = normalize(input.normalWS); //interpolated normal is NOT unit vector, we need to normalize it
    lightingData.shadowCoord=TransformWorldToShadowCoord(input.positionWSAndFogFactor.xyz);
    return lightingData;
}

2.在ShadeAllLights()函数里的GetMainLight输入lightingData.shadowCoord:

Light mainLight = GetMainLight(lightingData.shadowCoord);

相当于把#ifdef _MAIN_LIGHT_SHADOWS #endif里的代码拿出来了。

关键函数:

InitializeLightingData();

ShadeAllLights();

ShadeSingleLight();

Develop build里树的近距离模型看不见

解决方法:在场景里放一个树的预制体,不要只把树用于地形工具绘制树。😓

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐