当前位置: 首页 > news >正文

自由学习记录(58)

Why you were able to complete the SpringBoot + MyBatisPlus task smoothly:

  • Clear logic flow:

    • Database → Entity → Service → Controller → API → JSON response.

    • Errors are explicit, results are verifiable — you know what’s broken and what’s fixed.

  • Stable community support:

    • Tons of examples, tutorials, consistent practices.

    • Stack Overflow, official docs — everything has precedent.

  • I can walk you through it step-by-step, and you can follow, understand, and validate your result.

So it feels like: “If I follow the steps, I know I’ll make it.”

But with Shader and rendering topics:

  • It’s highly abstract, with little feedback:

    • You're asking: “What does this expression mean?” or “Why is this transform needed?”

    • But you don’t get logs or compile errors — you just see a weird screen result (or nothing at all).

  • The knowledge is fragmented:

    • Shader language (HLSL/GLSL) ≠ Unity’s ShaderLab ≠ Unreal’s Material Editor.

    • Most of the explanations lie in academic papers, engine source code, or obscure forums — not in neat tutorials.

  • You’re asking deeper questions than what most people ever go into:

    • “Why is lighting calculated this way?”

    • “What’s the mathematical meaning of this coordinate space?”
      These are rarely answered directly, because most just copy working code without knowing why.

TBN空间的构成

  • T(Tangent)切线:沿着 UV 的 U 方向

  • B(Bitangent)副切线:沿着 UV 的 V 方向(通常通过法线和切线叉乘得到)。

  • N(Normal)法线:表面垂直方向。

#pragma multi_compile_fwdbase

前向渲染路径下的主光照渲染 Pass

#pragma multi_compile_shadowcaster

阴影贴图渲染阶段

v2f 中不能再写 NORMAL,因为这不是固定输出语义了,只能用 TEXCOORD

🌟 “颜色乘法”并不是为了混色而存在,而是为了表示能量削弱(滤光、遮罩、反射比)等真实的物理过程。

fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Shininess);

_LightColor0.rgb 是主动的能量源(光源)
_Specular.rgb 是材质的响应能力(反射率)

人类的解释是一种“语义包装”

  • 从“光源主动打向物体”的角度说:光削弱物体

  • 从“物体决定反射多少”的角度说:物体调制光

✔️ “从直觉出发,我们说:物体‘吸收’或‘反射’光能,但在实际计算中,颜色乘法是对等的,谁在前谁在后只是人为赋予的含义。”

  • 加法更容易失控(亮度飙升、图像曝光)

  • 乘法更容易用于调节颜色权重/叠加色调,因为:

float3 red = float3(1,0,0);
float3 tint = float3(0.8, 1.0, 1.0); // 青蓝遮罩
result = red * tint → float3(0.8, 0, 0); // 红色被冷调“削弱”

在Unity的ForwardBase渲染路径中:

  1. 主光源判定规则
  • 只会自动处理被标记为Important的平行光作为主光源
  • 即使场景中只有一个光源,若被标记为Not Important,也不会在ForwardBase Pass中被自动处理

在 Forward 渲染路径或光照贴图关闭的场景中(比如 #ifdef LIGHTMAP_OFF 成立),你不能用 Lightmap,这时候 Unity 就使用 球谐数据 来补偿环境光。

  • ShadeSH9 只和 环境光 有关,不包含方向光、点光源。

  • 它是 Unity 处理光照探针(Light Probe)/环境光球谐贴图的底层调用。

  • 不需要你自己定义球谐系数,Unity 自动传给你(隐藏在全局 uniform 中)。

ShadeSH9(float4(normal, 1.0)) 实际上是一个在 GPU 端展开的数学表达,用来将顶点/片元的 世界空间法线方向 投入球谐光照函数中,从而计算出这一方向上接收到的环境光照强度。

为什么是 9 项(SH9)

Unity 使用的是 3阶球谐(l=0~2)共9项,这是图形学中环境光最常用的精度:

  • l = 0:常量项(1项)

  • l = 1:线性项(3项)

  • l = 2:二次项(5项)

这就是“SH9”的来源。

unity_SHArunity_SHAgunity_SHAbunity_SHBrunity_SHC 这类 球谐光照系数(Spherical Harmonics Coefficients)不是固定的常量,它们是:

动态计算生成,来源是 环境光照贴图或光照探针

它们的值由 Unity 在运行前(或运行时)预计算生成,流程如下:

🔧 场景光照构建流程(你点击 Build Lighting 时)

  1. Unity 会扫描场景中的所有静态环境光源,包括:

    • 天空盒的环境光照(Environment Lighting)

    • 反射探针(Reflection Probe)

    • 光照探针(Light Probe Group)

    • 光照贴图设置(如果是 Lightmap 模式)

  2. 对于每个探针点 / 网格点,它会:

    • 从环境各个方向采样天空盒 / 光照贴图

    • 使用球谐函数 Yᵢ(n) 来做一次 投影拟合(投影到球谐空间中)

      ⚠️ 你可以理解为:把环境光照在每个方向的强度拟合成一个球面上的函数 → 然后用 9 个基函数去逼近它。

  3. 最后生成 9 个 SH 系数(每个方向一个 Lᵢ),分 R/G/B 通道保存,赋值到对应的:

    • unity_SHAr, unity_SHAg, unity_SHAb...

    • 或者贴图 _SHCoefficientsTex

#ifdef VERTEXLIGHT_ON 是什么?

它用于判断是否启用了 逐顶点光照(Vertex Lighting)
这个宏在 Unity 的 Forward 渲染路径中有特殊用途:

✔ 它负责处理的不是环境光(如球谐函数),而是:

前 4 个非重要点光源的光照影响(per-vertex)

ShadeSH9 是为 间接光照(环境光)

它只根据方向,算出当前方向的环境亮度,没有考虑位置。

Shade4PointLights 是为 直接光照(局部点光源)

它会考虑点光源的位置、法线方向、距离衰减等。

one light source in Unity can be handled either per-vertex or per-fragment, and which shader stage does the calculation depends on how Unity (or you) choose to process it, not on the light itself.

What is Shade4PointLights(...)?

This is a built-in Unity CG/HLSL helper function used in Forward Rendering to calculate the lighting contribution from up to 4 point lights in one go — in the vertex shader.

Since it's batching 4 point lights together, it needs a lot of inputs.

What is the Main Light in Unity?

  • In Forward Rendering, Unity gives 1 main light special treatment:

    • It is usually the brightest directional light in the scene.

    • It's rendered per-pixel (fragment) in the ForwardBase pass.

    • Accessible via built-in variables like _WorldSpaceLightPos0, _LightColor0.

This one light is the "true per-pixel" light by default in forward rendering.

Is the Shade4PointLights dealing with main light?

No, it's not the main directional light.

Shade4PointLights(...) is used to handle additional per-object point lights (up to 4), not the main directional light.

💡 Limitation: Only 4 Additional Lights?

Yes — by default, Unity limits Forward Rendering to:

  • 1 main directional light per object (per-pixel in ForwardBase)

  • Up to 4 additional lights per object (per-vertex in Shade4PointLights)

This is:

  • A performance design.

  • Configurable in Quality Settings > Pixel Light Count (defaults to 4).

  • Per-object — Unity selects the 4 most relevant lights per object.

Only a directional light can be the main light in Unity's Forward Rendering.

❌ You cannot choose a point light, spot light, or any other type to be the "main light".

✅ The main directional light is global, not per-object.

✅ The additional lights (point, spot, etc.) are selected per object, and only up to 4.

📘 Why only directional light for the main light?

Unity’s ForwardBase pass is optimized for:

  • Sunlight / moonlight scenarios (infinite distance, no attenuation).

  • These match the directional light type.

So:

  • Only 1 directional light (the brightest one) gets promoted to “main light”.

  • Its data is passed into shaders via:

    • _WorldSpaceLightPos0 (direction)

    • _LightColor0 (color)

  • It is evaluated per-pixel for high quality.

What about point / spot lights?

These can never be the main light in built-in forward rendering.

They go into the additional lights pool, which:

  • Is limited to 4 lights per object.

  • Is passed via unity_4LightPosX/Y/Z, unity_LightColor[], etc.

  • Evaluated per-vertex (interpolated to fragments).

🧩 If there is no directional light in the scene:

  • Yes, your scene can still render with lighting.

  • ❌ But there will be no "main light" injected into the shader's ForwardBase pass.

  • ✅ Unity will skip the main light logic and rely entirely on additional lights, like point/spot lights.

🧠 How Unity handles this:

✅ ForwardBase Pass

  • If no directional light exists, then the ForwardBase pass contains:

    • Ambient light (environment lighting)

    • SH (Spherical Harmonics) if baked

    • Nothing in _WorldSpaceLightPos0 / _LightColor0 — those will be zeroed.

ForwardAdd Pass(es)

  • All non-directional lights — point & spot — will be handled via:

LightMode = "ForwardAdd"
These passes are generated per-light.

Unity applies additive blending (Blend One One) to stack these.

#ifdef VERTEXLIGHT_ON
    float3 vertexLight = Shade4PointLights(...);
    o.vertexLight += vertexLight;
#endif

意味着:

如果在编译时 VERTEXLIGHT_ON 被定义(define)了,就会启用这段代码,否则就会被跳过。

✅ 那么 VERTEXLIGHT_ON 是谁定义的?

Unity 自动在以下情况下定义这个宏:

✔ 场景中存在使用逐顶点光照(Vertex Light)的光源:

  • 非主光源(non-main lights)

  • 被设置为 Important 以外的优先级(如 Not Important)

  • 使用的是 Forward Rendering 渲染路径

  • 当前物体的 Shader 使用了 ForwardBase pass

🔧 并且 Unity 会将这些次要光源,打包为最多 4 个 point lights 的信息,然后在 vertex shader 中通过 Shade4PointLights 模拟它们的影响(以节省性能)。

#pragma multi_compile_fwdbase

是告诉 Unity 要为多种光照情况编译多个变种(variant),其中一种就是定义了 VERTEXLIGHT_ON 的情况。

Unity 在构建时会根据场景的光源使用情况,自动选择并启用合适的 shader 变种,比如:

情况是否定义 VERTEXLIGHT_ON
只有一个主方向光❌ 不定义
有主光源 + 其他低优先级光源✅ 定义
没有灯光(全黑)❌ 不定义

Forward 渲染路径(Forward Rendering Path) 中,Unity 的默认行为是:

  • 将场景中第一个最重要的 Directional Light 作为 主光源(Main Light)

  • 主光源的光照会在 ForwardBase Pass 中 逐像素处理(per-pixel lighting)

  • 其他光源(如 Point Light、Spot Light)则通过:

    • ForwardAdd pass(逐像素加光)

    • 或者 Shade4PointLights(顶点光照)来模拟

🔸 没有 Directional Light 时会发生什么?

  • 主光源为空(不存在)
    UNITY_LIGHTMODEL_AMBIENT 仍然存在(环境光),但 LightColor0不会被设置为主光源色

  • UnityWorldSpaceLightDir() 可能会返回错误的值或默认值(通常是 float3(0, 0, 0)

  • Shader 中像 _LightColor0.rgb_WorldSpaceLightPos0UnityWorldSpaceLightDir() 等,本质是针对主光源定义的,此时不再生效

除了唯一的主光源(逐像素处理)之外,其余的光源(Point/Spot)默认是逐顶点光照,但在被标记为 Important 且在 ForwardAdd Pass 中生效 的时候,会切换为逐像素光照

在一个 Pass 里的定义(包括 #pragma、变量、宏等)默认只在这个 Pass 里生效

第二个 Pass 里(比如 ForwardAdd)要再次使用这些属性,就需要重新写一遍

如果你有一堆公共的定义想要“共享”而不重复写 —— 就用 #include "MyCommon.cginc" 的方式复用(你可以自己写 .cginc 文件)。

宏开关是由 #pragma 决定的,“宏”不会自动在所有 Pass 中共享,它们只对当前使用了 #pragma multi_compile_* 的 Pass 有效

🌞 ForwardBase(主通道):

  • 只渲染 最多一个主光源(通常是 Directional Light)

  • 如果主光源设置为 重要(Important),会被分配到这里逐像素渲染

  • 同时还会处理:

    • 环境光(Ambient)

    • 球谐光(SH)

    • 光照贴图(Lightmap)

    • 逐顶点的额外光源(Point/Spot)最多 4 个

💡 ForwardAdd(附加通道):

  • 每个光源一个 Pass,使用 加法混合

  • 渲染的通常是非主光源、且设置为 重要(Important) 的 Point/Spot

  • 都是 逐像素渲染,用于加强主光照之外的表现

是不是 Point/Spot 光源 只能在 ForwardAdd Pass 中使用?

不是!

  • Unity 会自动分配非主光源中的最多 4 个,通过 Shade4PointLights() 加到 ForwardBasevertex 阶段,用于 逐顶点光照计算,提高性能。

  • 也就是说:在 ForwardBase Pass 中确实可以看到非主光源的影响,但是以低精度的逐顶点形式。

VERTEXLIGHT_ON 是 Unity 在编译 Shader 时自动定义的

触发条件是:

只要场景中存在:

  • 类型为 Point Light 或 Spot Light

  • 并且这些光源设置为 Not Important(非重要)

  • 并且你当前渲染的是 Forward 渲染路径ForwardBase Pass

就会自动开启宏 VERTEXLIGHT_ON

ForwardAdd Pass 的作用

它负责处理除主光源以外的所有逐像素光源,包括:

光源类型Importance 设置会进入 ForwardAdd 吗?
Point LightImportant✅ 是,逐像素处理
Point LightNot Important✅ 是,逐像素处理
Spot LightImportant✅ 是,逐像素处理
Spot LightNot Important✅ 是,逐像素处理
Directional-(只有一个主光源)❌ 否,ForwardBase 处理

也就是说,只要是 Point 或 Spot 类型的光源,不管它是 Important 还是 Not Important,Unity 都会用 ForwardAdd Pass 进行逐像素处理。

特性ForwardBaseForwardAdd
主光源(主 Directional)✅ 是❌ 否
逐顶点光(仅 4 个)✅ 是(限 Not Important)❌ 否(ForwardAdd 不处理逐顶点)
逐像素光源❌ 否✅ 是
混合方式一般默认Blend One One(累加)

🌞 1. 主光源(Main Light)

  • Unity 自动选择一个 Directional Light 作为主光源。

  • 通常是第一个启用的 Directional Light(也可以在脚本中指定)。

  • 主光源会在 ForwardBase Pass 中作为逐像素光处理。

💡 2. 非主光源的处理逻辑

光源类型默认 Importance处理方式
Point / Spot默认是 AutoUnity 根据性能自动决定逐像素或逐顶点
Not Important会进入 逐顶点光照,最多 4 个光,写入 unity_4LightPosX0 等变量,VERTEXLIGHT_ON 为真时可用
Important会作为 逐像素光照进入 ForwardAdd Pass
场景条件是否需要你设置为 Not Important
你想用 Shade4PointLights(...)✅ 是,必须设置为 Not Important(或让 Unity 自动评估为)
你用 ForwardAdd 来处理点光/聚光照明❌ 否,不需要设置 Not Important,只要是非主光源它就会进来

LIGHTING_COORDSTRANSFER_VERTEX_TO_FRAGMENT 是用于处理光照和阴影计算的关键工具。​它们定义在 Unity 的 AutoLight.cginc 文件中,主要用于在 ForwardAdd Pass 中处理逐像素光照和阴影。

LIGHTING_COORDSTRANSFER_VERTEX_TO_FRAGMENT 这两个宏主要用于 Unity 的 ForwardAdd Pass 中,处理逐像素光照和阴影衰减计算。​它们定义在 Unity 的 AutoLight.cginc 文件中,旨在简化自定义顶点/片元着色器中光照和阴影数据的传递和计算。

在 Unity 的内置渲染管线中,阴影信息通常存储在阴影贴图(Shadow Map)中。​为了在自定义着色器中访问这些阴影数据,Unity 提供了一些宏和函数,主要定义在 AutoLight.cginc 文件中。

在复杂光源的理解中,存在许多默认设定,需要大家进行简单区分。逐顶点光源、逐像素光源和球协光源(SH光源)是我们之前讲过的用的forward渲染的几种光源处理方式。逐顶点光源和SH光源在forward base里面进行处理,而逐像素光源中,最重要的平行光也在forward base处理。

当物体数量增加时,渲染次数会呈指数性增长。例如,当物体为三个时,若每个物体有三个光源,则需要渲染九次。可以想象,物体数量和光源数量越多,渲染次数将非常庞大。因此,在真实的Unity手机项目中,很少使用实时光源,可能仅使用一盏方向光,而避免使用其他实时光源。大部分情况下,会使用light map进行烘培,并通过贴图采样来大大减少渲染次数,降低性能消耗。

“这个 Shader 是基于前向渲染写的。”

👉 #pragma target 3.0, 4.0, etc. is used to specify the Shader Model version,
and different Shader Models allow a different number of interpolators between the vertex and fragment shaders.

Shader Model (target)Max InterpolatorsNotes
3.0 (#pragma target 3.0)Up to 10 interpolatorsOlder devices (PS3/Xbox360, old PCs)
4.0 (#pragma target 4.0)Up to 32 interpolatorsModern GPUs (DX10+, Metal, Vulkan)
OpenGL ES 3.0Up to 16 interpolatorsMobile devices (Android/iOS)
Very old: Direct3D 9.1Only 8 interpolatorsAncient hardware

#pragma multi_compile __ UNITY_HDR_ON
 

"Please compile two versions of this shader:"

  • One where UNITY_HDR_ON is NOT defined (regular LDR – low dynamic range)

  • One where UNITY_HDR_ON is defined (HDR – high dynamic range)

multi_compile = make multiple variants of the shader at build time.

DeferredOutput is a packaging struct for writing G-Buffer data in Deferred Rendering — each SV_TargetN maps to a real RenderTarget in the pipeline.

It makes your fragment shader return all necessary surface information at once, ready for Unity's deferred lighting system to consume.

  • #pragma multi_compile_lightpass: This directive ensures that Unity compiles multiple variants of your shader to handle different lighting scenarios in the deferred pass.​

  • #pragma exclude_renderers nomrt: This excludes platforms that do not support Multiple Render Targets (MRT), which are essential for deferred rendering.​

  • #pragma multi_compile __ UNITY_HDR_ON: This compiles shader variants based on whether High Dynamic Range (HDR) is enabled.

Memory bandwidth is the amount of information that can be transferred to and from memory per unit time. Put another way, memory bandwidth determines how often values can be accessed from memory.

Screen Space Ambient Occlusion (SSAO) is a real-time rendering technique used in 3D graphics to simulate the subtle shadows that occur in creases, corners, and areas where objects are close to each other. By approximating how ambient light is occluded or blocked in these areas, SSAO enhances the perception of depth and adds realism to scenes without the computational cost of full global illumination.

 Material Property Attributes(材质属性标签)

作用:

在 Shader 的 Properties 块里,每一个属性可以带一个方括号包起来的 Attribute,用于告诉 Unity 编辑器,怎么在 Inspector(材质面板)上处理这个属性。

比如要不要用 HDR 颜色选择器?要不要隐藏?是不是法线贴图?要不要禁止用户调 tiling/offset?

——就是这些小控制。

 Attribute | 作用

[Gamma] | 说明这个 float 或 vector 属性用的是 sRGB,需要做 gamma 处理。通常少用。

[HDR] | 说明这个 color 或 texture 是 HDR 格式的。颜色属性的话,编辑器会弹出HDR颜色选择器。

[HideInInspector] | 让这个属性在 Inspector 上完全隐藏(内部用,不让美术看见)。

[MainTexture] | 指定“主纹理”,可以让 material.mainTexture 正确找到它。(默认只有 _MainTex 会被认主纹理)

[MainColor] | 指定“主颜色”,可以让 material.color 正确指到它。(默认只有 _Color)

[NoScaleOffset] | 禁止材质面板显示 Tiling 和 Offset 控件(一般是贴图用,但你不想让人调)。

[Normal] | 指出这个纹理是法线贴图,如果错误地贴了普通图片,Unity会提示警告。

[PerRendererData] | 说明这个属性由 MaterialPropertyBlock 来动态设置,材质面板显示为灰色、不可编辑

阴影区域是由光源(Directional Light)照射到物体的 Mesh(确切说是它的 Mesh Renderer 和 Mesh Filter)决定的。

  • 并不是简单的物体背后就是阴影。

  • Unity 在计算实时阴影时,使用了一个叫做 Shadow Map(阴影贴图) 的技术流程。

  • 在渲染主画面之前,Directional Light 会额外进行一次专门的渲染过程:

    • 从光源的角度,以正交投影方式渲染整个场景。

    • 这个渲染结果不是颜色,而是记录每个像素到光源的深度(Depth),形成一个叫做 Shadow Map 的深度图。

  • 这个 Shadow Map 相当于记录了:“从光源看,哪儿被挡住了。”

当真正渲染主画面时

  • 每个像素都去 Shadow Map 查询自己。

  • 比对当前像素到光源的距离 vs Shadow Map 存储的深度值。

  • 如果当前像素比 Shadow Map 记录的更远,说明这个像素是在某个挡光物体后面,于是判断为在阴影中

“是不是给自己加一层 texture,改变整体?”

✅ 正确方向,但不是直接加一张 texture。
✅ Unity 实际上是在Lighting阶段,在着色器(Shader)里,根据是否在阴影中改变片元(pixel)的最终光照强度

更精确地说:

  • 阴影区域的像素,不是给它们贴一张特殊阴影图;

  • 而是:

    • 降低它们接收到的直接光照(Direct Light Intensity)

    • 环境光(Ambient)和自发光(Emission)保持正常

  • 最终看起来就是:亮度下降,显得暗淡,呈现“在阴影里”的感觉。

📌 额外小知识:

  • Unity 内置着色器里,一般通过 UNITY_LIGHT_ATTENUATION 这种宏,来在片元着色器阶段查询阴影信息。

  • 如果你自己写自定义 Shader(比如 Surface Shader 或手写光照模型),你也需要调用 SHADOW_ATTENUATION 来判断是否在阴影中。

什么是 Surface Shader?

Surface Shader 是 Unity 提供的一种简化版 Shader 编写方式,专门用于做标准光照计算的。

简单来说,它是:

一种特别给“光照模型”设计的高级封装写法,让你只需要描述“表面属性”,Unity会帮你自动生成后面复杂的 Vertex/Fragment Shader,包括各种 Forward/Deferred 渲染适配。

它的特点总结

  • 你只需要专心写“材质表面怎么反应光”的描述,比如颜色、法线、粗糙度、金属度;

  • 不需要自己写完整的顶点着色器(Vertex Shader)和片元着色器(Fragment Shader);

  • Unity会根据你的描述,自动生成支持 Forward、Deferred、阴影、GI、Lightmap 等各种系统的完整底层 Shader 代码。

✅ 一个Surface Shader示例

Shader "Custom/SimpleSurface"
{Properties{_Color ("Color", Color) = (1,1,1,1)}SubShader{Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM#pragma surface surf Standard fullforwardshadowsfixed4 _Color;struct Input{float2 uv_MainTex;};void surf (Input IN, inout SurfaceOutputStandard o){o.Albedo = _Color.rgb;o.Metallic = 0.0;o.Smoothness = 0.5;o.Alpha = 1.0;}ENDCG}
}

你只需要告诉 Unity:

  • Albedo (表面颜色)

  • Metallic(是否金属)

  • Smoothness(粗糙程度)

  • Alpha(透明度)

剩下的:

  • 如何兼容 Forward/Deferred?

  • 如何做阴影采样?

  • 如何适配 Lightmap?

  • 如何支持多光源?

内容说明
Surface Shader 本质上是一种高阶封装,生成底层 Pass(ForwardBase, ForwardAdd, Deferred)
Surface Shader 适配的渲染管线Built-in Render Pipeline(内置管线)
Surface Shader 适配 SRP(URP/HDRP)吗?❌ 不适配,SRP 要自己写 Shader Graph 或手写 HLSL
Surface Shader 自动处理什么?兼容 Forward 渲染、Deferred 渲染、阴影、光照探针、GI、Lightmap
  • 传统 Unity Built-in Pipeline里,Surface Shader是极好用、官方推荐的方法;

  • 在**新 SRP(URP、HDRP)**里,Surface Shader 体系就被淘汰了,要自己用 Shader Graph 或直接手写HLSL。

✔ ShaderLab
    └── defining a Shader object
    └── defining a SubShader
    └── defining a Pass
    └── adding shader programs
    └── commands(用于配置状态)

✔ HLSL in Unity
    └── 核心语言:变量声明、矩阵变换、光照计算、纹理采样等

ShaderLab = 组织结构(SubShader、Pass、Program)
HLSL      = 执行逻辑(变量、变换、光照、纹理)

Unity3D面试之渲染系列:说说ZTest与ZWrite的核心原理_哔哩哔哩_bilibili

天空盒(Skybox) ≈ z-buffer 中的“无穷远”值

🌌 从渲染原理上讲:

  • 天空盒是背景元素永远在所有物体后面

  • 它通常用一张立方体贴图(cubemap)或者6张面图来模拟全景天空。

  • 天空盒不受相机位置影响(看起来始终固定),但受相机朝向影响。

  1. 天空盒通常设置为不写入 Z-buffer(ZWrite Off)

  2. 并且 ZTest 设为 Always,这样无论当前像素的 z 值是多少,天空盒像素都会被“通过”(但由于它不写 Z,所以不会覆盖其他物体的 Z 值)

  3. 渲染顺序:在所有不透明物体之后、透明物体之前,或者最先渲染(具体取决于渲染管线)

🔁 因此可以这么理解:

天空盒并不是真的在 Z-buffer 里写了“无穷远”,
而是通过跳过 Z 写入和特定的 ZTest 设置,让它“逻辑上”处于所有东西之后。

从视觉和深度逻辑上,它就像是 Z = ∞ 的背景层

一旦 Z-buffer(深度测试)没有通过,fragment shader(即你的 frag() 函数)就不会被执行。而 vertex shader 一定会执行。

  • Vertex shader 是图形管线中第一个 programmable stage。

  • 它负责将每个顶点变换到裁剪空间,并生成插值信息(如 UV、法线、颜色、fog 坐标等)。

  • 不管顶点最终会不会出现在屏幕上,每个顶点都要经过 vertex shader 执行,否则连三角形都无法构建。

  • ZTest(深度测试)在 rasterization 之后、fragment shader 之前执行

  • 如果某个片元(fragment)被判定为被前面的像素挡住(比如 z 值更大),那么:

    • 不会进入 fragment shader

    • 也不会进行颜色混合或写入 framebuffer

    • 更不会触发你图中写的 frag() 函数

URP、HDRP、Surface Shader 其实都是对 Vertex 和 Fragment Shader 的封装或组织方式
而 Vertex Shader 和 Fragment Shader 就是 Unity 渲染系统中最底层、最基础的计算单元(可编程的部分)

【GPU 硬件级别】
   ▲
   │
【底层】HLSL代码 → Vertex Shader / Fragment Shader(最底层计算单元)
   ▲
   │
【ShaderLab】Shader 对象的结构组织语言(封装 shader 程序 + 配置状态)
   ▲
   │
【封装框架】
   ├─ Surface Shader(内置管线时代的简化写法,自动生成 V/F shader)
   ├─ URP Shader(封装为 Shader Graph 或 HLSL 文件,基于 SRP 架构)
   └─ HDRP Shader(高复杂度 SRP 框架,重封装光照和材质处理)
   ▲
   │
【图形界面】
   ├─ Shader Graph(封装成节点图,编译成 V/F shader)
   └─ Visual Effects Graph(粒子、体积雾、复杂特效)

Unity 默认:

类型Queue 值名称
不透明物体2000Geometry(默认)
透明物体3000Transparent

这保证了 不透明物体先渲染,填充好深度缓冲区(Z-buffer),然后透明物体根据 ZTest 来决定哪些片元可见。

除非你主动设置:

ZTest Always
ZWrite Off

这时候,你才可以“无视深度”地盖在别人头上。

渲染队列决定顺序,ZTest 决定能不能“抢上色”

即使你排得再后,ZTest 不允许,你也画不上。

开了 ztest 就是参与 反画家算法 去参加“谁在后面谁没资格填色”活动了,

启用 ZTest,就是将当前片元交给 GPU 的 Z-buffer 系统,参与深度竞争(Depth Competition),决定是否被“剔除”掉,或者有权写入屏幕。

RenderQueue 先于一切,它只是决定“谁先被提交到渲染管线”。
之后每个物体的片元进入渲染时,才执行 ZTest
所以:RenderQueue → ZTest → Fragment Shader(如果没被剔除)

  • RenderQueue → 只在 CPU 阶段,控制对象顺序

  • ZTest → GPU 上,每个像素决定能不能继续执行

  • ZWrite → 决定这个片元会不会影响后续的 ZTest

  • Fragment Shader → 只有在 ZTest 通过后才会运行

RenderPipeline[Stage["CPU阶段",{RenderQueueSorting[Description -> "基于 Shader 的 Queue 值排序:决定对象提交顺序",Example -> {"Opaque -> Queue = 2000","Transparent -> Queue = 3000"}],ObjectSubmission[Description -> "提交一个个物体到 GPU 渲染管线,按照队列顺序"]}],Stage["GPU阶段",{VertexShader[Description -> "每个顶点执行变换、传递数据(位置/UV/法线)"],PrimitiveAssembly[Description -> "组成三角形"],Rasterization[Description -> "三角形 → 屏幕像素片元"],FragmentLevelProcessing[Description -> {ZTest[Description -> "判断该片元的 Z 值是否比当前 Z-buffer 小",Result -> {"Pass -> 进入 Fragment Shader","Fail -> 被丢弃,fragment shader 不执行"}],FragmentShader[Description -> "执行光照、纹理、颜色混合等计算"],Blend[Description -> "根据混合模式决定如何和已有颜色合成"],ZWrite[Description -> "如启用,则将当前片元的 Z 值写入 Z-buffer"]}]}]
]

"判断该片元的 Z 值是否比当前 Z-buffer 小" 这一步,是按 RenderQueue 的顺序进入的吗?

答案是:

✔️ 是的:每个片元被送入 GPU 执行这段 ZTest 逻辑,必须先等到它所属的物体按 RenderQueue 顺序被提交进去。

送入GPU去被处理的顺序就是render queue的顺序

更精确地说:

  • RenderQueue 决定物体的绘制提交顺序

  • Unity 在 CPU 阶段会将所有物体按 RenderQueue + SortingLayer + material priority 进行排序。

  • 然后这些物体一个个地被送入 GPU:

    • 它们的顶点先执行 vertex shader

    • 然后形成三角形

    • 然后光栅化为片元

    • 再进行 ZTest → fragment shader → blend

绿色在蓝色的前面,但是绿色的zwrite为off,所以尽管在像素判断为绿色后,zbuffer的值还是无穷远,没有被绿色改变

到下一次遍历给蓝色的时候,当前面对的的zbuffer里的值--skybox无穷远,所以蓝色拥有覆盖       此时先手被填充到colorbuffer的绿色-colorbuffer    的权利(绿色和蓝色都开启了ztest参与剔除判断了,zwrite要不要做,对于蓝和绿来说都是可以自行决定的)

ps-----蓝和绿的传递给GPU的先后顺序由render queue决定,这里干涉的是    谁先可以抢先手去“填参加ztest深度比赛的表”  (重点是每个像素填完表之后就----参与进比赛,此时后续也有将要参与的像素,但他们并不参与当前像素的比赛,,比赛结果产生新的colorbuffer和zbuffer)

所以render queue,,影响的是报名的先后顺序,,这个报名的先后顺序绝对不是无所谓的,,

相关文章:

  • n8n工作流自动化平台的实操:利用本地嵌入模型,完成文件内容的向量化及入库
  • 从 0 到 1:使用 Jetpack Compose 和智能自动化实现高效 Android UI 开发
  • 2025 年如何使用 Pycharm、Vscode 进行树莓派 Respberry Pi Pico 编程开发详细教程(更新中)
  • HTML学习笔记(7)
  • PHP的include和require
  • 基于STM32的心电图监测系统设计
  • 【前端】【面试】在 Vue-React 的迁移重构工作中,从状态管理角度来看,Vuex 迁移到 Redux 最大的挑战是什么,你是怎么应对的?
  • 力扣面试150题--相同的树
  • 嵌入式按键原理、中断过程与中断程序设计(键盘扫描程序)
  • 【CISCO】什么是静态路由(Static Route)?ip route 192.0.1.0 255.255.255.0 200.0.0.1
  • 高等数学同步测试卷 同济7版 试卷部分 上 做题记录 第四章 不定积分同步测试卷 B卷
  • LeetCode刷题链表
  • Spring AI 实战:第四章、Spring AI多模态之看图说话
  • Go语言实现Kafka消息队列
  • 【图书管理系统】环境介绍、设计数据库和表、配置文件、引入依赖
  • JVM——JVM是怎么实现invokedynamic的?
  • Go语言--语法基础4--基本数据类型--类型转换
  • 4个纯CSS自定义的简单而优雅的滚动条样式
  • 图片压缩与尺寸调整的便捷工具推荐
  • Qt输入控件(QInput Widgets)详解:从基础到实战
  • 热点问答|澳大利亚联邦选举结果有何看点
  • 五一假期前三日多景区客流刷新纪录,演艺、古镇、山水都很火
  • 月薪3万文科友好,“AI训练师”真有那么赚?
  • 哈马斯:愿与以色列达成为期5年的停火协议
  • 用小型核反应堆给数据中心供电,国内企业正在开展项目论证
  • 多地景区发公告称售票达接待峰值,有景区暂停网络和线下售票