Unity-Shader详解-其二

news2025/4/28 15:05:12

前向渲染和延迟渲染

前向渲染和延迟渲染总的来说是我们的两种主要的渲染方式。

我们在Unity的Project Settings中的Graphic界面能够找到渲染队列的设定:

我们也可以在Main Camera这里进行设置:

那这里我们首先介绍一下两种渲染(Forward Renderring\Deferred Renderring)的基本概念:

渲染就像去餐馆吃饭,每一桌就是一个需要渲染的对象,我们在吃饭前要先指指点点(渲染的设置),前向渲染就是比较朴素的餐馆运行模式:针对每一桌客人,我们都执行:客人下单,上菜,执行完一桌之后我们才去咨询下一桌,这样的话问题:作为餐馆的服务人员,我们来回跑了太多次(调用GPU进行渲染的指令),且这样计算的总开销时间较长(下一桌客人一定要等到上一桌客人的菜上完才能点菜);延迟渲染就是我们先总的收集所有客人的点餐情况,然后根据总的要求进行上菜,这样最大的改善就是我们大大减少了咨询客人点菜情况的时间,且减小了整体的计算时长。

两种渲染方式在多光源场景下的性能差异尤为明显:前向渲染的过程中我们要分别每个渲染对象单独地计算完光照效果之后再计算下一个对象,但是对于延迟渲染来说,整个场景的所有物体只用计算一次光照,本身光照计算就几乎是开销最大的部分,这样可以相当大一部分节省性能。

但是显然延迟渲染并不是十全十美的,你能统计全餐馆的下单情况的前提是你得有足够大的一个记事本,在计算机里这个记事本叫做:G-BUFFER,我们会把各个对象的诸多信息放入这个缓冲区,然后后续再在缓冲区中统一计算光照。因此,从这个角度来说,我们可以认为延迟渲染是前向渲染的以空间换时间的一种方式。

我们现在展开来说:

通俗地说,针对场景里的多光源,前向渲染会给光源分为三个优先级:最亮的一档就会采取逐像素渲染,而最不重要的一档我们会直接使用球谐函数来生成一个结果避免复杂运算,中间的一档(逐顶点渲染)则不会超过四个。其中每个光源并不是一定处以某个优先级,而是一个两种优先级的混合叠加态。

上述说的光源的优先级和限制的逐像素渲染光源数都是可以修改的:

这是前向渲染处理多光源的方法,那么对于延迟渲染来说呢?

延迟渲染是不支持抗锯齿的,这是一个非常致命的问题,同时还不支持半透明:

 

因此,根据不同的场景来选择不同的渲染策略才是最重要的。 

阴影实现

还是那句话,现在并没有完美的生成阴影的算法,大多数都是有些缺陷的。

我们直接上代码来介绍吧:

Shader "Chapter3/chapter3_3_shadow"
{
    Properties
    {
        // 定义主颜色属性,可在材质面板中调整
        _MainColor ("Main Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        // -------- 基础Pass:处理主要光源的投影 --------
        Pass
        {
            // 标签定义,用于指定此Pass的光照模式为"ForwardBase"
            Tags{"LightMode" = "ForwardBase"}

            CGPROGRAM
            // 顶点着色器和片段着色器入口
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase // 多重编译,支持不同光照模型
            #include "UnityCG.cginc"      // 包含Unity常用工具函数和宏
            #include "Lighting.cginc"    // 包含Unity光照计算函数
            #include "AutoLight.cginc"   // 包含Unity自动化光照相关代码

            // 定义顶点到片段的数据结构
            struct v2f
            {
                float4 pos : SV_POSITION;   // 裁剪空间的顶点位置
                float3 normal : TEXCOORD0; // 法线,用于光照计算
                float4 vertex : TEXCOORD1; // 模型空间的顶点位置
                SHADOW_COORDS(2)           // 使用Unity预定义宏存储阴影坐标
            };

            fixed4 _MainColor; // 主颜色变量

            // 顶点着色器
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 转换顶点到裁剪空间
                o.normal = v.normal;                   // 传递法线信息
                o.vertex = v.vertex;                   // 传递顶点位置
                TRANSFER_SHADOW(o)                     // 计算阴影坐标并存储
                return o;
            }

            // 片段着色器
            fixed4 frag (v2f i) : SV_Target
            {
                // 计算法线方向
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);

                // 计算世界空间中的光源方向
                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);

                // 将顶点从模型空间转换到世界空间
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert光照模型:计算法线与光线夹角的点积
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 叠加4个点光源的光照
                color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb, 
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,       // 点光源颜色
                    unity_4LightAtten0, worldPos.rgb, n                     // 衰减和位置
                ) * _MainColor;

                // 叠加环境光照
                color += unity_AmbientSky;

                // 使用Unity宏计算阴影衰减系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

                // 将阴影系数与颜色相乘,应用阴影效果
                color.rgb *= shadowmask;

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

        // -------- 额外的Pass:处理其他逐像素灯光的投影 --------
        Pass
        {
            // 标签定义,此Pass用于附加光源,模式为"ForwardAdd"
            Tags{"LightMode" = "ForwardAdd"}

            // 混合模式:相加混合
            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;
                float3 normal : TEXCOORD0;
                float4 vertex : TEXCOORD1;
                SHADOW_COORDS(2)
            };

            fixed4 _MainColor;

            // 顶点着色器
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                TRANSFER_SHADOW(o)
                return o;
            }

            // 片段着色器
            fixed4 frag (v2f i) : SV_Target
            {
                // 计算法线和光照方向
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);
                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);

                // 转换顶点到世界空间
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert光照计算
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 叠加点光源的光照
                color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                    unity_4LightAtten0, worldPos.rgb, n
                ) * _MainColor;

                // 使用阴影宏计算阴影系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

                // 应用阴影到颜色
                color.rgb *= shadowmask;

                return color; // 返回最终颜色
            }
            ENDCG
        }
    }
    // 回退着色器,定义为Diffuse
    FallBack "Diffuse"
}

我想我需要首先介绍两个Tags中的LightMode:ForwardBase和ForwardAdd。

在代码中我们用:

            // 混合模式:相加混合
            Blend One One

的混合模式把这两种前向渲染的结果进行结合就可以得到一个完整的生成阴影的方法。

现在我们来看具体代码:

        // -------- 基础Pass:处理主要光源的投影 --------
        Pass
        {
            // 标签定义,用于指定此Pass的光照模式为"ForwardBase"
            Tags{"LightMode" = "ForwardBase"}

            CGPROGRAM
            // 顶点着色器和片段着色器入口
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase // 多重编译,支持不同光照模型
            #include "UnityCG.cginc"      // 包含Unity常用工具函数和宏
            #include "Lighting.cginc"    // 包含Unity光照计算函数
            #include "AutoLight.cginc"   // 包含Unity自动化光照相关代码

            // 定义顶点到片段的数据结构
            struct v2f
            {
                float4 pos : SV_POSITION;   // 裁剪空间的顶点位置
                float3 normal : TEXCOORD0; // 法线,用于光照计算
                float4 vertex : TEXCOORD1; // 模型空间的顶点位置
                SHADOW_COORDS(2)           // 使用Unity预定义宏存储阴影坐标
            };

            fixed4 _MainColor; // 主颜色变量

            // 顶点着色器
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 转换顶点到裁剪空间
                o.normal = v.normal;                   // 传递法线信息
                o.vertex = v.vertex;                   // 传递顶点位置
                TRANSFER_SHADOW(o)                     // 计算阴影坐标并存储
                return o;
            }

            // 片段着色器
            fixed4 frag (v2f i) : SV_Target
            {
                // 计算法线方向
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);

                // 计算世界空间中的光源方向
                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);

                // 将顶点从模型空间转换到世界空间
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert光照模型:计算法线与光线夹角的点积
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 叠加4个点光源的光照
                color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb, 
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,       // 点光源颜色
                    unity_4LightAtten0, worldPos.rgb, n                     // 衰减和位置
                ) * _MainColor;

                // 叠加环境光照
                color += unity_AmbientSky;

                // 使用Unity宏计算阴影衰减系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

                // 将阴影系数与颜色相乘,应用阴影效果
                color.rgb *= shadowmask;

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

我们来说新东西:

#pragma multi_compile_fwdbase // 多重编译,支持不同光照模型

这句指令就是允许我们的前向渲染根据场景中的光照动态地调整一些涉及着色器参数的关键字选择。

ForwardBase中主要分成三个部分:v2f,vert和frag。

v2f中我们可以看到熟悉的顶点坐标(裁剪空间和模型空间),法线和一个阴影的预定义宏,其中:

                float4 vertex : TEXCOORD1; // 模型空间的顶点位置
                SHADOW_COORDS(2)           // 使用Unity预定义宏存储阴影坐标

我们法线模型空间的顶点位置居然使用了TEXCOORD1来存储,这不是纹理坐标的语义吗?是的这确实是,但是其实也没人规定你不可以用,只要合乎语法即可(但是有一种语义不可以用,是的就是我们之前提到过的系统值语义:这种语义存储的内容是被规定好的特殊阶段的特殊数据,如果不符合则整个渲染流程报错),阴影的预定义宏则是提前为将来生成的阴影坐标分配好了存储的坐标索引(2)。

TRANSFER_SHADOW(o)                     // 计算阴影坐标并存储

这一步就是计算阴影坐标的方法,Unity的着色器语言为我们封装成了一个函数。

                // 将顶点从模型空间转换到世界空间
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // Lambert光照模型:计算法线与光线夹角的点积
                fixed ndotl = saturate(dot(n, l));
                fixed4 color = _LightColor0 * _MainColor * ndotl;

                // 叠加4个点光源的光照
                color.rgb += Shade4PointLights(
                    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
                    unity_LightColor[0].rgb, unity_LightColor[1].rgb, 
                    unity_LightColor[2].rgb, unity_LightColor[3].rgb,       // 点光源颜色
                    unity_4LightAtten0, worldPos.rgb, n                     // 衰减和位置
                ) * _MainColor;

                // 叠加环境光照
                color += unity_AmbientSky;

                // 使用Unity宏计算阴影衰减系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

                // 将阴影系数与颜色相乘,应用阴影效果
                color.rgb *= shadowmask;

                return color; // 返回最终颜色

这里为什么我们要采取兰伯特模型而不是更精准的冯模型呢?

 我们根据兰伯特模型的公式计算出基本的漫反射光照颜色值之后,再叠加四个点光源的效果,这里我们用了Unity内置的Shade4PointLights函数:

 综上所述,现在我们的漫反射光照颜色值是兰伯特光照模型和点光源效果的总和,我们再添加一个unity自带的:

最后再乘以一个Unity自带的阴影衰落因子就得到了ForwardBase输出的结果。

然后是我们的ForwardAdd部分:
 

// -------- 额外的Pass:处理其他逐像素灯光的投影 --------
Pass
{
    // 标签定义,此Pass用于附加光源,模式为"ForwardAdd"
    Tags{"LightMode" = "ForwardAdd"}

    // 混合模式:相加混合
    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;
        float3 normal : TEXCOORD0;
        float4 vertex : TEXCOORD1;
        SHADOW_COORDS(2)
    };

    fixed4 _MainColor;

    // 顶点着色器
    v2f vert (appdata_base v)
    {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.normal = v.normal;
        o.vertex = v.vertex;
        TRANSFER_SHADOW(o)
        return o;
    }

    // 片段着色器
    fixed4 frag (v2f i) : SV_Target
    {
        // 计算法线和光照方向
        float3 n = UnityObjectToWorldNormal(i.normal);
        n = normalize(n);
        float3 l = WorldSpaceLightDir(i.vertex);
        l = normalize(l);

        // 转换顶点到世界空间
        float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

        // Lambert光照计算
        fixed ndotl = saturate(dot(n, l));
        fixed4 color = _LightColor0 * _MainColor * ndotl;

        // 叠加点光源的光照
        color.rgb += Shade4PointLights(
            unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
            unity_LightColor[0].rgb, unity_LightColor[1].rgb,
            unity_LightColor[2].rgb, unity_LightColor[3].rgb,
            unity_4LightAtten0, worldPos.rgb, n
        ) * _MainColor;

        // 使用阴影宏计算阴影系数
        UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)

        // 应用阴影到颜色
        color.rgb *= shadowmask;

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

我们首先可以看到Blend One One,这是一种混合模式:

除此之外的ForwardAdd的代码几乎与ForwardBase的部分一模一样,我们都只采用兰伯特光照模型即可(其实具体的渲染模式和内部的光照模型并没有直接挂钩,不同的渲染模式主要是负责的职能不同而光照模型则是定义光照计算的方式不同)。

大体效果如图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2343812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入浅出理解并应用自然语言处理(NLP)中的 Transformer 模型

1 引言 随着信息技术的飞速发展,自然语言处理(Natural Language Processing, NLP)作为人工智能领域的一个重要分支,已经取得了长足的进步。从早期基于规则的方法到如今的深度学习技术,NLP 正在以前所未有的速度改变着我…

当自动驾驶遇上“安全驾校”:NVIDIA如何用技术给无人驾驶赋能?

自动驾驶技术的商业化落地,核心在于能否通过严苛的安全验证。国内的汽车企业其实也在做自动驾驶,但是吧,基本都在L2级别。换句话说就是在应急时刻内,还是需要人来辅助驾驶,AI驾驶只是决策层,并不能完全掌握…

【OSG学习笔记】Day 9: 状态集(StateSet)与渲染优化 —— 管理混合、深度测试、雾效等渲染状态

干货开始。_ 一、StateSet核心概念与作用 StateSet 是OSG(OpenSceneGraph)中管理渲染状态的核心类,用于封装 OpenGL 渲染状态(如混合、深度测试、雾效、材质、纹理、着色器等),并将这些状态应用于节点或几何体。 通过合理组织 StateSet,可实现: 渲染状态的高效复用:…

Operating System 实验七 Linux文件系统实验

实验目标: 使用dd命令创建磁盘镜像文件ext2.img并格式化为ext2文件系统,然后通过mount命令挂载到Linux主机文件系统。查看ext2文件系统的超级块的信息,以及数据块的数量、数据块的大小、inode个数、空闲数据块的数量等信息 在文件系统中创建文件xxxxx.txt(其中xxxxx为你的学…

linux中shell脚本的编程使用

linux中shell脚本的编程使用 1.shell的初步理解1.1 怎么理解shell1.2 shell命令 2.shell编程2.1 什么是shell编程2.2 C语言编程 和 shell编程的区别 3.编写和运行第一个shell脚本程序3.1 编写时需要注意以下几点:3.1.1 shell脚本没有main函数,没有头文件…

图像畸变-径向切向畸变实时图像RTSP推流

实验环境 注意:ffmpeg进程stdin写入两张图片的时间间隔不能太长,否则mediamtx会出现对应的推流session超时退出。 实验效果 全部代码 my_util.py #进度条 import os import sys import time import shutil import logging import time from datetime i…

手搓雷达图(MATLAB)

看下别人做出来什么效果 话不多说,咱们直接开始 %% 可修改 labels {用户等级, 发帖数, 发帖频率, 点度中心度, 中介中心度, 帖子类型计分, 被列为提案数}; cluster_centers [0.8, 4.5, 3.2, 4.0, 3.8, 4.5, 4.2; % 核心用户0.2, 0.5, 0.3, 0.2, 0.1, 0.0, 0.0;…

汽车零配件供应商如何通过EDI与主机厂生产采购流程结合

当前,全球汽车产业正经历深刻的数字化转型,供应链协同模式迎来全新变革。作为产业链核心环节,汽车零部件供应商与主机厂的高效对接已成为企业发展的战略要务。然而,面对主机厂日益严格的数字化采购要求,许多供应商在ED…

闻性与空性:从耳根圆通到究竟解脱的禅修路径

一、闻性之不动:超越动静的觉性本质 在《楞严经》中,佛陀以钟声为喻揭示闻性的奥秘:钟声起时,闻性显现;钟声歇时,闻性不灭。此“不动”并非如磐石般凝固,而是指觉性本身超越生灭、来去的绝对性…

第34课 常用快捷操作——按“空格键”旋转图元

概述 旋转某个图元,是设计过程中常需要用到的操作,无论是在原理图中旋转某个图形,还是在PCB图中旋转某个元素。 旋转操作的快捷键是空格键。下面作详细介绍。 按空格键旋转图元 当我们选中一个图元时,按下空格键,即…

基于亚马逊云科技构建音频转文本无服务器应用程序

Amazon Transcribe是一项基于机器学习模型自动将语音转换为文本的服务。它提供了多种可以提高文本转录准确性的功能,例如语言自定义、内容过滤、多通道音频分析和说话人语音分割。Amazon Transcribe 可用作独立的转录服务,也可以集成到应用程序中提供语音…

K8S Service 原理、案例

一、理论介绍 1.1、3W 法则 1、是什么? Service 是一种为一组功能相同的 pod 提供单一不变的接入点的资源。当 Service 存在时,它的IP地址和端口不会改变。客户端通过IP地址和端口号与 Service 建立连接,这些连接会被路由到提供该 Service 的…

实验四 进程调度实验

一、实验目的 1、了解操作系统CPU管理的主要内容。 2、加深理解操作系统管理控制进程的数据结构--PCB。 3、掌握几种常见的CPU调度算法(FCFS、SJF、HRRF、RR)的基本思想和实现过程。 4、用C语言模拟实现CPU调度算法。 5、掌握CPU调度算法性能评价指…

linux blueZ 第四篇:BLE GATT 编程与自动化——Python 与 C/C++ 实战

本篇聚焦 BLE(Bluetooth Low Energy)GATT 协议层的编程与自动化实践,涵盖 GATT 基础、DBus API 原理、Python(dbus-next/bleak)示例、C/C++ (BlueZ GATT API)示例,以及自动发现、读写特征、订阅通知、安全配对与脚本化测试。 目录 BLE GATT 基础概念 BlueZ DBus GATT 模…

Linux线程与进程:探秘共享地址空间的并发实现与内

Linux系列 文章目录 Linux系列前言一、线程的概念二、线程与地址空间2.1 线程资源的分配2.2 虚拟地址到物理地址的转换 三 、线程VS进程总结 前言 在Linux操作系统中,线程作为CPU调度的基本单位,起着至关重要的作用。深入理解线程控制机制,是…

科学养生,开启健康生活新方式

在快节奏的现代生活中,健康养生已成为人们关注的焦点。科学的养生方式不仅能增强体质,还能有效预防疾病,提升生活质量。​ 合理饮食是健康养生的基础。日常饮食应遵循均衡原则,保证蛋白质、碳水化合物、脂肪、维生素和矿物质的合…

外贸图片翻译软件推荐用哪些?不损原图画质的跨境图片翻译器,收藏!

在跨境电商的 “江湖” 里,卖家们怀揣着全球 “捞金” 的梦想扬帆起航,可谁能想到,一个看似不起眼的 “小怪兽”—— 图片翻译难题,却常常让大家在 “出海” 途中 “栽跟头”。 电商跨境图片翻译全能王——风车AI翻译 [fengchef…

3.1/Q1,Charls最新文章解读

文章题目:The impact of chronic diseases and lifestyle on sarcopenia risk in older adults: a population-based longitudinal study DOI:10.3389/fmed.2025.1500915 中文标题:慢性病和生活方式对老年人肌肉减少症风险的影响:…