<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Axon&apos;s Blog</title><description>Axonの碎碎叨叨空间</description><link>https://fuwari.vercel.app/</link><language>en</language><item><title>为Snapdragon Profiler添加Vulkan支持</title><link>https://fuwari.vercel.app/posts/%E4%B8%BAsnapdragon-profiler%E6%B7%BB%E5%8A%A0vulkan%E6%94%AF%E6%8C%81/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E4%B8%BAsnapdragon-profiler%E6%B7%BB%E5%8A%A0vulkan%E6%94%AF%E6%8C%81/</guid><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/posts/image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要Vulkan SDK. 如果不安装的话, Vulkan API下, 是看不到Shader Analysis的, 甚至看不到任何的图形操作
https://docs.qualcomm.com/doc/80-71528-1/topic/Alldocs.html?product=1601111740010426&amp;amp;query=Vulkan#List 这里列出来了, 需要手动安装.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;https://www.vulkan.org/tools#download-these-essential-development-tools
然后在这里下载即可, 默认安装到C:\VulkanSDK\1.4.341.1&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_02.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;放这里这个目录本身，不要再加 bin 子目录。
也就是截图里的这种填法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\VulkanSDK\1.4.341.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\VulkanSDK\1.4.341.1\Bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是这个字段叫 Vulkan SDK path，通常要的是 SDK 根目录。LunarG 官方文档也说明：安装 Vulkan SDK 后，系统环境变量 VULKAN_SDK 指向的是安装目录根路径，而 %VULKAN_SDK%\Bin 只是被额外加入到 PATH 里，用来找可执行文件。(vulkan.lunarg.com)
另外，LunarG 的目录结构里也明确把 Bin 说明为“64 位可执行文件和相关清单所在目录”，说明它是 SDK 根目录下面的一个子目录，不是 SDK 路径本身。(vulkan.lunarg.com)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_03.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果填了根目录还是不行，再检查这几个点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo %VULKAN_SDK%vulkaninfoSDK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 vulkaninfoSDK 能跑，通常说明 PC 侧 Vulkan SDK 安装是正常的。LunarG 也把 vulkaninfoSDK 列为验证安装的方法之一。(vulkan.lunarg.com)
Learn more:&lt;/p&gt;
</content:encoded></item><item><title>Unity的shader_feature_local_fragment操作</title><link>https://fuwari.vercel.app/posts/unity%E7%9A%84shader_feature_local_fragment%E6%93%8D%E4%BD%9C/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity%E7%9A%84shader_feature_local_fragment%E6%93%8D%E4%BD%9C/</guid><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;shader_feature_local_fragment 是 Unity Shader 中的一个 pragma 指令，它是 shader_feature_local 的更精细化版本。要理解它，可以把它拆解为三个部分：&lt;/p&gt;
&lt;h2&gt;拆解理解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部分&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;shader_feature&lt;/td&gt;
&lt;td&gt;声明一个 shader 关键字变体（keyword variant），未被任何材质引用的变体会在打包时被剔除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_local&lt;/td&gt;
&lt;td&gt;关键字作用域为局部（local），仅限当前 shader，不占用全局关键字的有限名额&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;_fragment&lt;/td&gt;
&lt;td&gt;该关键字变体仅作用于片元着色器（Fragment Shader）阶段，不会影响顶点着色器（Vertex Shader）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;它和 shader_feature_local 的区别&lt;/h2&gt;
&lt;p&gt;shader_feature_local 会同时为 顶点阶段和片元阶段 都生成对应的变体，而 shader_feature_local_fragment只在片元阶段生成变体。
这意味着：如果你的某个关键字（比如控制某种光照效果、颜色混合等）只在 frag 函数中使用，而在 vert 函数中完全没用到，那就可以用 _fragment 后缀来避免为顶点阶段编译不必要的变体，减少编译的 shader 变体数量，从而优化编译时间和内存占用。&lt;/p&gt;
&lt;h2&gt;用法示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#pragma shader_feature_local_fragment _EFFECT_ON _EFFECT_OFF// 顶点着色器 —— 不会因为上述关键字产生额外变体v2f vert(appdata v){    // ...}// 片元着色器 —— 关键字仅在此生效fixed4 frag(v2f i) : SV_Target{    fixed4 col = tex2D(_MainTex, i.uv);    #if defined(_EFFECT_ON)        col *= _TintColor; // 仅在开启时执行    #endif    return col;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类似的后缀变体&lt;/h2&gt;
&lt;p&gt;Unity 提供了一整套类似的阶段限定后缀：
总结：shader_feature_local_fragment = 局部关键字 + 仅片元阶段生效，是一种更精细的变体控制手段，适合在关键字只影响片元着色器逻辑时使用，以减少不必要的变体编译开销。&lt;/p&gt;
</content:encoded></item><item><title>有关Frag的VFACE</title><link>https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3frag%E7%9A%84vface/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3frag%E7%9A%84vface/</guid><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是 HLSL/ShaderLab（Unity HDRP/URP 里常见） 的片元着色器（fragment shader）函数签名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half4 OceanMainFrag(Varyings input, float facing : VFACE) : SV_Target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐段解释：&lt;/p&gt;
&lt;h2&gt;1) half4 是返回值&lt;/h2&gt;
&lt;h2&gt;2) OceanMainFrag 是函数名&lt;/h2&gt;
&lt;p&gt;随便起的名字，这里是海面（Ocean）主片元函数。&lt;/p&gt;
&lt;h2&gt;3) Varyings input&lt;/h2&gt;
&lt;h2&gt;4) float facing : VFACE 是关键：VFACE 语义&lt;/h2&gt;
&lt;h3&gt;VFACE 做什么？&lt;/h3&gt;
&lt;p&gt;VFACE 是一种系统语义，用于告诉你当前像素属于三角形的正面还是背面（front face / back face）。
Unity 里常用写法（兼容一些平台差异）是把它当成“是否正面”的标记来用。&lt;/p&gt;
&lt;h3&gt;常见用途&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;float isFrontFace = (facing &amp;gt; 0.0);float3 N = normalize(input.normalWS);N = isFrontFace ? N : -N;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5) : SV_Target 是输出语义&lt;/h2&gt;
&lt;p&gt;表示这个返回值写到渲染目标（Render Target）的颜色缓冲里（即最终像素颜色）。
这不是“某个函数内部的操作”，而是函数参数里声明了一个由 GPU 自动提供的变量：facing : VFACE。
你可以把它理解为：片元着色器额外接收了一个“这个像素当前是正面还是背面”的标记。&lt;/p&gt;
&lt;h2&gt;可能的坑/前提&lt;/h2&gt;
&lt;h1&gt;为什么几乎没啥shader用这个vface?&lt;/h1&gt;
&lt;p&gt;因为 VFACE（或 Unity 里的 SV_IsFrontFace）只在少数需要“正反面区分”的场景里才真正有价值，而大多数材质默认是背面剔除（Cull Back），导致你“看不到它出场”。
下面是常见原因：&lt;/p&gt;
&lt;h2&gt;1) 默认背面剔除，背面根本进不了 Fragment&lt;/h2&gt;
&lt;p&gt;多数 Shader/材质是 Cull Back（剔除背面）。这样：
只有在 Cull Off（双面渲染）或 Cull Front 时，区分正反面才重要。&lt;/p&gt;
&lt;h2&gt;2) 很多效果不需要“双面不同处理”&lt;/h2&gt;
&lt;p&gt;常见不透明物体（墙、角色、道具）：
真正需要的通常是：纸片/树叶/布料/薄壳、海面、玻璃薄片、毛发卡片、体积壳等。&lt;/p&gt;
&lt;h2&gt;3) 引擎/管线已经把“两面光照”封装掉了&lt;/h2&gt;
&lt;p&gt;在 Unity HDRP/URP 里，经常有：
所以你“很少看到”，其实是因为它被藏在 include（ShaderLibrary）里了。&lt;/p&gt;
&lt;h2&gt;4) 跨平台/语义差异让人更倾向用更标准的写法&lt;/h2&gt;
&lt;p&gt;历史上：
在 Unity 里你会更常见到类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;half4 frag(Varyings i, bool isFrontFace : SV_IsFrontFace) : SV_Target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不是 float facing : VFACE。
（很多人甚至把它用宏包起来，进一步让你“看不见”。）&lt;/p&gt;
&lt;h2&gt;5) 性能/分支与一致性考虑&lt;/h2&gt;
&lt;p&gt;做双面时你通常会写：&lt;/p&gt;
&lt;h2&gt;6) 还有一条：法线翻转不一定要靠 VFACE “显式写”&lt;/h2&gt;
&lt;p&gt;有些情况下可以通过其它方式规避：&lt;/p&gt;
&lt;h3&gt;什么时候你反而会经常看到它？&lt;/h3&gt;
&lt;p&gt;如果你愿意，把你所在管线（URP/HDRP/内置）以及那段 Ocean shader 的 Pass 里 Cull/TwoSided 设置贴出来，我可以告诉你：这份代码用 VFACE 大概率是在做“背面法线翻转”还是“正反面两套着色”。&lt;/p&gt;
</content:encoded></item><item><title>有关结构体初始化的 (Varyings)0</title><link>https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3%E7%BB%93%E6%9E%84%E4%BD%93%E5%88%9D%E5%A7%8B%E5%8C%96%E7%9A%84-varyings0/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3%E7%BB%93%E6%9E%84%E4%BD%93%E5%88%9D%E5%A7%8B%E5%8C%96%E7%9A%84-varyings0/</guid><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是 HLSL/Shader 中的结构体初始化语法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Varyings Vert(Attributes input){    Varyings output = (Varyings)0;    UNITY_SETUP_INSTANCE_ID(input);    UNITY_TRANSFER_INSTANCE_ID(input, output);    VertexPositionInputs vp = GetVertexPositionInputs(input.positionOS.xyz);    VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS, input.tangentOS);    vp.positionWS = TransformObjectToWorld(input.positionOS.xyz);    vp.positionCS = TransformWorldToHClip(vp.positionWS);    real sign = input.tangentOS.w * GetOddNegativeScale();    output.positionCS = vp.positionCS;    output.uv = TRANSFORM_TEX(input.uv, _BaseMap);    output.positionOS = input.positionOS.xyz;    output.positionWS = vp.positionWS;    output.normalWS = vn.normalWS;    output.tangentWS = half4(vn.tangentWS, sign);    return output;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Varyings)0 的含义是：将 0 强制转换为 Varyings 结构体类型，这会将结构体的所有成员初始化为
0（或等效的默认值，如 float4 会是 (0,0,0,0)）。
为什么这样写？
// 这两种写法效果类似，但 (Varyings)0 更简洁
Varyings output = (Varyings)0;
// 等价于手动初始化每个成员为 0
Varyings output;
output.positionCS = 0;
output.uv = 0;
output.color = 0;
// ... 其他成员
常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保结构体所有字段都有确定的初始值，避免未初始化的垃圾数据&lt;/li&gt;
&lt;li&gt;防止某些字段忘记赋值导致渲染异常&lt;/li&gt;
&lt;li&gt;是 shader 编程中的惯用写法，简洁高效
这在 Unity 的 URP/HDRP shader 中非常常见，特别是顶点着色器返回 Varyings 结构体时。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>为Unity的后端使用OpenGL Debug</title><link>https://fuwari.vercel.app/posts/%E4%B8%BAunity%E7%9A%84%E5%90%8E%E7%AB%AF%E4%BD%BF%E7%94%A8opengl-debug/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E4%B8%BAunity%E7%9A%84%E5%90%8E%E7%AB%AF%E4%BD%BF%E7%94%A8opengl-debug/</guid><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;默认情况下, Unity使用的后端是DirectX (D3D11&amp;amp;12). 但是有时候, 有需要使用切换整个后端api来测试renderfeature和compute shader的可用性.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这里即可直接调换渲染api. 整个渲染管线都会被Opengl改变.&lt;/p&gt;
</content:encoded></item><item><title>用RenderDoc解析的csv文件手写还原gltf</title><link>https://fuwari.vercel.app/posts/%E7%94%A8renderdoc%E8%A7%A3%E6%9E%90%E7%9A%84csv%E6%96%87%E4%BB%B6%E6%89%8B%E5%86%99%E8%BF%98%E5%8E%9Fgltf/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E7%94%A8renderdoc%E8%A7%A3%E6%9E%90%E7%9A%84csv%E6%96%87%E4%BB%B6%E6%89%8B%E5%86%99%E8%BF%98%E5%8E%9Fgltf/</guid><description>怎么利用renderdoc还原出模型原来的结构</description><pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;写这篇的时候是因为我的一个项目此前一直是直接解析csv的数据写成renderdoc的json的, 但是未来要加入更多的材质参数和解析gltf的node节点. 要换成pygltf库, 但是之前手写的又不想放弃, 遂放进一个blog中作为知识点记录了(&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;简单介绍&lt;/h2&gt;
&lt;p&gt;什么是glTF？&lt;br /&gt;
glTF（GL Transmission Format）是 Khronos 集团开发的一种 3D 场景和模型传输格式(没错就是那个OpenGL和Vulkan的)。它也称为“JPEG in 3D”，专为高效的 3D 内容交付而设计。&lt;br /&gt;
1.0 版本于 2015 年发布，glTF 2.0 现已被广泛采用为标准。&lt;br /&gt;
与fbx和obj工作流相比, 具有紧凑的文件大小; 针对网络传输进行了优化; 发送到 GPU 的处理最少. 同时汲取了fbx工作流的优势, 同样可以进行外部引用/内嵌. 使用灵活的机制来添加新功能.&lt;/p&gt;
&lt;p&gt;gltf主要有两种格式:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;格式&lt;/th&gt;
&lt;th&gt;扩展名&lt;/th&gt;
&lt;th&gt;特点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;glTF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.gltf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON 文本 + 外部二进制/图片文件&lt;/td&gt;
&lt;td&gt;调试、手动编辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.glb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;单一二进制文件，包含所有数据&lt;/td&gt;
&lt;td&gt;网络传输, 生产交付环境&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在这里只用一个最简单的组装模型来说, 贴图啥的就先不搞了(&lt;/p&gt;
&lt;h2&gt;数据结构&lt;/h2&gt;
&lt;p&gt;glTF（GL Transmission Format）本质上就两样东西：一个 &lt;strong&gt;JSON 文件&lt;/strong&gt;描述场景的结构和引用关系，加上一块或多块二进制数据（又称为Buffer）存储实际的顶点、索引等数值。&lt;/p&gt;
&lt;p&gt;所有的设计都围绕一个核心思想：&lt;strong&gt;JSON 描述&quot;是什么&quot;，二进制存储&quot;具体数据&quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;所有的gltf的数据结构可以简单看成这样的数据流:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Buffer → BufferView → Accessor → Mesh Primitive
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第一层：Buffer&lt;/h3&gt;
&lt;p&gt;Buffer 是最底层的原始二进制数据块，它本身没有任何语义，就是一堆字节, 存放了所有的数据. 它可以是一个外部 &lt;code&gt;.bin&lt;/code&gt; 文件(glTF)，也可以是完全内嵌的 base64 字符串(glB).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;buffers&quot;: [
    {
      &quot;uri&quot;: &quot;data.bin&quot;,
      &quot;byteLength&quot;: 21120
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以把它想象成一张白纸，上面写满了数字，但没人会告诉你哪些数字是位置、哪些是法线。&lt;/p&gt;
&lt;h3&gt;第二层：BufferView&lt;/h3&gt;
&lt;p&gt;BufferView 会在 Buffer 的基础上上划出一个区域，说&quot;从第 X 字节开始，取 Y 个字节&quot;。它还可以指定 &lt;code&gt;byteStride&lt;/code&gt;（交错布局时每个顶点之间的间距）和 &lt;code&gt;target&lt;/code&gt;（表示这块数据是顶点属性还是索引）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;bufferViews&quot;: [
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 0,
      &quot;byteLength&quot;: 7920,
      &quot;byteStride&quot;: 24,
      &quot;target&quot;: 34962
    },
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 7920,
      &quot;byteLength&quot;: 1320,
      &quot;target&quot;: 34963
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;target: 34962&lt;/code&gt; 是 &lt;code&gt;ARRAY_BUFFER&lt;/code&gt;（顶点数据），&lt;code&gt;34963&lt;/code&gt; 是 &lt;code&gt;ELEMENT_ARRAY_BUFFER&lt;/code&gt;（索引数据）。这些常量直接来自 OpenGL/WebGL 的枚举值。&lt;/p&gt;
&lt;h3&gt;第三层：Accessor&lt;/h3&gt;
&lt;p&gt;Accessor 是最重要的一层。它在 BufferView 的基础上赋予了&lt;strong&gt;类型语义&lt;/strong&gt;——告诉你这些字节应该被解读为什么类型的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;accessors&quot;: [
    {
      &quot;bufferView&quot;: 0,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5126,
      &quot;count&quot;: 330,
      &quot;type&quot;: &quot;VEC3&quot;,
      &quot;max&quot;: [13.53504, 10.8303, 12.28831],
      &quot;min&quot;: [-13.84957, 0.03842, -11.62553]
    },
    {
      &quot;bufferView&quot;: 0,
      &quot;byteOffset&quot;: 12,
      &quot;componentType&quot;: 5126,
      &quot;count&quot;: 330,
      &quot;type&quot;: &quot;VEC3&quot;
    },
    {
      &quot;bufferView&quot;: 1,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5123,
      &quot;count&quot;: 330,
      &quot;type&quot;: &quot;SCALAR&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;componentType&lt;/code&gt; 表示每个分量的数据类型，后面跟的数字是数据类型. &lt;code&gt;5126&lt;/code&gt; 是 &lt;code&gt;FLOAT&lt;/code&gt;（4字节），&lt;code&gt;5123&lt;/code&gt; 是 &lt;code&gt;UNSIGNED_SHORT&lt;/code&gt;（2字节），&lt;code&gt;5121&lt;/code&gt; 是 &lt;code&gt;UNSIGNED_BYTE&lt;/code&gt;。这些也是 OpenGL 的常量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; 表示每个元素的结构，&lt;code&gt;&quot;SCALAR&quot;&lt;/code&gt; 就是一个数，&lt;code&gt;&quot;VEC2&quot;&lt;/code&gt; 是两个分量，&lt;code&gt;&quot;VEC3&quot;&lt;/code&gt; 三个，&lt;code&gt;&quot;VEC4&quot;&lt;/code&gt; 四个，还有 &lt;code&gt;&quot;MAT4&quot;&lt;/code&gt; 等矩阵类型。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;count&lt;/code&gt; 是元素的个数，比如 330 个顶点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;max&lt;/code&gt; 和 &lt;code&gt;min&lt;/code&gt; 是每个分量的最大最小值，&lt;strong&gt;POSITION 的 accessor 必须提供，其他属性可选&lt;/strong&gt;。这让加载器不读取实际数据就能计算包围盒。&lt;/p&gt;
&lt;h3&gt;第四层：Mesh Primitive&lt;/h3&gt;
&lt;p&gt;现在把 accessor 组装成一个可渲染的几何体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;meshes&quot;: [
    {
      &quot;name&quot;: &quot;MyMesh&quot;,
      &quot;primitives&quot;: [
        {
          &quot;attributes&quot;: {
            &quot;POSITION&quot;: 0,
            &quot;NORMAL&quot;: 1,
            &quot;TANGENT&quot;: 2,
            &quot;TEXCOORD_0&quot;: 3,
            &quot;TEXCOORD_1&quot;: 4
          },
          &quot;indices&quot;: 5,
          &quot;material&quot;: 0,
          &quot;mode&quot;: 4
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;attributes&lt;/code&gt; 里的数值是 accessor 的索引号。&lt;code&gt;mode: 4&lt;/code&gt; 表示三角形列表（&lt;code&gt;TRIANGLES&lt;/code&gt;），这也是默认值。&lt;/p&gt;
&lt;p&gt;最后我们就可以组装为一个最基础的物体了!&lt;/p&gt;
&lt;h3&gt;最后进行合并&lt;/h3&gt;
&lt;p&gt;在这里我们默认是一个node(有关于更多的数据可以看下面的扩展, 这里用一个最基本的组装来展示, 没有贴图, 材质指定等其他数据流, 只有基本的顶点组装)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{

  
  &quot;asset&quot;: { &quot;version&quot;: &quot;2.0&quot; },  // glTF 规范版本
  &quot;scene&quot;: 0,  // 默认场景索引
  &quot;scenes&quot;: [{ &quot;nodes&quot;: [0] }],  // 场景数组，nodes 指向根节点索引
  &quot;nodes&quot;: [{ &quot;mesh&quot;: 0 }],  // 节点数组，mesh 引用网格索引

  
  &quot;meshes&quot;: [{
    &quot;primitives&quot;: [{
      &quot;attributes&quot;: { &quot;POSITION&quot;: 0 },  // 顶点位置 → accessors[0]
      &quot;indices&quot;: 1,  // 索引缓冲区 → accessors[1]
      &quot;mode&quot;: 4  // 绘制模式：4=TRIANGLES
    }]
  }],

  
  &quot;accessors&quot;: [
    {
      &quot;bufferView&quot;: 0,  // → bufferViews[0]（顶点数据）
      &quot;componentType&quot;: 5126,  // 5126=FLOAT.
      &quot;count&quot;: 4,  // 4个顶点
      &quot;type&quot;: &quot;VEC3&quot;,  // 每顶点3个分量(x,y,z)
      &quot;min&quot;: [0.0, 0.0, 0.0],  // 坐标最小值
      &quot;max&quot;: [1.0, 1.0, 0.0]   // 坐标最大值
    },
    {
      &quot;bufferView&quot;: 1,  // → bufferViews[1]（索引数据）
      &quot;componentType&quot;: 5123,  // 5123=UNSIGNED_SHORT
      &quot;count&quot;: 6,  // 6个索引（2个三角形）
      &quot;type&quot;: &quot;SCALAR&quot;  // 标量
    }
  ],

  
  &quot;bufferViews&quot;: [
    {
      &quot;buffer&quot;: 0,  // → buffers[0]
      &quot;byteOffset&quot;: 0,  // 从第0字节开始
      &quot;byteLength&quot;: 48,  // 4顶点×3分量×4字节=48字节
      &quot;target&quot;: 34962  // 34962=ARRAY_BUFFER（顶点属性）
    },
    {
      &quot;buffer&quot;: 0,  // → buffers[0]
      &quot;byteOffset&quot;: 48,  // 从第48字节开始（紧接顶点数据后）
      &quot;byteLength&quot;: 12,  // 6索引×2字节=12字节
      &quot;target&quot;: 34963  // 34963=ELEMENT_ARRAY_BUFFER（索引）
    }
  ],

  
  &quot;buffers&quot;: [{
    &quot;byteLength&quot;: 60,  // 总长度：48+12=60字节
    &quot;uri&quot;: &quot;data:application/octet-stream;base64,...&quot;  // base64编码的二进制数据
  }]

  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;扩展:  Node 和 Scene, 材质系统&lt;/h3&gt;
&lt;h4&gt;场景图：Node 和 Scene&lt;/h4&gt;
&lt;p&gt;几何体要放到场景里才有意义，glTF 用 Node 树来组织.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;nodes&quot;: [
    {
      &quot;name&quot;: &quot;RootNode&quot;,
      &quot;mesh&quot;: 0,
      &quot;translation&quot;: [0, 0, 0],
      &quot;rotation&quot;: [0, 0, 0, 1],
      &quot;scale&quot;: [1, 1, 1],
      &quot;children&quot;: [1, 2]
    }
  ],
  &quot;scenes&quot;: [
    {
      &quot;name&quot;: &quot;Scene&quot;,
      &quot;nodes&quot;: [0]
    }
  ],
  &quot;scene&quot;: 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node 可以有 &lt;code&gt;translation&lt;/code&gt;、&lt;code&gt;rotation&lt;/code&gt;（四元数）、&lt;code&gt;scale&lt;/code&gt;，也可以直接给一个 &lt;code&gt;matrix&lt;/code&gt;（4x4列主序矩阵），但两者不能同时存在。Node 通过 &lt;code&gt;children&lt;/code&gt; 形成树状层级，&lt;strong&gt;变换逐级累乘, 要特别注意母级的变换影响.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;材质系统&lt;/h4&gt;
&lt;p&gt;glTF 使用 &lt;strong&gt;PBR 金属-粗糙度&lt;/strong&gt;工作流.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;materials&quot;: [
    {
      &quot;name&quot;: &quot;MyMaterial&quot;,
      &quot;pbrMetallicRoughness&quot;: {
        &quot;baseColorTexture&quot;: {
          &quot;index&quot;: 0,
          &quot;texCoord&quot;: 0
        },
        &quot;metallicFactor&quot;: 0.0,
        &quot;roughnessFactor&quot;: 0.8
      },
      &quot;normalTexture&quot;: {
        &quot;index&quot;: 1,
        &quot;texCoord&quot;: 0,
        &quot;scale&quot;: 1.0
      },
      &quot;emissiveFactor&quot;: [0, 0, 0]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;texCoord&lt;/code&gt; 字段可以指定用 &lt;code&gt;TEXCOORD_0&lt;/code&gt; 还是 &lt;code&gt;TEXCOORD_1&lt;/code&gt;。模型同样可以写入多套uv. 如果你的 lightmap 贴图要用第二套UV，就可以写 &lt;code&gt;&quot;texCoord&quot;: 1&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里还有一个比较有意思的点, glTF 规范要求实现至少支持两套UV（TEXCOORD_0 和 TEXCOORD_1），但 mesh 本身可以包含更多套。&lt;a href=&quot;https://github.com/atteneder/glTFast/issues/206&quot;&gt;[1]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一般来说, 第一套UV就是给正常贴图用的, 第二套UV则是专门给LightMap做的.&lt;/p&gt;
&lt;p&gt;不过实际兼容性要注意几点：&lt;/p&gt;
&lt;p&gt;早期的 Three.js 引擎不允许为纹理使用任意UV坐标集，而是将 channel 0 硬编码给所有贴图，channel 1 给 lightMap。&lt;a href=&quot;https://github.com/mrdoob/three.js/issues/12608&quot;&gt;[7]&lt;/a&gt;虽然后来有改善，但这说明不是所有查看器和引擎都能完美处理 3 套以上的UV。&lt;/p&gt;
&lt;p&gt;在一些实现中，支持超过两套UV被认为是低优先级功能，因为需求极少，且会增加shader的复杂度或变体数量。&lt;a href=&quot;https://github.com/atteneder/glTFast/issues/206&quot;&gt;[1]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;贴图的引用链也是分层的：&lt;code&gt;Material → Texture → Image + Sampler&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;textures&quot;: [{ &quot;source&quot;: 0, &quot;sampler&quot;: 0 }],
  &quot;images&quot;: [{ &quot;uri&quot;: &quot;albedo.png&quot; }],
  &quot;samplers&quot;: [{
    &quot;magFilter&quot;: 9729,
    &quot;minFilter&quot;: 9987,
    &quot;wrapS&quot;: 10497,
    &quot;wrapT&quot;: 10497
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Sampler 里的数字同样是 OpenGL 常量&lt;/strong&gt;. &lt;code&gt;9729&lt;/code&gt; 是 &lt;code&gt;LINEAR&lt;/code&gt;，&lt;code&gt;9987&lt;/code&gt; 是 &lt;code&gt;LINEAR_MIPMAP_LINEAR&lt;/code&gt;，&lt;code&gt;10497&lt;/code&gt; 是 &lt;code&gt;REPEAT&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── asset (资产信息)
├── scenes[] (场景数组)
├── nodes[] (节点数组)
├── meshes[] (网格数组)
│   └── primitives[] (基元数组)
│       └── attributes (顶点属性: POSITION, NORMAL, TEXCOORD_0, etc.)
├── materials[] (材质数组)
├── textures[] (纹理数组)
├── images[] (图像数组)
├── samplers[] (采样器数组)
├── animations[] (动画数组)
├── cameras[] (相机数组)
├── skins[] (皮肤/骨骼数组)
├── buffers[] (缓冲区数组)
├── bufferViews[] (缓冲区视图数组)
├── accessors[] (访问器数组)
├── extensionsUsed[] (使用的扩展)
├── extensionsRequired[] (必需的扩展)
└── extras (自定义扩展数据)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入所有扩展以后(当然还有很多功能, 可以见上), 一个基本的就有:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scene
 └─ node (transform)
     └─ mesh
         └─ primitive
             ├─ attributes → accessors → bufferViews → buffer
             ├─ indices   → accessor  → bufferView  → buffer
             └─ material
                 └─ textures → images + samplers
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;扩展完整示例&lt;/h4&gt;
&lt;p&gt;这个是ai给我的一个示例, 当然我还没有尝试过, 但是看了一眼大概是正确的&lt;/p&gt;
&lt;p&gt;同一个正方形，加上场景层级、材质、贴图后的完整结构。用 &lt;code&gt;// 注释&lt;/code&gt; 来标注每个部分的作用（实际 JSON 不支持注释，这里只是方便阅读）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  // ═══════════════════════════════════════════════
  // 元信息
  // ═══════════════════════════════════════════════
  &quot;asset&quot;: {
    &quot;version&quot;: &quot;2.0&quot;,
    &quot;generator&quot;: &quot;my-csv-to-gltf&quot;
  },

  // ═══════════════════════════════════════════════
  // 场景层级：scene → node → mesh
  // ═══════════════════════════════════════════════
  &quot;scene&quot;: 0,                         // 默认激活第0个场景

  &quot;scenes&quot;: [
    {
      &quot;name&quot;: &quot;MainScene&quot;,
      &quot;nodes&quot;: [0]                    // 场景里放了1个节点
    }
  ],

  &quot;nodes&quot;: [
    {
      &quot;name&quot;: &quot;SquarePlane&quot;,
      &quot;mesh&quot;: 0,                      // 这个节点引用第0个mesh
      &quot;translation&quot;: [0, 0, 0],       // 可选：位移
      &quot;rotation&quot;: [0, 0, 0, 1],       // 可选：四元数旋转
      &quot;scale&quot;: [1, 1, 1]              // 可选：缩放
    }
  ],

  // ═══════════════════════════════════════════════
  // 网格：把顶点属性 + 索引 + 材质绑在一起
  // ═══════════════════════════════════════════════
  &quot;meshes&quot;: [
    {
      &quot;name&quot;: &quot;SquareMesh&quot;,
      &quot;primitives&quot;: [
        {
          &quot;attributes&quot;: {
            &quot;POSITION&quot;:   0,          // → accessors[0]
            &quot;NORMAL&quot;:     1,          // → accessors[1]
            &quot;TEXCOORD_0&quot;: 2           // → accessors[2]
          },
          &quot;indices&quot;: 3,               // → accessors[3]
          &quot;material&quot;: 0,              // → materials[0]
          &quot;mode&quot;: 4                   // 4 = TRIANGLES
        }
      ]
    }
  ],

  // ═══════════════════════════════════════════════
  // 材质：PBR 金属度-粗糙度工作流
  // ═══════════════════════════════════════════════
  &quot;materials&quot;: [
    {
      &quot;name&quot;: &quot;WallMaterial&quot;,
      &quot;pbrMetallicRoughness&quot;: {
        &quot;baseColorTexture&quot;: {
          &quot;index&quot;: 0,                 // → textures[0]
          &quot;texCoord&quot;: 0               // 使用 TEXCOORD_0
        },
        &quot;baseColorFactor&quot;: [1, 1, 1, 1],
        &quot;metallicFactor&quot;: 0.0,
        &quot;roughnessFactor&quot;: 0.8
      },
      &quot;normalTexture&quot;: {              // 可选：法线贴图
        &quot;index&quot;: 1,                   // → textures[1]
        &quot;texCoord&quot;: 0,
        &quot;scale&quot;: 1.0
      },
      &quot;doubleSided&quot;: false
    }
  ],

  // ═══════════════════════════════════════════════
  // 贴图链：texture → sampler + image
  // ═══════════════════════════════════════════════
  &quot;textures&quot;: [
    {
      &quot;name&quot;: &quot;BaseColorTex&quot;,
      &quot;sampler&quot;: 0,                   // → samplers[0]
      &quot;source&quot;: 0                     // → images[0]
    },
    {
      &quot;name&quot;: &quot;NormalTex&quot;,
      &quot;sampler&quot;: 0,                   // 可以复用同一个sampler
&quot;source&quot;: 1                     // → images[1]
}
],

&quot;samplers&quot;: [
  {
    &quot;magFilter&quot;: 9729,              // LINEAR
    &quot;minFilter&quot;: 9987,              // LINEAR_MIPMAP_LINEAR
    &quot;wrapS&quot;: 10497,                 // REPEAT
    &quot;wrapT&quot;: 10497                  // REPEAT
  }
],

&quot;images&quot;: [
  {
    &quot;name&quot;: &quot;wall_diffuse&quot;,
    &quot;mimeType&quot;: &quot;image/png&quot;,
    &quot;bufferView&quot;: 4                 // → bufferViews[4]（图片二进制嵌在buffer里）
},
{
  &quot;name&quot;: &quot;wall_normal&quot;,
  &quot;mimeType&quot;: &quot;image/png&quot;,
  &quot;bufferView&quot;: 5                 // → bufferViews[5]
    }
  ],

  // ═══════════════════════════════════════════════
  // 数据描述层：accessor → bufferView → buffer
  // ═══════════════════════════════════════════════
  &quot;accessors&quot;: [
    // [0] 顶点位置
    {
      &quot;bufferView&quot;: 0,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5126,          // FLOAT
      &quot;count&quot;: 4,                     // 4个顶点（去重后）
      &quot;type&quot;: &quot;VEC3&quot;,
      &quot;min&quot;: [0.0, 0.0, 0.0],        // 包围盒最小角（必填）
      &quot;max&quot;: [1.0, 1.0, 0.0]         // 包围盒最大角（必填）
    },
    // [1] 顶点法线
    {
      &quot;bufferView&quot;: 1,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5126,          // FLOAT
      &quot;count&quot;: 4,
      &quot;type&quot;: &quot;VEC3&quot;
    },
    // [2] UV坐标
    {
      &quot;bufferView&quot;: 2,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5126,          // FLOAT
      &quot;count&quot;: 4,
      &quot;type&quot;: &quot;VEC2&quot;
    },
    // [3] 三角形索引
    {
      &quot;bufferView&quot;: 3,
      &quot;byteOffset&quot;: 0,
      &quot;componentType&quot;: 5123,          // UNSIGNED_SHORT
      &quot;count&quot;: 6,                     // 2个三角形 × 3 = 6个索引
      &quot;type&quot;: &quot;SCALAR&quot;
    }
  ],

  &quot;bufferViews&quot;: [
    // [0] 位置数据
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 0,
      &quot;byteLength&quot;: 48,              // 4顶点 × 3分量 × 4字节 = 48
      &quot;target&quot;: 34962                 // ARRAY_BUFFER（顶点属性）
    },
    // [1] 法线数据
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 48,
      &quot;byteLength&quot;: 48,              // 4顶点 × 3分量 × 4字节 = 48
      &quot;target&quot;: 34962
    },
    // [2] UV数据
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 96,
      &quot;byteLength&quot;: 32,              // 4顶点 × 2分量 × 4字节 = 32
      &quot;target&quot;: 34962
    },
    // [3] 索引数据
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 128,
      &quot;byteLength&quot;: 12,              // 6索引 × 2字节 = 12
      &quot;target&quot;: 34963                 // ELEMENT_ARRAY_BUFFER（索引）
    },
    // [4] 漫反射贴图的PNG二进制
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 140,
      &quot;byteLength&quot;: 8192             // 图片大小（举例）
    },
    // [5] 法线贴图的PNG二进制
    {
      &quot;buffer&quot;: 0,
      &quot;byteOffset&quot;: 8332,
      &quot;byteLength&quot;: 6144             // 图片大小（举例）
    }
  ],

  // ═══════════════════════════════════════════════
  // 最底层：一个大的二进制块，所有数据都塞在里面
  // ═══════════════════════════════════════════════
  &quot;buffers&quot;: [
    {
      &quot;byteLength&quot;: 14476,
      &quot;uri&quot;: &quot;data:application/octet-stream;base64,AAAA...&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;整体引用关系&lt;/h4&gt;
&lt;p&gt;从上到下是这样一棵树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scene
  └─ node（位移/旋转/缩放）
       └─ mesh
            └─ primitive
                 ├─ attributes
                 │    ├─ POSITION   → accessor[0] → bufferView[0] ─┐
                 │    ├─ NORMAL     → accessor[1] → bufferView[1]  ├→ buffer[0]
                 │    └─ TEXCOORD_0 → accessor[2] → bufferView[2] ─┤    (一个大的
                 ├─ indices         → accessor[3] → bufferView[3] ─┤     二进制块)
                 └─ material                                       │
                      ├─ baseColorTexture → texture[0]             │
                      │                      ├─ sampler[0]         │
                      │                      └─ image[0] → bufferView[4] ─┤
                      └─ normalTexture    → texture[1]             │
                                             ├─ sampler[0]         │
                                             └─ image[1] → bufferView[5] ─┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有数据最终都汇聚到同一个 &lt;code&gt;buffer[0]&lt;/code&gt;，它就是一整块连续的二进制内存。&lt;code&gt;bufferView&lt;/code&gt; 通过 &lt;code&gt;byteOffset&lt;/code&gt; 和 &lt;code&gt;byteLength&lt;/code&gt; 在这块内存里&quot;切窗口&quot;，&lt;code&gt;accessor&lt;/code&gt; 再告诉 GPU 怎么解读窗口里的字节（是 float 还是 unsigned short，是 VEC3 还是 SCALAR，有几个元素）。&lt;/p&gt;
&lt;p&gt;贴图也一样——PNG 文件的原始字节直接嵌在同一个 buffer 里，&lt;code&gt;image&lt;/code&gt; 通过 &lt;code&gt;bufferView&lt;/code&gt; 指向它，加载器读到后解码成纹理上传给 GPU。&lt;/p&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;这里展示了一个石头, 用RenderDoc导出的csv, 手写写成一个gltf文件. 也可以尝试运行这个脚本转换一下. 日志callback要自己改成print到终端. 因为callback原来是给我的客户端输出日志用的, 不需要终端. 如果是命令行的话那要改一下.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/attachments/yuque/0/2026/csv/48487597/1771698511821-4f664da5-d60c-4840-b754-e2b17d894643.csv&quot;&gt;OpenGL_Models.csv&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- 这是一张图片，ocr 内容为： --&amp;gt;
&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2026/png/48487597/1771698889900-36eb19b7-55d0-46d9-b331-fef6a4600265.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# csv_to_gltf.py
import csv
import argparse
import os
import json
import struct
import base64


def find_column_indices(header, log_callback=None):
    &quot;&quot;&quot;根据表头猜测POSITION, NORMAL, TEXCOORD, COLOR, TANGENT的起始列索引&quot;&quot;&quot;
    indices = {&apos;pos&apos;: -1, &apos;norm&apos;: -1, &apos;uv&apos;: -1, &apos;color&apos;: -1, &apos;tangent&apos;: -1}
    # 将header转为大写以进行不区分大小写的匹配
    upper_header = [h.upper() for h in header]

    try:
        # 查找包含 &apos;POS&apos; 或 &apos;POSITION&apos; 的第一个元素
        indices[&apos;pos&apos;] = next(i for i, h in enumerate(upper_header) if &apos;POS&apos; in h)
        if log_callback:
            log_callback(f&quot;  [OK] 检测到位置(Position)数据起始于列: {indices[&apos;pos&apos;]}&quot;)
    except StopIteration:
        if log_callback:
            log_callback(&quot;  [警告] 在CSV表头中未找到位置(Position)数据。&quot;)

    try:
        # 查找包含 &apos;NORM&apos; 或 &apos;NORMAL&apos; 的第一个元素
        indices[&apos;norm&apos;] = next(i for i, h in enumerate(upper_header) if &apos;NORM&apos; in h)
        if log_callback:
            log_callback(f&quot;  [OK] 检测到法线(Normal)数据起始于列: {indices[&apos;norm&apos;]}&quot;)
    except StopIteration:
        if log_callback:
            log_callback(&quot;  [警告] 在CSV表头中未找到法线(Normal)数据。&quot;)

    try:
        # 查找包含 &apos;TEX&apos; 或 &apos;TEXCOORD&apos; 的第一个元素
        indices[&apos;uv&apos;] = next(i for i, h in enumerate(upper_header) if &apos;TEX&apos; in h)
        if log_callback:
            log_callback(f&quot;  [OK] 检测到UV(TexCoord)数据起始于列: {indices[&apos;uv&apos;]}&quot;)
    except StopIteration:
        if log_callback:
            log_callback(&quot;  [警告] 在CSV表头中未找到UV(TexCoord)数据。&quot;)

    try:
        # 查找包含 &apos;COLOR&apos; 或 &apos;COLOUR&apos; 的第一个元素
        indices[&apos;color&apos;] = next(i for i, h in enumerate(upper_header) if &apos;COLOR&apos; in h or &apos;COLOUR&apos; in h)
        if log_callback:
            log_callback(f&quot;  [OK] 检测到顶点色(Color)数据起始于列: {indices[&apos;color&apos;]}&quot;)
    except StopIteration:
        if log_callback:
            log_callback(&quot;  [警告] 在CSV表头中未找到顶点色(Color)数据。&quot;)

    try:
        # 查找包含 &apos;TANGENT&apos; 的第一个元素
        indices[&apos;tangent&apos;] = next(i for i, h in enumerate(upper_header) if &apos;TANGENT&apos; in h)
        if log_callback:
            log_callback(f&quot;  [OK] 检测到切线(Tangent)数据起始于列: {indices[&apos;tangent&apos;]}&quot;)
    except StopIteration:
        if log_callback:
            log_callback(&quot;  [警告] 在CSV表头中未找到切线(Tangent)数据。&quot;)

    return indices


def convert_csv_to_gltf(input_filepath, output_filepath, log_callback=None):
    &quot;&quot;&quot;
    将RenderDoc导出的CSV文件转换为GLTF模型文件。
    拓扑结构为三角面列表 (Triangle List)。
    &quot;&quot;&quot;
    if log_callback:
        log_callback(f&quot;正在处理: {input_filepath} -&amp;gt; {output_filepath}&quot;)
    try:
        with open(input_filepath, &apos;r&apos;, encoding=&apos;utf-8&apos;) as infile:
            reader = csv.reader(infile)

            header = next(reader)
            col_indices = find_column_indices(header, log_callback)

            if col_indices[&apos;pos&apos;] == -1:
                if log_callback:
                    log_callback(&quot;  [错误] CSV文件中必须包含顶点位置数据。跳过此文件。&quot;)
                return False

            vertices = list(reader)
            num_vertices = len(vertices)

            if num_vertices == 0:
                if log_callback:
                    log_callback(&quot;  [错误] CSV文件中没有数据行。跳过此文件。&quot;)
                return False

            # 准备数据缓冲区
            positions = []
            normals = []
            texcoords = []
            colors = []
            tangents = []
            indices = []

            # 读取顶点数据
            for idx, row in enumerate(vertices):
                # 位置数据
                x, y, z = map(float, row[col_indices[&apos;pos&apos;]:col_indices[&apos;pos&apos;] + 3])
                positions.extend([x, y, z])

                # 法线数据
                if col_indices[&apos;norm&apos;] != -1:
                    nx, ny, nz = map(float, row[col_indices[&apos;norm&apos;]:col_indices[&apos;norm&apos;] + 3])
                    normals.extend([nx, ny, nz])

                # UV数据
                if col_indices[&apos;uv&apos;] != -1:
                    u, v = map(float, row[col_indices[&apos;uv&apos;]:col_indices[&apos;uv&apos;] + 2])
                    texcoords.extend([u, 1.0 - v])  # 翻转V坐标

                # 顶点色数据
                if col_indices[&apos;color&apos;] != -1:
                    # 尝试读取4个分量(RGBA)，如果只有3个则添加alpha=1.0
                    try:
                        r = float(row[col_indices[&apos;color&apos;]])
                        g = float(row[col_indices[&apos;color&apos;] + 1])
                        b = float(row[col_indices[&apos;color&apos;] + 2])
                        # 尝试读取alpha通道
                        try:
                            a = float(row[col_indices[&apos;color&apos;] + 3])
                        except (IndexError, ValueError):
                            a = 1.0
                        colors.extend([r, g, b, a])
                    except (IndexError, ValueError) as e:
                        if log_callback:
                            log_callback(f&quot;  [警告] 第{idx}行顶点色数据读取失败: {e}&quot;)
                        colors.extend([1.0, 1.0, 1.0, 1.0])  # 默认白色

                # 切线数据
                if col_indices[&apos;tangent&apos;] != -1:
                    try:
                        tx = float(row[col_indices[&apos;tangent&apos;]])
                        ty = float(row[col_indices[&apos;tangent&apos;] + 1])
                        tz = float(row[col_indices[&apos;tangent&apos;] + 2])
                        tw = float(row[col_indices[&apos;tangent&apos;] + 3])
                        tangents.extend([tx, ty, tz, tw])
                    except (IndexError, ValueError) as e:
                        if log_callback:
                            log_callback(f&quot;  [警告] 第{idx}行切线数据读取失败: {e}&quot;)
                        tangents.extend([1.0, 0.0, 0.0, 1.0])  # 默认切线

                # 索引数据（对于三角形列表，索引就是顺序）
                indices.append(idx)

            # 计算边界框
            min_pos = [min(positions[i::3]) for i in range(3)]
            max_pos = [max(positions[i::3]) for i in range(3)]

            buffer_parts = []  # 用于存储各个部分的二进制数据
            buffer_views = []
            accessors = []
            current_offset = 0

            def add_buffer_data(data, component_type, accessor_type, target=None):
                &quot;&quot;&quot;添加数据到buffer并创建对应的bufferView和accessor&quot;&quot;&quot;
                nonlocal current_offset

                # 将数据转换为二进制
                if component_type == 5126:  # FLOAT
                    binary_data = struct.pack(f&apos;{len(data)}f&apos;, *data)
                elif component_type == 5125:  # UNSIGNED_INT
                    binary_data = struct.pack(f&apos;{len(data)}I&apos;, *data)
                elif component_type == 5123:  # UNSIGNED_SHORT
                    binary_data = struct.pack(f&apos;{len(data)}H&apos;, *data)
                else:
                    raise ValueError(f&quot;不支持的component_type: {component_type}&quot;)

                byte_length = len(binary_data)

                # 添加padding以满足4字节对齐要求
                padding_needed = (4 - (byte_length % 4)) % 4
                if padding_needed &amp;gt; 0:
                    binary_data += b&apos;\x00&apos; * padding_needed

                # 创建bufferView
                buffer_view = {
                    &quot;buffer&quot;: 0,
                    &quot;byteOffset&quot;: current_offset,
                    &quot;byteLength&quot;: byte_length
                }
                if target is not None:
                    buffer_view[&quot;target&quot;] = target

                buffer_view_index = len(buffer_views)
                buffer_views.append(buffer_view)

                # 计算accessor的count
                if accessor_type == &quot;SCALAR&quot;:
                    count = len(data)
                elif accessor_type == &quot;VEC2&quot;:
                    count = len(data) // 2
                elif accessor_type == &quot;VEC3&quot;:
                    count = len(data) // 3
                elif accessor_type == &quot;VEC4&quot;:
                    count = len(data) // 4
                else:
                    raise ValueError(f&quot;不支持的accessor_type: {accessor_type}&quot;)

                # 创建accessor
                accessor = {
                    &quot;bufferView&quot;: buffer_view_index,
                    &quot;componentType&quot;: component_type,
                    &quot;count&quot;: count,
                    &quot;type&quot;: accessor_type
                }

                accessor_index = len(accessors)
                accessors.append(accessor)

                # 更新偏移量
                buffer_parts.append(binary_data)
                current_offset += len(binary_data)

                return accessor_index

            # 添加各种数据的accessor
            attributes = {}

            # 位置 (必须有min/max)
            pos_accessor = add_buffer_data(positions, 5126, &quot;VEC3&quot;, 34962)
            accessors[pos_accessor][&quot;min&quot;] = min_pos
            accessors[pos_accessor][&quot;max&quot;] = max_pos
            attributes[&quot;POSITION&quot;] = pos_accessor

            # 法线
            if normals:
                norm_accessor = add_buffer_data(normals, 5126, &quot;VEC3&quot;, 34962)
                attributes[&quot;NORMAL&quot;] = norm_accessor

            # UV
            if texcoords:
                uv_accessor = add_buffer_data(texcoords, 5126, &quot;VEC2&quot;, 34962)
                attributes[&quot;TEXCOORD_0&quot;] = uv_accessor

            # 顶点色
            if colors:
                color_accessor = add_buffer_data(colors, 5126, &quot;VEC4&quot;, 34962)
                attributes[&quot;COLOR_0&quot;] = color_accessor

            # 切线
            if tangents:
                tangent_accessor = add_buffer_data(tangents, 5126, &quot;VEC4&quot;, 34962)
                attributes[&quot;TANGENT&quot;] = tangent_accessor

            # 索引 - 根据顶点数选择合适的类型
            if num_vertices &amp;lt; 65536:
                # 使用UNSIGNED_SHORT节省空间
                indices_accessor = add_buffer_data(indices, 5123, &quot;SCALAR&quot;, 34963)
            else:
                # 使用UNSIGNED_INT
                indices_accessor = add_buffer_data(indices, 5125, &quot;SCALAR&quot;, 34963)

            # 合并所有二进制数据
            combined_buffer = b&apos;&apos;.join(buffer_parts)

            # 从输入文件名生成 mesh 名称和材质名称
            input_basename = os.path.basename(input_filepath)
            mesh_name = os.path.splitext(input_basename)[0]
            # 移除可能的 &quot;fbx_&quot; 前缀，使名称更简洁
            if mesh_name.lower().startswith(&quot;fbx_&quot;):
                mesh_name = mesh_name[4:]
            material_name = f&quot;M_{mesh_name}&quot;

            # 创建GLTF结构
            gltf = {
                &quot;asset&quot;: {
                    &quot;version&quot;: &quot;2.0&quot;,
                    &quot;generator&quot;: &quot;csv_to_gltf.py v2&quot;
                },
                &quot;scene&quot;: 0,
                &quot;scenes&quot;: [
                    {
                        &quot;nodes&quot;: [0]
                    }
                ],
                &quot;nodes&quot;: [
                    {
                        &quot;name&quot;: mesh_name,
                        &quot;mesh&quot;: 0
                    }
                ],
                &quot;meshes&quot;: [
                    {
                        &quot;name&quot;: mesh_name,
                        &quot;primitives&quot;: [
                            {
                                &quot;attributes&quot;: attributes,
                                &quot;indices&quot;: indices_accessor,
                                &quot;mode&quot;: 4,  # TRIANGLES
                                &quot;material&quot;: 0
                            }
                        ]
                    }
                ],
                &quot;materials&quot;: [
                    {
                        &quot;name&quot;: material_name,
                        &quot;pbrMetallicRoughness&quot;: {
                            &quot;baseColorFactor&quot;: [1.0, 1.0, 1.0, 1.0],
                            &quot;metallicFactor&quot;: 0.0,
                            &quot;roughnessFactor&quot;: 0.5
                        }
                    }
                ],
                &quot;accessors&quot;: accessors,
                &quot;bufferViews&quot;: buffer_views,
                &quot;buffers&quot;: [
                    {
                        &quot;byteLength&quot;: len(combined_buffer),
                        &quot;uri&quot;: f&quot;data:application/octet-stream;base64,{base64.b64encode(combined_buffer).decode(&apos;ascii&apos;)}&quot;
                    }
                ]
            }

            # 写入GLTF文件
            with open(output_filepath, &apos;w&apos;, encoding=&apos;utf-8&apos;) as outfile:
                json.dump(gltf, outfile, indent=2)

            if log_callback:
                log_callback(f&quot;  [成功] 转换完成！模型已保存到: {output_filepath}&quot;)
                log_callback(f&quot;  顶点数: {num_vertices}, 三角形数: {num_vertices // 3}&quot;)
                log_callback(f&quot;  包含属性: {&apos;, &apos;.join(attributes.keys())}&quot;)
            return True

    except FileNotFoundError:
        if log_callback:
            log_callback(f&quot;  [错误] 输入文件 &apos;{input_filepath}&apos; 未找到。&quot;)
    except Exception as e:
        if log_callback:
            log_callback(f&quot;  [错误] 转换过程中发生错误: {e}&quot;)
        import traceback
        traceback.print_exc()
    return False


# ==============================================================================
# 脚本主入口点
# ==============================================================================
if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(
        description=&quot;将RenderDoc导出的顶点CSV文件转换为GLTF模型。支持单文件或批量转换。&quot;,
        formatter_class=argparse.RawTextHelpFormatter
    )

    parser.add_argument(
        &quot;input&quot;,
        nargs=&apos;?&apos;,
        default=None,
        help=&quot;可选: 要转换的单个CSV文件路径。\n如果留空，脚本将自动转换当前目录下的所有.csv文件。&quot;
    )
    parser.add_argument(
        &quot;-o&quot;, &quot;--output&quot;,
        help=&quot;可选: 输出的GLTF文件路径 (仅在指定单个输入文件时有效)。&quot;
    )

    args = parser.parse_args()
    if args.input:
        # **单文件模式**: 用户提供了输入文件名
        print(&quot;模式: 单文件转换&quot;)
        input_filepath = args.input

        # 决定输出文件名
        if args.output:
            output_filepath = args.output
        else:
            base_name = os.path.splitext(input_filepath)[0]
            output_filepath = f&quot;{base_name}.gltf&quot;

        convert_csv_to_gltf(input_filepath, output_filepath)

    else:
        # **批量模式**: 用户没有提供输入文件名
        print(&quot;模式: 批量转换 (未指定输入文件，将搜索当前目录)&quot;)

        csv_files = [f for f in os.listdir(&apos;.&apos;) if f.lower().endswith(&apos;.csv&apos;)]

        if not csv_files:
            print(&quot;在当前目录中未找到任何 .csv 文件。&quot;)
        else:
            print(f&quot;找到 {len(csv_files)} 个CSV文件，准备开始转换...\n&quot;)
            success_count = 0
            fail_count = 0
            for filename in csv_files:
                base_name = os.path.splitext(filename)[0]
                output_filename = f&quot;{base_name}.gltf&quot;

                if convert_csv_to_gltf(filename, output_filename):
                    success_count += 1
                else:
                    fail_count += 1
                print(&quot;-&quot; * 60)

            print(&quot;\n批量转换完成！&quot;)
            print(f&quot;总计: {success_count} 个成功, {fail_count} 个失败。&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第一阶段：解析 CSV，把原始数据分类装桶&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for idx, row in enumerate(vertices):
    x, y, z = map(float, row[col_indices[&apos;pos&apos;]:col_indices[&apos;pos&apos;] + 3])
    positions.extend([x, y, z])

    if col_indices[&apos;norm&apos;] != -1:
        nx, ny, nz = map(float, row[col_indices[&apos;norm&apos;]:col_indices[&apos;norm&apos;] + 3])
        normals.extend([nx, ny, nz])

    if col_indices[&apos;uv&apos;] != -1:
        u, v = map(float, row[col_indices[&apos;uv&apos;]:col_indices[&apos;uv&apos;] + 2])
        texcoords.extend([u, 1.0 - v])  # 翻转V坐标

    indices.append(idx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步还没涉及 glTF，纯粹是根据RenderDoc的解析把 CSV 的每一行拆开，把位置、法线、UV 等数据分别存进独立的 Python 列表。每个列表是一个扁平的浮点数组，比如 &lt;code&gt;positions = [x0, y0, z0, x1, y1, z1, ...]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意 &lt;strong&gt;&lt;code&gt;**1.0 - v**&lt;/code&gt;&lt;/strong&gt; 这个翻转——因为 CSV 来自 RenderDoc（OpenGL 捕获），OpenGL 的纹理坐标原点在左下角，而 glTF 规范要求纹理原点在左上角，所以 V 坐标要翻转。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还有一个关键点：&lt;code&gt;indices&lt;/code&gt; 在这里就是 &lt;code&gt;[0, 1, 2, 3, 4, ...]&lt;/code&gt; 的顺序索引。因为 RenderDoc 导出的 CSV 已经是展开的三角形列表（每三行一个三角形），所以索引就是顺序排列。 CSV 里虽然有 &lt;code&gt;IDX&lt;/code&gt; 列，但这个脚本没有用它——这意味着重复的顶点没有被去重复用，而是直接按原样写入. 这确实是我未来的一个改进方式&lt;/p&gt;
&lt;h3&gt;第二阶段：把浮点数组打包成二进制, 构建 Buffer&lt;/h3&gt;
&lt;p&gt;这是整个脚本的核心，&lt;code&gt;add_buffer_data&lt;/code&gt; 函数做了三件事：&lt;/p&gt;
&lt;h4&gt;2.1 把 Python 列表变成字节&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;if component_type == 5126:  # FLOAT
    binary_data = struct.pack(f&apos;{len(data)}f&apos;, *data)
elif component_type == 5125:  # UNSIGNED_INT
    binary_data = struct.pack(f&apos;{len(data)}I&apos;, *data)
elif component_type == 5123:  # UNSIGNED_SHORT
    binary_data = struct.pack(f&apos;{len(data)}H&apos;, *data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;struct.pack&lt;/code&gt; 把 Python 的浮点数/整数按照指定格式编码为原始字节。比如 &lt;code&gt;positions = [4.52453, 10.11227, -1.35187, ...]&lt;/code&gt; 经过 &lt;code&gt;struct.pack(&apos;330f&apos;, ...)&lt;/code&gt; 就变成了 1320 字节的二进制数据（每个 float 4 字节，330 个顶点 × 3 分量 = 990 个 float，共 3960 字节）。&lt;/p&gt;
&lt;p&gt;这里的 &lt;code&gt;5126&lt;/code&gt;、&lt;code&gt;5125&lt;/code&gt;、&lt;code&gt;5123&lt;/code&gt; 就是 glTF/OpenGL 的常量，分别对应 &lt;code&gt;FLOAT&lt;/code&gt;、&lt;code&gt;UNSIGNED_INT&lt;/code&gt;、&lt;code&gt;UNSIGNED_SHORT&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;2.2 处理字节对齐&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;padding_needed = (4 - (byte_length % 4)) % 4
if padding_needed &amp;gt; 0:
    binary_data += b&apos;\x00&apos; * padding_needed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;glTF 规范要求 bufferView 的 &lt;code&gt;byteOffset&lt;/code&gt; 必须是 4 的倍数。如果某段数据长度不是 4 的整数倍（比如索引用 UNSIGNED_SHORT，3 个索引 = 6 字节），就在末尾补零填充到 4 的倍数（8 字节）。这段代码保证了后续数据的起始位置一定是对齐的。&lt;/p&gt;
&lt;h4&gt;2.3 创建 BufferView 和 Accessor&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;buffer_view = {
    &quot;buffer&quot;: 0,
    &quot;byteOffset&quot;: current_offset,
    &quot;byteLength&quot;: byte_length
}

accessor = {
    &quot;bufferView&quot;: buffer_view_index,
    &quot;componentType&quot;: component_type,
    &quot;count&quot;: count,
    &quot;type&quot;: accessor_type
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每调用一次 &lt;code&gt;add_buffer_data&lt;/code&gt;，就在同一个 Buffer 里&quot;划出一块区域&quot;（BufferView），然后给这块区域一个&quot;解读说明&quot;（Accessor）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;current_offset&lt;/code&gt; 是一个不断递增的指针，记录当前写到了 Buffer 的第几个字节。每次写完一段数据（包括 padding），指针往后移。所以最终的 Buffer 内存布局是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[positions 数据 | padding | normals 数据 | padding | uvs 数据 | padding | tangents 数据 | padding | indices 数据 | padding]
 ^                         ^                        ^                     ^                         ^
 bufferView 0              bufferView 1              bufferView 2          bufferView 3              bufferView 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是我之前说的&lt;strong&gt;分离布局&lt;/strong&gt;——每种属性各占一个 BufferView，简单明了&lt;/p&gt;
&lt;h3&gt;第三阶段：组装 Mesh Primitive&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;attributes = {}

pos_accessor = add_buffer_data(positions, 5126, &quot;VEC3&quot;, 34962)
accessors[pos_accessor][&quot;min&quot;] = min_pos
accessors[pos_accessor][&quot;max&quot;] = max_pos
attributes[&quot;POSITION&quot;] = pos_accessor

if normals:
    norm_accessor = add_buffer_data(normals, 5126, &quot;VEC3&quot;, 34962)
    attributes[&quot;NORMAL&quot;] = norm_accessor

if texcoords:
    uv_accessor = add_buffer_data(texcoords, 5126, &quot;VEC2&quot;, 34962)
    attributes[&quot;TEXCOORD_0&quot;] = uv_accessor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;34962&lt;/code&gt; 是 &lt;code&gt;ARRAY_BUFFER&lt;/code&gt;（顶点属性），&lt;code&gt;34963&lt;/code&gt; 是 &lt;code&gt;ELEMENT_ARRAY_BUFFER&lt;/code&gt;（索引）。&lt;/p&gt;
&lt;p&gt;最终 &lt;code&gt;attributes&lt;/code&gt; 字典会变成类似 &lt;code&gt;{&quot;POSITION&quot;: 0, &quot;NORMAL&quot;: 1, &quot;TEXCOORD_0&quot;: 2, &quot;TANGENT&quot;: 3}&lt;/code&gt;，每个值就是对应 accessor 的索引号。&lt;/p&gt;
&lt;p&gt;注意 POSITION 的 accessor 额外添加了 &lt;code&gt;min&lt;/code&gt; 和 &lt;code&gt;max&lt;/code&gt;——glTF 规范&lt;strong&gt;强制要求&lt;/strong&gt; POSITION 必须有这两个字段，用于加载器快速计算包围盒，不需要遍历所有顶点。&lt;/p&gt;
&lt;h3&gt;第四阶段：编码 Buffer 并组装最终 JSON&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;combined_buffer = b&apos;&apos;.join(buffer_parts)

&quot;buffers&quot;: [
    {
        &quot;byteLength&quot;: len(combined_buffer),
        &quot;uri&quot;: f&quot;data:application/octet-stream;base64,{base64.b64encode(combined_buffer).decode(&apos;ascii&apos;)}&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里把所有二进制数据合并成一整块，然后用 base64 编码直接内嵌到 JSON 里。这是 glTF 支持的一种方式——所有数据都在一个 &lt;code&gt;.gltf&lt;/code&gt; 文件里，不需要额外的 &lt;code&gt;.bin&lt;/code&gt; 文件。好处是分发方便（只有一个文件），代价是 base64 编码会让数据体积膨胀约 33%。&lt;/p&gt;
&lt;p&gt;另一种做法是把二进制数据写成独立的 &lt;code&gt;.bin&lt;/code&gt; 文件，然后 &lt;code&gt;&quot;uri&quot;: &quot;model.bin&quot;&lt;/code&gt;。还有第三种就是 &lt;code&gt;.glb&lt;/code&gt; 格式，把 JSON 和二进制数据打包在一个二进制容器里。&lt;/p&gt;
&lt;h3&gt;第五阶段：场景结构和材质&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&quot;scenes&quot;: [{&quot;nodes&quot;: [0]}],
&quot;nodes&quot;: [{&quot;name&quot;: mesh_name, &quot;mesh&quot;: 0}],
&quot;meshes&quot;: [{
    &quot;name&quot;: mesh_name,
    &quot;primitives&quot;: [{
        &quot;attributes&quot;: attributes,
        &quot;indices&quot;: indices_accessor,
        &quot;mode&quot;: 4,
        &quot;material&quot;: 0
    }]
}],
&quot;materials&quot;: [{
    &quot;name&quot;: material_name,
    &quot;pbrMetallicRoughness&quot;: {
        &quot;baseColorFactor&quot;: [1.0, 1.0, 1.0, 1.0],
        &quot;metallicFactor&quot;: 0.0,
        &quot;roughnessFactor&quot;: 0.5
    }
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是最上层的组装。一个 Scene 包含一个 Node，Node 引用一个 Mesh，Mesh 有一个 Primitive，Primitive 把 attributes 和 indices 连接起来，并指向一个默认材质。&lt;code&gt;mode: 4&lt;/code&gt; 是三角形列表。&lt;/p&gt;
&lt;p&gt;材质这里很简单，白色基础色、非金属（&lt;code&gt;metallicFactor: 0&lt;/code&gt;）、中等粗糙度。因为 CSV 里没有贴图信息，所以没有 textures、images、samplers 部分。&lt;/p&gt;
&lt;h3&gt;这个脚本的改进方向&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一，只写入了 TEXCOORD_0，丢掉了 TEXCOORD_1。&lt;/strong&gt;&lt;code&gt;find_column_indices&lt;/code&gt; 只找了第一个匹配 &lt;code&gt;&apos;TEX&apos;&lt;/code&gt; 的列，你的第二套UV（&lt;code&gt;in_TEXCOORD1.x&lt;/code&gt;, &lt;code&gt;in_TEXCOORD1.y&lt;/code&gt;）被忽略了。要支持多套UV，需要在检测时找到所有匹配的列，然后分别写入 &lt;code&gt;TEXCOORD_0&lt;/code&gt; 和 &lt;code&gt;TEXCOORD_1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，索引没有去重。&lt;/strong&gt; RenderDoc解析出的 CSV 里有大量重复顶点（比如 IDX=0 出现了三次），但脚本把它们当作独立顶点写入，索引就是 &lt;code&gt;[0, 1, 2, 3, ...]&lt;/code&gt; 的顺序。如果用 IDX 列来构建真正的索引缓冲区，可以显著减小文件体积。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三，法线的 w 分量被丢弃了。&lt;/strong&gt; CSV 里法线有 4 个分量（&lt;code&gt;in_NORMAL0.x/y/z/w&lt;/code&gt;），但 glTF 的 NORMAL 只支持 VEC3，所以 w 分量被忽略了。这通常没问题，因为法线的 w 一般就是 0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后, 我的评价是: 还是专区pygltf吧(毕竟手写要加减和node啥的人家有轮子了我为什么还要传统手写呢, 不过确实手写能了解到很多有关于gltf的底层数据结构了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后补充一个纹理嵌入引用(半废弃的)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
纹理匹配器和GLTF材质处理器
用于为RenderDoc导出的GLTF模型添加纹理材质
&quot;&quot;&quot;

import os
import json
import base64
import struct
from typing import Dict, List, Optional, Tuple
import mimetypes


class TextureMatcher:
    &quot;&quot;&quot;纹理匹配器 - 基于关键字匹配纹理类型&quot;&quot;&quot;

    def __init__(self, texture_keywords: Dict[str, List[str]], texture_formats: List[str] = None):
        &quot;&quot;&quot;
        初始化纹理匹配器

        Args:
            texture_keywords: 纹理关键字映射，如 {&apos;diffuse&apos;: [&apos;diff&apos;, &apos;albedo&apos;], &apos;normal&apos;: [&apos;norm&apos;, &apos;nrm&apos;]}
            texture_formats: 纹理格式优先级，默认 [&apos;exr&apos;, &apos;png&apos;]
        &quot;&quot;&quot;
        self.texture_keywords = texture_keywords
        self.texture_formats = texture_formats or [&apos;png&apos;, &apos;exr&apos;]

    def analyze_texture_type(self, filename: str) -&amp;gt; str:
        &quot;&quot;&quot;
        基于文件名分析纹理类型，智能过滤内部缓冲区名称

        Args:
            filename: 纹理文件名

        Returns:
            纹理类型 (&apos;diffuse&apos;, &apos;normal&apos;, &apos;unknown&apos;)
        &quot;&quot;&quot;
        filename_lower = filename.lower()

        # 排除渲染内部缓冲区名称（这些不是真正的纹理）
        # 模式：包含技术规格参数，如分辨率、格式等
        internal_patterns = [
            r&apos;\d+x\d+&apos;,  # 分辨率，如 1920x1200
            r&apos;_[rb]\d+g\d+b\d+&apos;,  # 格式，如 r8g8b8a8
            r&apos;_tex\d*d&apos;,  # tex2d, tex3d
            r&apos;_sf\d+&apos;,  # 采样格式
            r&apos;_bf\d+&apos;,  # 缓冲区格式
        ]

        import re
        # 如果文件名匹配内部缓冲区模式，降低优先级
        is_internal = any(re.search(pattern, filename_lower) for pattern in internal_patterns)

        # 计算匹配分数
        best_match = &apos;unknown&apos;
        best_score = 0

        for tex_type, keywords in self.texture_keywords.items():
            for keyword in keywords:
                if keyword in filename_lower:
                    # 基础分数
                    score = 1

                    # 内部缓冲区名称扣分
                    if is_internal:
                        score -= 0.5

                    # 在文件名开头或紧跟在tex_后的关键字加分（如 tex_diff.png）
                    if filename_lower.startswith(keyword) or f&apos;tex_{keyword}&apos; in filename_lower:
                        score += 1

                    # 简短文件名加分（真正的纹理通常有简短名称）
                    if len(filename) &amp;lt; 30:
                        score += 0.5

                    # 选择最高分
                    if score &amp;gt; best_score:
                        best_score = score
                        best_match = tex_type

        return best_match

    def find_best_texture(self, textures_folder: str, texture_type: str) -&amp;gt; Optional[str]:
        &quot;&quot;&quot;
        在指定文件夹中找到最佳纹理文件（使用评分系统）

        Args:
            textures_folder: 纹理文件夹路径
            texture_type: 纹理类型 (&apos;diffuse&apos;, &apos;normal&apos;)

        Returns:
            最佳纹理文件名，如果没找到返回None
        &quot;&quot;&quot;
        if not os.path.exists(textures_folder):
            return None

        best_file = None
        best_score = -999

        for file in os.listdir(textures_folder):
            file_lower = file.lower()
            extension = file_lower.split(&apos;.&apos;)[-1]

            # 检查格式
            if extension not in self.texture_formats:
                continue

            # 分析类型
            actual_type = self.analyze_texture_type(file)
            if actual_type != texture_type:
                continue

            # 计算综合分数
            score = 0

            # 格式优先级加分
            format_priority = self.texture_formats.index(extension)
            score -= format_priority * 10

            # 内部缓冲区扣分
            internal_patterns = [
                r&apos;\d+x\d+&apos;,
                r&apos;_[rb]\d+g\d+b\d+&apos;,
                r&apos;_tex\d*d&apos;,
                r&apos;_sf\d+&apos;,
                r&apos;_bf\d+&apos;,
                r&apos;_camera&apos;,
                r&apos;_attachment&apos;,
                r&apos;_shadow&apos;,
                r&apos;_normals&apos;,
                r&apos;_depth&apos;,
            ]

            import re
            if any(re.search(pattern, file_lower) for pattern in internal_patterns):
                score -= 100

            # 友好的纹理名称加分
            friendly_patterns = [
                (r&apos;tex_.*?_diff&apos;, 50),  # tex_xxx_diff
                (r&apos;tex_.*?_nm&apos;, 50),   # tex_xxx_nm
                (r&apos;.*_diffuse&apos;, 40),
                (r&apos;.*_normal&apos;, 40),
                (r&apos;.*_albedo&apos;, 40),
                (r&apos;.*_col&apos;, 20),
                (r&apos;.*_nrm&apos;, 20),
            ]

            for pattern, bonus in friendly_patterns:
                if re.search(pattern, file_lower):
                    score += bonus
                    break

            # 简短文件名加分
            if len(file) &amp;lt; 30:
                score += 10

            # 选择最高分
            if score &amp;gt; best_score:
                best_score = score
                best_file = file

        return best_file

    def find_textures_in_model_folder(self, model_folder: str) -&amp;gt; Dict[str, str]:
        &quot;&quot;&quot;
        在模型文件夹中查找所有纹理

        Args:
            model_folder: 模型文件夹路径

        Returns:
            纹理映射字典 {&apos;diffuse&apos;: &apos;path/to/diffuse.exr&apos;, &apos;normal&apos;: &apos;path/to/normal.png&apos;}
        &quot;&quot;&quot;
        texture_map = {}

        # 检查的纹理文件夹列表
        texture_folders = [
            os.path.join(model_folder, &quot;textures&quot;),       # 优先PNG
            os.path.join(model_folder, &quot;textures_exr&quot;)   # 备选EXR
        ]

        for texture_type in [&apos;diffuse&apos;, &apos;normal&apos;]:
            for folder in texture_folders:
                texture_file = self.find_best_texture(folder, texture_type)
                if texture_file:
                    texture_map[texture_type] = os.path.join(folder, texture_file)
                    break  # 找到就停止

        return texture_map


class GLTFMaterialProcessor:
    &quot;&quot;&quot;GLTF材质处理器 - 为GLTF文件添加材质和纹理&quot;&quot;&quot;

    def __init__(self, embed_textures: bool = True, color_spaces: Dict[str, str] = None):
        &quot;&quot;&quot;
        初始化GLTF材质处理器

        Args:
            embed_textures: 是否内嵌纹理到GLB文件
            color_spaces: 颜色空间映射 {&apos;diffuse&apos;: &apos;srgb&apos;, &apos;normal&apos;: &apos;linear&apos;}
        &quot;&quot;&quot;
        self.embed_textures = embed_textures
        self.color_spaces = color_spaces or {&apos;diffuse&apos;: &apos;srgb&apos;, &apos;normal&apos;: &apos;linear&apos;}

    def create_pbr_material(self, material_name: str = &quot;PBR_Material&quot;, 
                           diffuse_texture: str = None, normal_texture: str = None) -&amp;gt; Dict:
        &quot;&quot;&quot;
        创建PBR材质

        Args:
            material_name: 材质名称
            diffuse_texture: 漫反射纹理路径
            normal_texture: 法线纹理路径

        Returns:
            GLTF材质字典
        &quot;&quot;&quot;
        material = {
            &quot;name&quot;: material_name,
            &quot;pbrMetallicRoughness&quot;: {}
        }

        # 添加漫反射纹理
        if diffuse_texture:
            material[&quot;pbrMetallicRoughness&quot;][&quot;baseColorTexture&quot;] = {
                &quot;index&quot;: 0  # 将在后续填充正确的索引
            }

        # 添加法线纹理
        if normal_texture:
            material[&quot;normalTexture&quot;] = {
                &quot;index&quot;: 1,  # 将在后续填充正确的索引
                &quot;scale&quot;: 2.0  # 增加法线强度，部分渲染器需要更强的法线效果
            }

        # 设置默认值
        if not diffuse_texture:
            material[&quot;pbrMetallicRoughness&quot;][&quot;baseColorFactor&quot;] = [1.0, 1.0, 1.0, 1.0]

        material[&quot;pbrMetallicRoughness&quot;][&quot;metallicFactor&quot;] = 0.0
        material[&quot;pbrMetallicRoughness&quot;][&quot;roughnessFactor&quot;] = 1.0

        return material

    def load_texture_data(self, texture_path: str) -&amp;gt; bytes:
        &quot;&quot;&quot;
        加载纹理数据

        Args:
            texture_path: 纹理文件路径

        Returns:
            纹理二进制数据
        &quot;&quot;&quot;
        try:
            with open(texture_path, &apos;rb&apos;) as f:
                return f.read()
        except Exception as e:
            raise Exception(f&quot;Failed to load texture {texture_path}: {str(e)}&quot;)

    def get_texture_mime_type(self, texture_path: str) -&amp;gt; str:
        &quot;&quot;&quot;
        获取纹理MIME类型

        Args:
            texture_path: 纹理文件路径

        Returns:
            MIME类型字符串
        &quot;&quot;&quot;
        # 根据文件扩展名确定MIME类型
        ext = os.path.splitext(texture_path)[1].lower()
        
        if ext == &apos;.png&apos;:
            return &apos;image/png&apos;
        elif ext == &apos;.jpg&apos; or ext == &apos;.jpeg&apos;:
            return &apos;image/jpeg&apos;
        elif ext == &apos;.webp&apos;:
            return &apos;image/webp&apos;
        elif ext == &apos;.ktx2&apos;:
            return &apos;image/ktx2&apos;
        else:
            # 对于EXR等其他格式，glTF规范不支持，但我们需要返回一个合理的默认值
            # 实际上，如果纹理是EXR格式，我们应该在导出时转换为PNG
            mime_type, _ = mimetypes.guess_type(texture_path)
            return mime_type or &apos;image/png&apos;  # 默认返回PNG

    def create_texture_info(self, texture_path: str) -&amp;gt; Dict:
        &quot;&quot;&quot;
        创建纹理信息

        Args:
            texture_path: 纹理文件路径

        Returns:
            GLTF纹理信息字典
        &quot;&quot;&quot;
        return {
            &quot;uri&quot;: os.path.basename(texture_path),
            &quot;mimeType&quot;: self.get_texture_mime_type(texture_path)
        }

    def add_materials_to_gltf(self, gltf_path: str, model_folder: str,
                            texture_matcher: TextureMatcher, log_callback=None) -&amp;gt; bool:
        &quot;&quot;&quot;
        为GLTF文件添加材质和纹理

        Args:
            gltf_path: GLTF文件路径
            model_folder: 模型文件夹路径
            texture_matcher: 纹理匹配器实例
            log_callback: 日志回调函数

        Returns:
            是否成功
        &quot;&quot;&quot;
        try:
            # 1. 读取现有GLTF文件
            with open(gltf_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
                gltf_data = json.load(f)

            if log_callback:
                log_callback(f&quot;正在处理GLTF文件: {os.path.basename(gltf_path)}&quot;)

            # 2. 查找纹理文件
            texture_map = texture_matcher.find_textures_in_model_folder(model_folder)

            if not texture_map:
                if log_callback:
                    log_callback(&quot;  未找到任何纹理文件，跳过材质处理&quot;)
                return False

            # 报告找到的纹理
            for tex_type, tex_path in texture_map.items():
                if log_callback:
                    log_callback(f&quot;  找到{tex_type}纹理: {os.path.basename(tex_path)}&quot;)

            # 3. 从GLTF文件名生成材质名称
            gltf_basename = os.path.basename(gltf_path)
            mesh_name = os.path.splitext(gltf_basename)[0]
            # 移除可能的 &quot;fbx_&quot; 前缀
            if mesh_name.lower().startswith(&quot;fbx_&quot;):
                mesh_name = mesh_name[4:]
            material_name = f&quot;M_{mesh_name}&quot;

            # 4. 创建材质
            diffuse_path = texture_map.get(&apos;diffuse&apos;)
            normal_path = texture_map.get(&apos;normal&apos;)

            material = self.create_pbr_material(material_name, diffuse_path, normal_path)

            # 4. 更新GLTF结构
            self._update_gltf_with_materials(gltf_data, texture_map, material, model_folder, log_callback)

            # 5. 保存更新后的GLTF文件
            output_path = gltf_path.replace(&apos;.gltf&apos;, &apos;_with_materials.gltf&apos;)
            with open(output_path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
                json.dump(gltf_data, f, indent=2)

            if log_callback:
                log_callback(f&quot;  [成功] 材质已添加，保存到: {os.path.basename(output_path)}&quot;)

            return True

        except Exception as e:
            if log_callback:
                log_callback(f&quot;  [错误] {str(e)}&quot;)
            return False

    def _update_gltf_with_materials(self, gltf_data: Dict, texture_map: Dict,
                                  material: Dict, model_folder: str, log_callback=None):
        &quot;&quot;&quot;
        更新GLTF数据结构，添加材质和纹理

        Args:
            gltf_data: GLTF数据字典
            texture_map: 纹理映射字典
            material: 材质字典
            model_folder: 模型文件夹路径
            log_callback: 日志回调函数
        &quot;&quot;&quot;
        # 确保有必要的数组
        if &apos;images&apos; not in gltf_data:
            gltf_data[&apos;images&apos;] = []
        if &apos;textures&apos; not in gltf_data:
            gltf_data[&apos;textures&apos;] = []
        if &apos;materials&apos; not in gltf_data:
            gltf_data[&apos;materials&apos;] = []
        if &apos;samplers&apos; not in gltf_data:
            gltf_data[&apos;samplers&apos;] = []
        if &apos;buffers&apos; not in gltf_data:
            gltf_data[&apos;buffers&apos;] = []
        if &apos;bufferViews&apos; not in gltf_data:
            gltf_data[&apos;bufferViews&apos;] = []

        # 创建采样器
        diffuse_sampler_index = len(gltf_data[&apos;samplers&apos;])
        gltf_data[&apos;samplers&apos;].append({
            &quot;magFilter&quot;: 9729,  # LINEAR
            &quot;minFilter&quot;: 9987,  # LINEAR_MIPMAP_LINEAR
            &quot;wrapS&quot;: 10497,     # REPEAT
            &quot;wrapT&quot;: 10497      # REPEAT
        })

        # 如果存在法线贴图，创建独立的采样器
        if &apos;normal&apos; in texture_map:
            normal_sampler_index = len(gltf_data[&apos;samplers&apos;])
            gltf_data[&apos;samplers&apos;].append({
                &quot;magFilter&quot;: 9729,  # LINEAR
                &quot;minFilter&quot;: 9987,  # LINEAR_MIPMAP_LINEAR for better quality
                &quot;wrapS&quot;: 10497,     # REPEAT
                &quot;wrapT&quot;: 10497      # REPEAT
            })
        else:
            normal_sampler_index = diffuse_sampler_index

        texture_indices = {}
        
        # 收集纹理信息
        all_texture_data = []
        texture_info_list = []
        texture_buffer_index = 0  # 初始化，稍后会更新

        if self.embed_textures:
            # 内嵌模式：加载纹理数据并准备合并到buffer中
            for tex_type, tex_path in texture_map.items():
                try:
                    texture_data = self.load_texture_data(tex_path)
                    mime_type = self.get_texture_mime_type(tex_path)
                    
                    # 检查纹理格式，如果不是glTF标准格式，记录警告但不停止
                    if mime_type not in [&apos;image/png&apos;, &apos;image/jpeg&apos;, &apos;image/webp&apos;, &apos;image/ktx2&apos;]:
                        if log_callback:
                            log_callback(f&quot;    [警告] 非标准glTF纹理格式: {os.path.basename(tex_path)} ({mime_type})&quot;)
                        # 但仍尝试使用它，因为Blender/其他工具可能支持
                    
                    # 计算当前偏移（不包括填充）
                    current_offset = sum(len(data) for data in all_texture_data)
                    
                    # 计算需要的填充字节数（4字节对齐）
                    padding_needed = (4 - (current_offset % 4)) % 4
                    
                    # 记录纹理信息（包含填充后的偏移）
                    texture_info_list.append({
                        &apos;type&apos;: tex_type,
                        &apos;path&apos;: tex_path,
                        &apos;data&apos;: texture_data,
                        &apos;mime_type&apos;: mime_type,
                        &apos;byte_offset&apos;: current_offset + padding_needed,
                        &apos;byte_length&apos;: len(texture_data)
                    })
                    
                    # 添加填充字节（如果需要）
                    if padding_needed &amp;gt; 0:
                        all_texture_data.append(b&apos;\x00&apos; * padding_needed)
                    
                    # 添加实际纹理数据
                    all_texture_data.append(texture_data)
                    
                except Exception as e:
                    if log_callback:
                        log_callback(f&quot;    [错误] 加载{tex_type}纹理失败: {str(e)}&quot;)
            
            # 合并所有纹理数据并创建新的buffer
            if all_texture_data:
                combined_data = b&apos;&apos;.join(all_texture_data)
                
                # 创建用于纹理的新buffer（buffer索引1，因为buffer 0通常是几何数据）
                texture_buffer_index = len(gltf_data[&apos;buffers&apos;])
                gltf_data[&apos;buffers&apos;].append({
                    &quot;byteLength&quot;: len(combined_data),
                    &quot;uri&quot;: f&quot;data:application/gltf-buffer;base64,{base64.b64encode(combined_data).decode(&apos;ascii&apos;)}&quot;
                })
        else:
            # 外部引用模式：只记录纹理信息，不加载数据
            for tex_type, tex_path in texture_map.items():
                try:
                    mime_type = self.get_texture_mime_type(tex_path)
                    texture_info_list.append({
                        &apos;type&apos;: tex_type,
                        &apos;path&apos;: tex_path,
                        &apos;mime_type&apos;: mime_type,
                        &apos;uri&apos;: os.path.basename(tex_path)
                    })
                except Exception as e:
                    if log_callback:
                        log_callback(f&quot;    [错误] 处理{tex_type}纹理失败: {str(e)}&quot;)

        # 处理每个纹理（内嵌模式和外部引用模式都在这里处理）
        for tex_info in texture_info_list:
            try:
                # 获取纹理的原始文件名（不含扩展名）作为名称
                texture_basename = os.path.splitext(os.path.basename(tex_info[&apos;path&apos;]))[0]

                # 根据纹理类型生成更具描述性的名称
                texture_name = f&quot;{texture_basename}_{tex_info[&apos;type&apos;]}&quot;

                # 根据是内嵌还是外部引用创建不同的结构
                if self.embed_textures:
                    # 内嵌模式：使用bufferView
                    buffer_view_index = len(gltf_data[&apos;bufferViews&apos;])
                    gltf_data[&apos;bufferViews&apos;].append({
                        &quot;buffer&quot;: texture_buffer_index,
                        &quot;byteOffset&quot;: tex_info[&apos;byte_offset&apos;],
                        &quot;byteLength&quot;: tex_info[&apos;byte_length&apos;]
                    })

                    # 创建图像（添加name属性）
                    image_index = len(gltf_data[&apos;images&apos;])
                    gltf_data[&apos;images&apos;].append({
                        &quot;name&quot;: texture_name,  # 添加名称，便于Blender等工具识别
                        &quot;bufferView&quot;: buffer_view_index,
                        &quot;mimeType&quot;: tex_info[&apos;mime_type&apos;]
                    })

                    if log_callback:
                        log_callback(f&quot;    [内嵌]{tex_info[&apos;type&apos;]}纹理: {os.path.basename(tex_info[&apos;path&apos;])} -&amp;gt; {texture_name}&quot;)
                else:
                    # 外部引用模式：直接使用uri（同样添加name属性）
                    image_index = len(gltf_data[&apos;images&apos;])
                    gltf_data[&apos;images&apos;].append({
                        &quot;name&quot;: texture_name,  # 添加名称
                        &quot;uri&quot;: tex_info[&apos;uri&apos;]
                    })

                    if log_callback:
                        log_callback(f&quot;    [外部引用]{tex_info[&apos;type&apos;]}纹理: {os.path.basename(tex_info[&apos;path&apos;])} -&amp;gt; {texture_name}&quot;)

                # 创建纹理，使用对应的采样器索引
                texture_index = len(gltf_data[&apos;textures&apos;])
                
                # 使用不同的采样器索引：diffuse用0，normal用1
                sampler_to_use = diffuse_sampler_index if tex_info[&apos;type&apos;] == &apos;diffuse&apos; else normal_sampler_index
                
                # 为纹理添加名称，方便在着色器编辑器中识别
                texture_type_label = &quot;Diffuse&quot; if tex_info[&apos;type&apos;] == &apos;diffuse&apos; else &quot;Normal&quot;
                gltf_data[&apos;textures&apos;].append({
                    &quot;name&quot;: f&quot;{texture_type_label}_Texture&quot;,
                    &quot;sampler&quot;: sampler_to_use,
                    &quot;source&quot;: image_index
                })

                texture_indices[tex_info[&apos;type&apos;]] = texture_index

            except Exception as e:
                if log_callback:
                    log_callback(f&quot;    [错误] 处理{tex_info[&apos;type&apos;]}纹理失败: {str(e)}&quot;)

        # 更新材质中的纹理索引
        if &apos;diffuse&apos; in texture_indices:
            material[&quot;pbrMetallicRoughness&quot;][&quot;baseColorTexture&quot;][&quot;index&quot;] = texture_indices[&apos;diffuse&apos;]

        if &apos;normal&apos; in texture_indices:
            material[&quot;normalTexture&quot;][&quot;index&quot;] = texture_indices[&apos;normal&apos;]

        # 添加材质
        material_index = len(gltf_data[&apos;materials&apos;])
        gltf_data[&apos;materials&apos;].append(material)

        # 更新网格图元以使用材质
        if &apos;meshes&apos; in gltf_data:
            for mesh in gltf_data[&apos;meshes&apos;]:
                if &apos;primitives&apos; in mesh:
                    for primitive in mesh[&apos;primitives&apos;]:
                        primitive[&apos;material&apos;] = material_index

        if log_callback:
            log_callback(f&quot;  [成功] 创建材质: {material[&apos;name&apos;]}&quot;)

    def _embed_texture_to_buffer(self, gltf_data: Dict, texture_data: bytes, mime_type: str):
        &quot;&quot;&quot;
        将纹理数据内嵌到GLTF buffer中
        
        注意：此方法已弃用，内嵌逻辑已在 _update_gltf_with_materials 中处理
        
        Args:
            gltf_data: GLTF数据字典
            texture_data: 纹理二进制数据
            mime_type: 纹理的MIME类型
            
        Returns:
            Buffer索引
        &quot;&quot;&quot;
        # 此方法已弃用，但为了向后兼容保留
        return 0


def process_gltf_with_textures(gltf_path: str, model_folder: str,
                             texture_keywords: Dict[str, List[str]],
                             embed_textures: bool = True,
                             texture_formats: List[str] = None,
                             log_callback=None) -&amp;gt; bool:
    &quot;&quot;&quot;
    处理GLTF文件，添加纹理材质的便捷函数

    Args:
        gltf_path: GLTF文件路径
        model_folder: 模型文件夹路径
        texture_keywords: 纹理关键字映射
        embed_textures: 是否内嵌纹理
        texture_formats: 纹理格式优先级列表
        log_callback: 日志回调函数

    Returns:
        是否成功
    &quot;&quot;&quot;
    # 创建纹理匹配器
    texture_formats = texture_formats or [&apos;png&apos;]
    texture_matcher = TextureMatcher(texture_keywords, texture_formats)

    # 创建材质处理器
    color_spaces = {&apos;diffuse&apos;: &apos;srgb&apos;, &apos;normal&apos;: &apos;linear&apos;}
    material_processor = GLTFMaterialProcessor(embed_textures, color_spaces)

    # 处理GLTF文件
    return material_processor.add_materials_to_gltf(
        gltf_path, model_folder, texture_matcher, log_callback
    )
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>程序化道路</title><link>https://fuwari.vercel.app/posts/%E7%A8%8B%E5%BA%8F%E5%8C%96%E9%81%93%E8%B7%AF/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E7%A8%8B%E5%BA%8F%E5%8C%96%E9%81%93%E8%B7%AF/</guid><pubDate>Fri, 30 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;思路初探&lt;/h1&gt;
&lt;p&gt;大概是三个路径, 两个主流的, 一个略微不主流的;&lt;/p&gt;
&lt;h2&gt;对应这三个思路, 找到的参考资料:&lt;/h2&gt;
&lt;p&gt;目前打算使用Unity自带的Spline包, 而不是自己画一个Spline. 这里有一些以前没有官方的Spline Package的实现, 但是对于Unity支持都是老版本(最晚的也是2018了):&lt;/p&gt;
&lt;h2&gt;其他: Houdini的全自定义实现&lt;/h2&gt;
&lt;p&gt;虽然没有&lt;/p&gt;
&lt;h1&gt;进一步: 有关于道路生成算法和半自动城市生成&lt;/h1&gt;
&lt;h1&gt;十字路口(交界)实现&lt;/h1&gt;
&lt;p&gt;最简单的: houdini直接用boolean取交界, 然后划分一个group. 这个group就是十字路口了; 如果要添加曲线可以使用extrude+smoothness.(卧槽轮椅啊)
然而这是Unity, 所以要像另外的办法来实现
我的思路: 先遍历出Spine的交界点, 随后根据Spine包的Width方法调取出各自Spline的宽度; 获取出以交界(各自Spine的那个交界点), 需要向前后扩张多少距离内的交界点被划分为十字路口; 还有一个平滑值, 控制十字路口的贝塞尔曲线的平滑度; 这个值越大, 以交界中心为原点扩张, 被标记为十字路口的点就越大; 这样从两个Spline道路的&quot;绝对交界&quot;, 从两个Spline交界的冗余的面积就更多; Spline的方向可以直接读取结构体的Spline Index获取.&lt;/p&gt;
&lt;h2&gt;大概思路笔记:&lt;/h2&gt;
&lt;p&gt;这个笔记是最详细的, 如果不想看后面的碎碎念, 直接看这个笔记来了解我的思路就好&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/wxy55s9tl87dup7p_image_00.jpeg&quot; alt=&quot;草稿本-72.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/wxy55s9tl87dup7p_image_01.jpeg&quot; alt=&quot;草稿本-73.jpg&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现思路:&lt;/h2&gt;
&lt;p&gt;有很大程度的部分灵感来源于这个视频: https://youtu.be/mRzGmaHEJiM?si=OrflaMPj_JUZdfin
搜索了大部分的路口的实现, 都是有一个固定的模板, 会配合出路口生成算法生成出合适的路口预制体. 在UE的官方演示黑客帝国中, 也是这么做的.
但是尼玛我要做的应该是像City Skyline那样可以全自动手动规划的PCG工具, 而不是生成算法啊! 找来找去就这又这位博主的视频, 能很详细的讲完全自定义的十字路口是如何生成的了. 大致的思路就是: 算出交汇点, 对spline曲线切边, 重新生成线段; 然后把ptnum根据单个线段的u值反转一下, 让所有的u方向都指向一个路口; 最后按照路口+这些线段, 各自组为一个组, 然后逐一处理, 直接用sweep遍历连接. 详情可以看这个总结: https://notebooklm.google.com/notebook/b7bdca60-d894-429f-966c-badcb3353452&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/wxy55s9tl87dup7p_image_02.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;在此之上进行的改进&lt;/h3&gt;
&lt;p&gt;如果按照这位博主的流程生成, 在Houdini中肯定是没有问题的. sweep, 几何拓补, fuse找交点, 过近自动焊接, 这些都有; 但是Unity可没有, 要自己慢慢搓;
还有就是博主的顺序问题. 虽然反转什么的操作确实简单又快, 但是对于可维护性(大嘘)对于纯代码的话还有另一种方式-直接上泛型+扩展属性. 维护一个虚拟的, 在Unity的Spline的数据结构上, 再维护一个被&quot;虚拟&quot;反转的泛型, 然后交给算法判断. 这样不会修改真正的点源数据, 并且同样可以进行切割计算, 和路口生成的算法. 方便后面做更多的计算.
mesh的生成可以使用Unity的Mesh库自行组装; 然后关于路口的交点判断也可以直接进行Spline遍历, 遍历为一个个折线段完毕以后, 就可以愉快的寻找交点了! 如果有三维(y)的要求, 那么直接对比一下y差值就好. 至于十字路口的组装, 这位博主用的是sweep节点来生成. 但是我们并不会重新为点进行排号, 或者说sweep本身就是一个很神奇的节点;
所以对于路口的mesh生成, 反正根据这位博主的思路, 切割段落, 对十字路口挖洞这部分都已经整理好了;
所以接下来就请出https://youtu.be/ZiHH_BvjoGk?si=DErefkb-UVCux7W4 这位博主, 他对于十字路口用的是一个扇形算法; 都有共同之处: 根据atan2对单个十字路口组的线段进行径向排序, 再依次进行连接. 因为在之前, 我们已经手动指定了每条线段的方向都要指向交点中心(详情看上面的评论), 所以径向排序非常好做, 直接某个轴的正方向(在这里我指定的是x轴的正方向), 然后取该组所有线段的xz平面方向的角度, 做简单排序, 扔进一个list即可.
随后根据这个list的顺序, 依次和中心点组成三角面并连接. 在博主之上, 还可以混进去一个贝塞尔的曲线细分连接.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/wxy55s9tl87dup7p_image_03.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里是有关于方向排序的重点. 如果不知道每个路口相对于中心点的方向会出现非常丑的拓补.  https://youtu.be/ZiHH_BvjoGk?si=lV-f1kUaz5BfpatW&amp;amp;t=663
当然这个是贝塞尔曲线的求法. 还有其他的办法, 比如基于圆来求解两个点有没有共同圆弧:
https://www.reddit.com/r/Unity3D/comments/82yed1/best_way_to_make_straight_and_curved_roads_and/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button
这个reddit讲了很多有关于两点之间, 道路可以如何处理弧线的部分.&lt;/p&gt;
</content:encoded></item><item><title>安卓15, SDK35无法正常使用RenderDoc截帧的问题</title><link>https://fuwari.vercel.app/posts/%E5%AE%89%E5%8D%9315-sdk35%E6%97%A0%E6%B3%95%E6%AD%A3%E5%B8%B8%E4%BD%BF%E7%94%A8renderdoc%E6%88%AA%E5%B8%A7%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%AE%89%E5%8D%9315-sdk35%E6%97%A0%E6%B3%95%E6%AD%A3%E5%B8%B8%E4%BD%BF%E7%94%A8renderdoc%E6%88%AA%E5%B8%A7%E7%9A%84%E9%97%AE%E9%A2%98/</guid><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;疑似还需要:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/nil6du233fso53to_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/nil6du233fso53to_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ro.force.debuggable=1 ← 关键属性&lt;/li&gt;
&lt;li&gt;ro.debuggable=1&lt;/li&gt;
&lt;li&gt;ro.secure=0&lt;/li&gt;
&lt;li&gt;ro.adb.secure=0
这里直接用用magisk resetprop就行, 主要是骗过系统层
要找一下安卓13有没有这个wrap.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;1. EGL扩展支持确认  从 /system/lib64/libEGL.so 中提取到的扩展字符串：  EGL_ANDROID_GLES_layers EGL_ANGLE_platform_angle EGL_EXT_client_extensions EGL_KHR_platform_android  关键发现：  - ✓ EGL_ANDROID_GLES_layers - 支持！这是用于GLES层注入的核心扩展  - ✓ EGL_EXT_client_extensions - 支持！允许在没有显示连接的情况下查询扩展  2. 设备信息  - 型号: TB322FC  - 主板: sun  - GPU: Adreno (Qualcomm)  - Android版本: 15 (API 35)  - ro.debuggable: 1  3. RenderDoc层已部署  /data/local/tmp/rdoc/  ├── libVkLayer_GLES_RenderDoc.so (26.5 MB)  └── wrap.sh  wrap.sh的内容：  #!/system/bin/sh  export renderdoc__replay__marker=1  # chain to asan&apos;s wrap if needed  if [ -f asan.sh ]; then    ./asan.sh &quot;$@&quot;  else    exec &quot;$@&quot;  fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是没有错的, 但是我在开发者选项中启用了GPU调试层和验证可调试应用的字节码(允许ART验证)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) PS C:\Users\Admin&amp;gt; adb shellTB322FC:/ # getprop ro.debuggable1TB322FC:/ # am force-stop com.arkgame.ftTB322FC:/ # setprop debug.renderengine.backend vulkanTB322FC:/ # setprop wrap.com.arkgame.ft RENDERDOC_DEBUG_LOG_FILE=/sdcard/rdoc.logTB322FC:/ # setprop dalvik.vm.dex2oat-flags --debuggableTB322FC:/ # pm clear com.arkgame.ftSuccessTB322FC:/ # getprop | grep wrap[debug.sys.wrapper.hook.lldcpro]: [0][debug.wrapper.hook.top.activity]: [org.renderdoc.renderdoccmd.arm64.Loader][wrap.com.arkgame.ft]: [RENDERDOC_DEBUG_LOG_FILE=/sdcard/rdoc.log]TB322FC:/ # settings put global art_verifier_verify_debuggable 0TB322FC:/ # setprop debug.renderengine.backend vulkanTB322FC:/ # setprop debug.hwui.renderer openglTB322FC:/ #
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步决定了renderdoc能否注入进程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TB322FC:/ # getprop | grep wrap[debug.sys.wrapper.hook.lldcpro]: [0][debug.wrapper.hook.top.activity]: [org.renderdoc.renderdoccmd.arm64.Loader][wrap.com.arkgame.ft]: [RENDERDOC_DEBUG_LOG_FILE=/sdcard/rdoc.log]// 比如下面这样也可以实现由wrapper接管setprop wrap.com.arkgame.ft &quot;&quot;[wrap.com.arkgame.ft]: []
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;在使用 RenderDoc 对 Android 应用 com.arkgame.ft 进行截帧时遇到以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error launching com.arkgame.ft/com.unity3d.player.TYUnityActivity for capture.RenderDoc injection failed: Timeout was reached waiting for app to start.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环境信息&lt;/h2&gt;
&lt;h2&gt;根本原因分析&lt;/h2&gt;
&lt;h3&gt;1. 应用不具备 debuggable 标志&lt;/h3&gt;
&lt;p&gt;通过 dumpsys package 检查发现应用的 flags 中不包含 DEBUGGABLE 标志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flags=[ HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着这是一个 Release 版本的应用，而 RenderDoc 默认需要注入到可调试的应用进程中。&lt;/p&gt;
&lt;h3&gt;2. Android 14+ 的安全限制&lt;/h3&gt;
&lt;p&gt;应用的 targetSdk=35 (Android 14)，该版本引入了更严格的调试和注入限制：&lt;/p&gt;
&lt;h3&gt;3. RenderDoc 注入机制&lt;/h3&gt;
&lt;p&gt;RenderDoc 通过以下方式在 Android 上工作：&lt;/p&gt;
&lt;h2&gt;解决方案实施&lt;/h2&gt;
&lt;h3&gt;步骤 1: 停止目标应用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;am force-stop com.arkgame.ft&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因: 确保应用完全停止，新的系统属性设置才能在应用重启时生效。&lt;/p&gt;
&lt;h3&gt;步骤 2: 禁用 ART 可调试验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;settings put global art_verifier_verify_debuggable 0&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因:&lt;/p&gt;
&lt;h3&gt;步骤 3: 设置图形渲染后端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;setprop debug.renderengine.backend vulkan&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因:&lt;/p&gt;
&lt;h3&gt;步骤 4: 启用 RenderDoc 调试日志&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;setprop wrap.com.arkgame.ft RENDERDOC_DEBUG_LOG_FILE=/sdcard/rdoc.log&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因:&lt;/p&gt;
&lt;h3&gt;步骤 5: 设置 Dalvik/ART 可调试标志&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;setprop dalvik.vm.dex2oat-flags --debuggable&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因:&lt;/p&gt;
&lt;h3&gt;步骤 6: 清除应用数据（可选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;pm clear com.arkgame.ft&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因:&lt;/p&gt;
&lt;h2&gt;技术原理深入解析&lt;/h2&gt;
&lt;h3&gt;Android Wrap 属性机制&lt;/h3&gt;
&lt;p&gt;Android 提供了 wrap.&amp;lt;package_name&amp;gt; 系统属性，允许：
RenderDoc 利用这个机制注入其捕获库到目标应用进程。&lt;/p&gt;
&lt;h3&gt;ART 调试验证流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;应用启动  ↓ART 检查 AndroidManifest.xml 中的 debuggable 标志  ↓验证 art_verifier_verify_debuggable 设置  ↓决定是否允许调试器附加
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过设置 art_verifier_verify_debuggable=0，我们跳过了第二步验证。&lt;/p&gt;
&lt;h3&gt;DEX2OAT 编译过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;DEX 字节码  ↓dex2oat (带 --debuggable 标志)  ↓OAT/ODEX 文件 (包含调试符号)  ↓应用运行时可被调试工具识别
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证步骤&lt;/h2&gt;
&lt;h3&gt;1. 检查系统属性是否设置成功&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;getprop | grep wrap&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[wrap.com.arkgame.ft]: [RENDERDOC_DEBUG_LOG_FILE=/sdcard/rdoc.log]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 查看 RenderDoc 日志（如果需要）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;cat /sdcard/rdoc.log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 在 RenderDoc 中重新连接&lt;/h3&gt;
&lt;h2&gt;常见问题与解决方案&lt;/h2&gt;
&lt;h3&gt;Q1: 设置后仍然失败&lt;/h3&gt;
&lt;p&gt;解决方案:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 重启 ADBD 服务adb kill-server &amp;amp;&amp;amp; adb start-server# 重启设备的 adbd（需要 root）adb shell &quot;su -c &apos;setprop ctl.restart adbd&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q2: 应用崩溃或无法启动&lt;/h3&gt;
&lt;p&gt;解决方案:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 恢复默认设置adb shell &quot;su -c &apos;settings delete global art_verifier_verify_debuggable&apos;&quot;adb shell &quot;su -c &apos;setprop dalvik.vm.dex2oat-flags \&quot;\&quot;&apos;&quot;adb shell &quot;su -c &apos;setprop wrap.com.arkgame.ft \&quot;\&quot;&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Q3: RenderDoc 版本兼容性&lt;/h3&gt;
&lt;h3&gt;Q4: 使用 &quot;Inject into Process&quot; 替代方案&lt;/h3&gt;
&lt;p&gt;如果 &quot;Launch Application&quot; 仍然失败：&lt;/p&gt;
&lt;h2&gt;安全与注意事项&lt;/h2&gt;
&lt;h3&gt;⚠️ Root 权限要求&lt;/h3&gt;
&lt;p&gt;所有 su -c 命令都需要设备已 root，并且：&lt;/p&gt;
&lt;h3&gt;⚠️ 生产环境警告&lt;/h3&gt;
&lt;p&gt;这些设置会降低系统安全性：&lt;/p&gt;
&lt;h3&gt;⚠️ 应用数据丢失&lt;/h3&gt;
&lt;p&gt;pm clear 命令会清除：&lt;/p&gt;
&lt;h2&gt;恢复默认配置&lt;/h2&gt;
&lt;p&gt;调试完成后，执行以下命令恢复系统设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 恢复 ART 验证adb shell &quot;su -c &apos;settings delete global art_verifier_verify_debuggable&apos;&quot;# 清除 wrap 属性adb shell &quot;su -c &apos;setprop wrap.com.arkgame.ft \&quot;\&quot;&apos;&quot;# 清除 dex2oat 标志adb shell &quot;su -c &apos;setprop dalvik.vm.dex2oat-flags \&quot;\&quot;&apos;&quot;# 恢复图形后端（可选）adb shell &quot;su -c &apos;setprop debug.renderengine.backend \&quot;\&quot;&apos;&quot;# 停止应用adb shell &quot;su -c &apos;am force-stop com.arkgame.ft&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;RenderDoc 在 Android 上对非 debuggable 应用的截帧需要：
这个解决方案利用了 Android 的调试基础设施，在保持应用原有行为的同时，允许 RenderDoc 进行图形调试和帧捕获。&lt;/p&gt;
&lt;h2&gt;参考资源&lt;/h2&gt;
&lt;p&gt;文档创建时间: 2025-12-24
问题状态: ✅ 已解决
适用版本: Android 7.1+ (API 25+), RenderDoc 1.24+
copilot-session-3988ecf5-f2dc-47e0-985b-159fd84d9114.md(10 KB)&lt;/p&gt;
</content:encoded></item><item><title>入门并创建一个React-TS-Vite项目</title><link>https://fuwari.vercel.app/posts/%E5%85%A5%E9%97%A8%E5%B9%B6%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AAreact-ts-vite%E9%A1%B9%E7%9B%AE/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%85%A5%E9%97%A8%E5%B9%B6%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AAreact-ts-vite%E9%A1%B9%E7%9B%AE/</guid><pubDate>Mon, 26 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;起因&lt;/h1&gt;
&lt;p&gt;大概的起因是我有一个用到了babylon js的一个模型查看器. ai给我写了一个基于qrc资源文件引用的html+js.
但是我看了一下, 对于后续开发似乎不是很友好啊, 下了一个js, 缓存到文件本地, 然后再用css和html来构建页面, 用js来做交互响应; 大致结构是这样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/rwlnen63s883kqhe_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不太对啊, 那么假如说我要自己写一个前端ui那我能怎么办呢? 虽然能展示出来, 但是对于ui构建十分不利. 每次都要开一边 python main.py 再点点点, 激活html. 这样非常不顺手. 所以干脆看一下标准的前端开发流程了. 热更新, 代码库, 框架啥的直接一把梭哈,&lt;/p&gt;
&lt;h1&gt;初步WorkFlow&lt;/h1&gt;
&lt;p&gt;使用JB的WebStorm(JB无敌这一块), 新建React-TS项目, 基于Vite模板构建;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vite（发音为 /vit/，法语中意为“快”）是一个现代化的前端构建工具，由 Vue.js 作者 尤雨溪（Evan You） 团队开发。虽然名字来自 Vue 生态，但它完全支持 React、Svelte、Lit 等多种框架，尤其在 React 开发中越来越受欢迎。
Vite模板的特点1开发阶段不打包（No Bundle in Dev）○利用现代浏览器原生支持 ES Modules（ESM）。○按需加载模块：你访问 /src/main.tsx，Vite 只转换并返回这个文件及其依赖，无需预构建整个项目。○启动速度几乎与项目大小无关（即使上万个模块，启动也只需几百毫秒）。2极速热更新（HMR）○修改一个组件，只重新编译该文件，更新速度恒定且极快。3生产环境仍打包○使用 Rollup 进行生产构建，生成高度优化的静态资源（体积小、兼容性好）. (未来还是用了使用Rust写的新方案, 用于接替Rollup)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;步骤&lt;/h1&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;p&gt;在开始之前，你需要确保电脑上安装了 Node.js。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -vnpm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能看到版本号，说明安装成功。&lt;/p&gt;
&lt;h2&gt;新建项目&lt;/h2&gt;
&lt;p&gt;使用目前最流行的脚手架工具 Vite 来创建项目（它比传统的 create-react-app 快得多）
WebStorm 会自动为你运行初始化命令。
新建了以后, WebStorm会为你自动处理有关的依赖; 随后会弹出调试界面;
然后就会这样子:
最后就是在你的终端（命令行）里输入：&lt;/p&gt;
&lt;h2&gt;插件安装&lt;/h2&gt;
&lt;p&gt;这里推荐两个插件(对于vsc来说, webstorm默认支持): ESLint 和 Prettier, 对于统一代码格式比较有用.
下面是来自ai的回答:
ESLint 和 Prettier 是前端开发中常用的两个工具，它们都用于提升代码质量和一致性，但侧重点不同：&lt;/p&gt;
&lt;h3&gt;区别&lt;/h3&gt;
&lt;h4&gt;1. ESLint&lt;/h4&gt;
&lt;p&gt;举例：ESLint 可以提示你“变量 foo 未使用”或“禁止使用 var，应使用 let 或 const”。&lt;/p&gt;
&lt;h4&gt;2. Prettier&lt;/h4&gt;
&lt;p&gt;举例：Prettier 会自动把 if (x){console.log(x)} 格式化为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (x) {  console.log(x);}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;两者如何配合使用？&lt;/h4&gt;
&lt;p&gt;虽然功能有重叠（比如都能处理缩进、引号等），但最佳实践是：
为避免冲突，通常会：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;关注点&lt;/th&gt;
&lt;th&gt;是否可自动修复&lt;/th&gt;
&lt;th&gt;典型用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ESLint&lt;/td&gt;
&lt;td&gt;代码质量、逻辑&lt;/td&gt;
&lt;td&gt;部分支持&lt;/td&gt;
&lt;td&gt;捕获 bug、强制编码规范&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prettier&lt;/td&gt;
&lt;td&gt;代码格式、风格&lt;/td&gt;
&lt;td&gt;完全支持&lt;/td&gt;
&lt;td&gt;统一代码外观，减少格式争论&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>为GitHub Copilot Cli和所有的nodejs配备本地代理服务</title><link>https://fuwari.vercel.app/posts/%E4%B8%BAgithub-copilot-cli%E5%92%8C%E6%89%80%E6%9C%89%E7%9A%84nodejs%E9%85%8D%E5%A4%87%E6%9C%AC%E5%9C%B0%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E4%B8%BAgithub-copilot-cli%E5%92%8C%E6%89%80%E6%9C%89%E7%9A%84nodejs%E9%85%8D%E5%A4%87%E6%9C%AC%E5%9C%B0%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1/</guid><pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;虽然代理软件有虚拟网卡功能, 但有时候我会使用copilot进行异步处理(比如有时候我想打游戏, 然后copilot在后面修bug); 那游戏肯定不能用TUN模式啊, 卡的批爆; 得用游戏加速器才行;
由nodejs管理的包能不能有一个全局的通用参数呢? 正好有其他项目会用nodejs, 如果自己配备了环境那滚包也不需要每次都开TUN来滚.
(插一句话, cli来自于nodejs的npm管理; 但是copilot这个插件和copilot cli不是一个东西; copilot插件在Jetbrains IDE中可以在设置里直接指定某个插件是否要使用代理; 在VS Code中指定copilot的代理则可以参阅https://docs.github.com/en/copilot/how-tos/configure-personal-settings/configure-network-settings)
正好现在新版的nodejs已经有很方便的指定代理了. 在旧版本的 Node.js 中， fetch 和 http.request 不会自动读取系统的 HTTP_PROXY 环境变量。如果想用代理，必须手动引入 undici 的 ProxyAgent 或者使用 https-proxy-agent 等第三方库.
但是Node.js 在 v24.5.0 之后引入了内置代理支持。只要开启一个“开关”，Node.js 的底层网络栈（Networking Stack）就会自动识别并使用你定义的代理服务器。
而copilot cli也加入到了nodejs新版proxy的原生支持, 详情在这个issue: https://github.com/github/copilot-cli/issues/41#issuecomment-3362444262&lt;/p&gt;
&lt;h2&gt;那么如何启用?&lt;/h2&gt;
&lt;h4&gt;第一步：设置开启开关&lt;/h4&gt;
&lt;p&gt;设置环境变量 NODE_USE_ENV_PROXY=1 或者在启动程序时使用命令行参数 --use-env-proxy。&lt;/p&gt;
&lt;h4&gt;第二步：配置代理地址&lt;/h4&gt;
&lt;p&gt;设置标准的代理环境变量：&lt;/p&gt;
&lt;h4&gt;例子&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 1. 设置开启原生代理支持export NODE_USE_ENV_PROXY=1# 2. 设置你的本地代理地址（例如 Clash 或其他服务）export HTTP_PROXY=http://127.0.0.1:7890export HTTPS_PROXY=http://127.0.0.1:7890# 3. 运行你的 Node.js 程序node app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$env:NODE_USE_ENV_PROXY=&quot;1&quot;$env:HTTP_PROXY=&quot;http://127.0.0.1:7897&quot;$env:HTTPS_PROXY=&quot;http://127.0.0.1:7890&quot;node app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是环境贡献命令, 具体操作如下, 要在每次调用copilot之前指定一下env:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/howy5sgl3e1abivv_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;少截图了一个指令, 首先要$env:NODE_USE_ENV_PROXY=&quot;1&quot;
或者直接拿这两个脚本来设置也可以. (记得要用管理员模式打开, 右键-管理员或者powershell-使用管理员运行), 这里设置的是127.0.0.1:7897
setup-nodejs-proxy.bat(1 KB)
或者pwsh版本: https://drive.google.com/file/d/13HiOt_imq6UIcI2cZG5VmpM-BFkUga6t/view?usp=drive_link
然后使用下面这个脚本进行测试, 测试nodejs能不能打开google.
test-proxy.js(3 KB)
然后就可以愉快的进行异步处理了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/howy5sgl3e1abivv_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;引用&lt;/h2&gt;
</content:encoded></item><item><title>使用RenderDoc抓取Steam游戏</title><link>https://fuwari.vercel.app/posts/%E4%BD%BF%E7%94%A8renderdoc%E6%8A%93%E5%8F%96steam%E6%B8%B8%E6%88%8F/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E4%BD%BF%E7%94%A8renderdoc%E6%8A%93%E5%8F%96steam%E6%B8%B8%E6%88%8F/</guid><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://zhuanlan.zhihu.com/p/534821939
Steam在开启RenderDoc之后无法启动，此时我们需要开启globalhook。详情请参见上面的知乎专栏。
RenderDoc 截取的帧文件（.rdc 文件）的存放位置有以下几种情况：
这些文件通常以 [应用程序名称]&lt;em&gt;[日期]&lt;/em&gt;[时间].rdc 的格式命名，例如 MyGame_2024_07_29_10_30_05.rdc。
总结来说：
建议养成习惯，在捕获到需要的帧后，立即将其另存为到你指定的项目文件夹中，这样可以避免临时文件被清理或混淆。&lt;/p&gt;
&lt;h2&gt;2025.11.11更新&lt;/h2&gt;
&lt;p&gt;Steam最近可能更新了一下启动的方式, 直接从exe或者用global hook已经不凑效了.
以前的方式是: 找出对应的游戏exe执行路径, 用RenderDoc直接Launch. 然后这个游戏的exe会直接指向Steam进行版本校验, 版本校验通过后就可以正常启动这个游戏(就是知乎里网页的那些说法)
最近我有需要对Steam的一些游戏进行截帧, 我研究了一下, Steam换了一个验证方式: 如果尝试直接从游戏路径的exe打开, 游戏一开始会正常运行, 左上角也有RenderDoc Capturing的标记, 但是马上就会闪退. 随后跳转到steam进行验证, 验证完毕之后直接再开一个新的进程实例.似乎是因为pid不一致所以在第二次启动的时候, RenderDoc就不可以正常抓帧了. 即&quot; 先能注入→立刻退出→Steam重新拉起干净进程”
这个情况似乎叫做二跳?我不知道G胖是不是故意这么搞的, 但是还是有解决办法的, 就是改为直接启动Steam, 用子进程注入识别(在Capture Options中勾选这个选项)到通过Steam启动的游戏:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/qqcog0eg6rlxkc85_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;直接选中Steam.exe的执行路径(不是快捷方式), 然后勾选&quot;捕获子进程&quot;. 接着直接点击启动 (注意, 启动之前不要留有任何steam的后台, 要进任务管理器强行杀掉所有有关steam的子进程, 直接退出steam有时候不会退出子进程).
启动之后,Steam第一次会黑屏闪退. 这时候选择&quot;在禁用浏览器沙盒的情况下尝试重新启动Steam&quot;.
启动之后就可以正常在Steam页面内启动游戏了(注意,不像之前或者知乎上说的选择游戏的路径了,这次直接在Steam商店GUI中打开即可). 如果还是不行, 可以直接从右下角的Steam徽标 右键启动就好.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后截帧保存的路径仍然在之前这篇文章开头说的那个Local那里的文件保存路径, 因为RenderDoc没变&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/qqcog0eg6rlxkc85_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后附上一张成功的图片&lt;/p&gt;
</content:encoded></item><item><title>面对安卓15新版调试失效的修复</title><link>https://fuwari.vercel.app/posts/%E9%9D%A2%E5%AF%B9%E5%AE%89%E5%8D%9315%E6%96%B0%E7%89%88%E8%B0%83%E8%AF%95%E5%A4%B1%E6%95%88%E7%9A%84%E4%BF%AE%E5%A4%8D/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E9%9D%A2%E5%AF%B9%E5%AE%89%E5%8D%9315%E6%96%B0%E7%89%88%E8%B0%83%E8%AF%95%E5%A4%B1%E6%95%88%E7%9A%84%E4%BF%AE%E5%A4%8D/</guid><pubDate>Tue, 23 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Android 平板调试配置修复报告&lt;/h1&gt;
&lt;p&gt;设备信息：
问题日期： 2025-12-23&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;在配置 Android 测试设备时遇到了两个主要问题：&lt;/p&gt;
&lt;h2&gt;问题一：设置应用闪退&lt;/h2&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;打开设置 → 开发者选项 → 应用立即崩溃并返回桌面&lt;/p&gt;
&lt;h3&gt;诊断过程&lt;/h3&gt;
&lt;p&gt;通过 adb logcat 查看崩溃日志，发现关键错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.RuntimeException: Unable to resume activity {com.android.settings/com.android.settings.SubSettings}:java.lang.RuntimeException: failed to set system property &quot;logd.logpersistd&quot; to &quot;clear&quot;at com.android.settingslib.development.AbstractLogpersistPreferenceController.updateLogpersistValues(AbstractLogpersistPreferenceController.java:179)at com.android.settings.development.LogPersistPreferenceController.updateState(LogPersistPreferenceController.java:57)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根本原因&lt;/h3&gt;
&lt;h3&gt;修复方案&lt;/h3&gt;
&lt;p&gt;使用Magisk在magisk的post-fs-data目录下 /data/adb/post-fs-data.d/99-resetprop.sh 中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 设置SELinux为Permissive模式setenforce 0# 2. 修改调试相关属性resetprop -n ro.debuggable 1# 3. 提前设置logd.logpersistd属性resetprop -n logd.logpersistd clear
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;h2&gt;问题二：RenderDoc 无法连接&lt;/h2&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;使用 RenderDoc 尝试连接 Android 设备时：&lt;/p&gt;
&lt;h3&gt;诊断过程&lt;/h3&gt;
&lt;p&gt;检查系统调试相关属性：
adb shell &quot;su -c &apos;getprop | grep -E &quot;ro.(secure|debuggable)&quot;&apos;&quot;
发现结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ro.adb.secure]: [1]  ❌[ro.debuggable]: [1]  ✅[ro.secure]: [1]      ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;根本原因&lt;/h3&gt;
&lt;p&gt;RenderDoc 工作原理：
安全属性的作用：&lt;/p&gt;
&lt;h3&gt;修复方案&lt;/h3&gt;
&lt;p&gt;更新启动脚本，添加安全属性禁用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 禁用安全限制（RenderDoc 和图形调试需要）resetprop -n ro.secure 0resetprop -n ro.adb.secure 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最终解决方案&lt;/h2&gt;
&lt;h3&gt;完整的启动脚本&lt;/h3&gt;
&lt;p&gt;文件路径：/data/adb/post-fs-data.d/99-resetprop.sh
脚本内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/system/bin/sh# ============================================# 测试机调试配置脚本# 执行时机：开机早期 post-fs-data 阶段# 用途：禁用安全限制 + 启用完整调试功能# ============================================# 1. 设置SELinux为Permissive模式#    - Enforcing: 严格模式，阻止未授权操作#    - Permissive: 宽松模式，只记录警告但不阻止setenforce 0# 2. 禁用系统安全限制（图形调试必需）#    ro.secure: 控制系统安全模式#    ro.adb.secure: 控制 ADB 安全验证resetprop -n ro.secure 0resetprop -n ro.adb.secure 0# 3. 启用调试标志#    ro.debuggable = 1 时，系统会：#    - 显示开发者选项#    - 允许更详细的调试日志#    - 启用更多调试工具resetprop -n ro.debuggable 1# 4. 预设置 logd 日志属性#    避免设置应用的开发者选项因权限问题崩溃resetprop -n logd.logpersistd clear
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行权限设置&lt;/h3&gt;
&lt;p&gt;adb shell &quot;su -c &apos;chmod 755 /data/adb/post-fs-data.d/99-resetprop.sh&apos;&quot;&lt;/p&gt;
&lt;h2&gt;技术原理说明&lt;/h2&gt;
&lt;h3&gt;post-fs-data 阶段&lt;/h3&gt;
&lt;p&gt;Android 启动过程中的关键阶段：
post-fs-data 阶段特点：&lt;/p&gt;
&lt;h3&gt;resetprop 命令&lt;/h3&gt;
&lt;p&gt;Magisk 提供的工具，用于修改系统属性：
resetprop -n &amp;lt;属性名&amp;gt; &amp;lt;值&amp;gt;&lt;/p&gt;
&lt;h3&gt;关键系统属性&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;th&gt;设置值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ro.debuggable&lt;/td&gt;
&lt;td&gt;启用系统调试功能&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;必需，开发者选项的基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ro.secure&lt;/td&gt;
&lt;td&gt;系统安全模式&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;RenderDoc 等图形调试工具需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ro.adb.secure&lt;/td&gt;
&lt;td&gt;ADB 安全验证&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;解锁完整 adb 调试功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logd.logpersistd&lt;/td&gt;
&lt;td&gt;日志持久化设置&lt;/td&gt;
&lt;td&gt;clear&lt;/td&gt;
&lt;td&gt;避免设置应用崩溃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELinux&lt;/td&gt;
&lt;td&gt;强制访问控制&lt;/td&gt;
&lt;td&gt;Permissive&lt;/td&gt;
&lt;td&gt;允许应用修改系统属性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;SELinux 模式&lt;/h3&gt;
&lt;p&gt;Enforcing（强制）： 默认模式
[ 1234.567] Type: 1400 audit(1234567890.123: 456): avc: denied { write } for pid=1234 comm=&quot;app&quot; name=&quot;property&quot; scontext=u:r:app:s0 tcontext=u:object_r:property:s0 tclass=file permissive=0
Permissive（宽松）： 修改后模式
[ 1234.567] Type: 1400 audit(1234567890.123: 456): avc: denied { write } for pid=1234 comm=&quot;app&quot; name=&quot;property&quot; scontext=u:r:app:s0 tcontext=u:object_r:property:s0 tclass=file permissive=1&lt;/p&gt;
&lt;h2&gt;验证方法&lt;/h2&gt;
&lt;h3&gt;1. 验证系统属性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查关键属性adb shell &quot;su -c &apos;getprop | grep -E \&quot;ro.(secure|debuggable)\&quot;&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预期输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ro.adb.secure]: [0][ro.debuggable]: [1][ro.secure]: [0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 验证 SELinux 状态&lt;/h3&gt;
&lt;p&gt;adb shell &quot;su -c &apos;getenforce&apos;&quot;
预期输出：
Permissive&lt;/p&gt;
&lt;h3&gt;3. 验证设置应用&lt;/h3&gt;
&lt;h3&gt;4. 验证 RenderDoc&lt;/h3&gt;
&lt;h3&gt;5. 查看启动脚本&lt;/h3&gt;
&lt;p&gt;adb shell &quot;su -c &apos;cat /data/adb/post-fs-data.d/99-resetprop.sh&apos;&quot;&lt;/p&gt;
&lt;h3&gt;6. 检查文件权限&lt;/h3&gt;
&lt;p&gt;adb shell &quot;su -c &apos;ls -la /data/adb/post-fs-data.d/&apos;&quot;
预期输出：
-rwxr-xr-x 1 root root  355 2025-12-23 21:23 99-resetprop.sh&lt;/p&gt;
&lt;h2&gt;故障排除&lt;/h2&gt;
&lt;h3&gt;如果设置应用仍然崩溃&lt;/h3&gt;
&lt;p&gt;检查是否重启了设备
验证脚本是否在开机时执行
应该输出 clear
手动执行脚本测试&lt;/p&gt;
&lt;h3&gt;如果 RenderDoc 仍然无法连接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;getprop ro.secure&apos;&quot;adb shell &quot;su -c &apos;getprop ro.adb.secure&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;都应该返回 0
重启设备确保属性生效
检查 RenderDoc 日志&lt;/p&gt;
&lt;h3&gt;如果脚本未执行&lt;/h3&gt;
&lt;p&gt;检查文件是否存在
检查 Magisk 是否正常运行
检查文件权限
应该是 -rwxr-xr-x&lt;/p&gt;
&lt;h2&gt;安全警告&lt;/h2&gt;
&lt;p&gt;⚠️ 重要提示：这些配置会显著降低系统安全性！&lt;/p&gt;
&lt;h3&gt;风险说明&lt;/h3&gt;
&lt;h3&gt;使用建议&lt;/h3&gt;
&lt;p&gt;✅ 适用场景：
❌ 不适用场景：&lt;/p&gt;
&lt;h2&gt;相关文件&lt;/h2&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;h3&gt;原脚本&lt;/h3&gt;
&lt;p&gt;https://www.reddit.com/r/Magisk/comments/174sx3n/help_magiskhidepropsconf_is_dead_so_what_is_the/&lt;/p&gt;
&lt;h3&gt;Android 属性系统&lt;/h3&gt;
&lt;h3&gt;SELinux&lt;/h3&gt;
&lt;h3&gt;RenderDoc&lt;/h3&gt;
&lt;h3&gt;Magisk&lt;/h3&gt;
&lt;h2&gt;修改历史&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;日期&lt;/th&gt;
&lt;th&gt;版本&lt;/th&gt;
&lt;th&gt;修改内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025-12-23&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;初始版本，修复设置应用崩溃和 RenderDoc 连接问题&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;文档作者： Claude (Anthropic)创建日期： 2025-12-23最后更新： 2025-12-23&lt;/p&gt;
&lt;h2&gt;附录：完整操作记录&lt;/h2&gt;
&lt;h3&gt;初始状态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查设备连接adb devices# List of devices attached# HA29QMVJ  device# 检查初始属性adb shell getprop ro.debuggable# 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;问题诊断&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看设置应用崩溃日志adb logcat -d | grep -E &quot;(AndroidRuntime|FATAL)&quot; -A 20# 发现错误# java.lang.RuntimeException: failed to set system property &quot;logd.logpersistd&quot; to &quot;clear&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决步骤&lt;/h3&gt;
&lt;p&gt;创建启动脚本
设置执行权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;cat /data/adb/post-fs-data.d/99-resetprop.sh&apos;&quot;adb shell &quot;su -c &apos;ls -la /data/adb/post-fs-data.d/&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;adb shell &quot;su -c &apos;setenforce 0&apos;&quot;adb shell &quot;su -c &apos;getenforce&apos;&quot;# Permissive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启设备&lt;/p&gt;
&lt;h3&gt;最终验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 检查所有属性adb shell &quot;su -c &apos;getprop | grep -E \&quot;ro.(secure|debuggable)\&quot;&apos;&quot;# [ro.adb.secure]: [0]# [ro.debuggable]: [1]# [ro.secure]: [0]# 检查 SELinuxadb shell &quot;su -c &apos;getenforce&apos;&quot;# Permissive# 检查 logd 属性adb shell &quot;su -c &apos;getprop logd.logpersistd&apos;&quot;# clear
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;END OF DOCUMENT&lt;/p&gt;
</content:encoded></item><item><title>RenderDoc抓帧原理</title><link>https://fuwari.vercel.app/posts/renderdoc%E6%8A%93%E5%B8%A7%E5%8E%9F%E7%90%86/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/renderdoc%E6%8A%93%E5%B8%A7%E5%8E%9F%E7%90%86/</guid><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;RenderDoc抓帧原理&lt;/p&gt;
&lt;h1&gt;名词说明&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RenderDocUI、Host、qrenderdoc&lt;/th&gt;
&lt;th&gt;运行在PC上的RenderDoc客户端&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HookTarget&lt;/td&gt;
&lt;td&gt;Record进程 - 需要捕获图形指令的应用&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReplayAPKrenderdoccmdorg.renderdoc.renderdoccmd.arm64&lt;/td&gt;
&lt;td&gt;Replay进程 - 执行指令重放的应用&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RemoteServer&lt;/td&gt;
&lt;td&gt;Replay进程创建的一个Server线程，与Host进行通信&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TargetControlServer&lt;/td&gt;
&lt;td&gt;Record进程创建的一个Server线程，与Host进行通信&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rdc文件&lt;/td&gt;
&lt;td&gt;Capture过程中捕获的资源、渲染相关的指令信息，还包括缩略图等其他信息&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;抓帧流程&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/dlslk40lp0fsl9sc_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;核心模块&lt;/h1&gt;
&lt;h2&gt;Hooks - Android GLES Layers&lt;/h2&gt;
&lt;h3&gt;什么是GLES Layers？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/dlslk40lp0fsl9sc_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如何制作一个GLES Layer&lt;/h3&gt;
&lt;p&gt;编译生成一个so文件，需要导出两个符号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef void* (*PFNEGLGETNEXTLAYERPROCADDRESSPROC)(void*, const char*);void* AndroidGLESLayer_Initialize(void* layer_id, PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address))typedef __eglMustCastToProperFunctionPointerType EGLFuncPointer;void* AndroidGLESLayer_GetProcAddress(const char *funcName, EGLFuncPointer next)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RenderDoc中的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef __eglMustCastToProperFunctionPointerType(EGLAPIENTRY *PFNEGLGETNEXTLAYERPROCADDRESSPROC)(void *, const char *funcName);HOOK_EXPORT void AndroidGLESLayer_Initialize(void *layer_id,PFNEGLGETNEXTLAYERPROCADDRESSPROC next_gpa){    RDCLOG(&quot;Initialising Android GLES layer with ID %p&quot;, layer_id);    // as a hook callback this is only called while capturing    RDCASSERT(!RenderDoc::Inst().IsReplayApp());    // populate EGL dispatch table with the next layer&apos;s function pointers. Fetch all &apos;hooked&apos; and    // non-hooked functions    #define EGL_FETCH(func, isext, replayrequired)                                 \    EGL.func = (CONCAT(PFN_egl, func))next_gpa(layer_id, &quot;egl&quot; STRINGIZE(func)); \    if(!EGL.func)                                                                \    RDCWARN(&quot;Couldn&apos;t fetch function pointer for egl&quot; STRINGIZE(func));    EGL_HOOKED_SYMBOLS(EGL_FETCH)        EGL_NONHOOKED_SYMBOLS(EGL_FETCH)        #undef EGL_FETCH        // populate GL dispatch table with the next layer&apos;s function pointers        GL.PopulateWithCallback(        [layer_id, next_gpa](const char *f) { return (void *)next_gpa(layer_id, f); });}HOOK_EXPORT void *AndroidGLESLayer_GetProcAddress(const char *funcName,__eglMustCastToProperFunctionPointerType next){    // return our egl hooks    #define GPA_FUNCTION(name, isext, replayrequired) \    if(!strcmp(funcName, &quot;egl&quot; STRINGIZE(name)))    \    return (void *)&amp;amp;CONCAT(egl, CONCAT(name, _renderdoc_hooked));    EGL_HOOKED_SYMBOLS(GPA_FUNCTION)    #undef GPA_FUNCTION    // otherwise, consult our database of hooks    // Android GLES layer spec expects us to return next unmodified for functions we don&apos;t support    return HookedGetProcAddress(funcName, (void *)next);}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据传入的next_gpa函数生成两个DispatchTable：EGL和GL，即EGL和GL相关的两个函数表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// We need to disable clang-format since this struct is programmatically parsed// clang-format offstruct GLDispatchTable{// This function can be used to populate the dispatch table, fully or partiall. Any NULL function// will be passed to the callback to get a pointervoid PopulateWithCallback(PlatformGetProcAddr lookupFunc);// These functions are called after fully populating the hookset, to emulate functions that are// one way or another unsupported but we can software emulate them and want to assume their// presence.void EmulateUnsupportedFunctions();void EmulateRequiredExtensions();void DriverForEmulation(WrappedOpenGL *driver);// first we list all the core functions. 1.1 functions are separate under &apos;dllexport&apos; for// different handling on windows. Extensions come after.// Any Core functions that are semantically identical to extension variants are listed as// &apos;aliases&apos; such that if the &apos;alias&apos; is requested via *GetProcAddress, the core function// will be returned and used.PFNGLBINDTEXTUREPROC glBindTexture;PFNGLBLENDFUNCPROC glBlendFunc;PFNGLCLEARPROC glClear;PFNGLCLEARCOLORPROC glClearColor;PFNGLCLEARDEPTHPROC glClearDepth;PFNGLCLEARSTENCILPROC glClearStencil// .......................}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回funcName对应的Hook后的函数地址，以eglDestroyContext举例，首先renderdoc定义了该函数的hook版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HOOK_EXPORT EGLBoolean EGLAPIENTRY eglDestroyContext_renderdoc_hooked(EGLDisplay dpy, EGLContext ctx){    if(RenderDoc::Inst().IsReplayApp())    {        if(!EGL.DestroyContext)            EGL.PopulateForReplay();        return EGL.DestroyContext(dpy, ctx);    }    EnsureRealLibraryLoaded();    eglhook.driver.SetDriverType(eglhook.activeAPI);    {        SCOPED_LOCK(glLock);        eglhook.driver.DeleteContext(ctx);        eglhook.contexts.erase(ctx);    }    return EGL.DestroyContext(dpy, ctx);}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AndroidGLESLayer_GetProcAddress(&quot;eglDestroyContext&quot;, next)就会返回eglDestroyContext_renderdoc_hooked的地址&lt;/p&gt;
&lt;h3&gt;GLES Layer的使用&lt;/h3&gt;
&lt;h4&gt;Layer的推送&lt;/h4&gt;
&lt;h4&gt;Layer的启用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;adb.exe &apos;-s YPRKYH99RGVCEEXO shell settings put global enable_gpu_debug_layers 1&apos;    adb.exe &apos;-s YPRKYH99RGVCEEXO shell settings put global gpu_debug_app com.glumes.opengl_tutorial_practice&apos;    adb.exe &apos;-s YPRKYH99RGVCEEXO shell settings put global gpu_debug_layer_app org.renderdoc.renderdoccmd.arm64&apos;    adb.exe &apos;-s YPRKYH99RGVCEEXO shell settings put global gpu_debug_layers VK_LAYER_RENDERDOC_Capture&apos;    adb.exe &apos;-s YPRKYH99RGVCEEXO shell settings put global gpu_debug_layers_gles libVkLayer_GLES_RenderDoc.so&apos;    adb.exe &apos;-s YPRKYH99RGVCEEXO shell setprop debug.rdoc.IGNORE_LAYERS 0&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Record &amp;amp; Replay&lt;/h2&gt;
&lt;h3&gt;一个朴素的实现&lt;/h3&gt;
&lt;p&gt;Hook上目标应用后序列化所有的GL调用，重放时再依序执行。
存在的问题&lt;/p&gt;
&lt;h3&gt;RenderDoc的优化&lt;/h3&gt;
&lt;h4&gt;录制引入的几个概念&lt;/h4&gt;
&lt;h5&gt;概念1：BackgroundCapturing和ActiveCapturing&lt;/h5&gt;
&lt;p&gt;优化1：对于BackgroundCapturing状态下的资源更新，不需要保存，只需要将该资源标记为dirty，因为我们只需要获取目标帧开始前对应资源的内容&lt;/p&gt;
&lt;h5&gt;概念2：资源的FrameReferrenceType&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// In what way (read, write, etc) was a resource referenced in a frame -// used to determine if initial contents are needed and to what degree.// These values are used both as states (representing the cumulative previous// accesses to the resource), and state transitions (access by a single// command, modifying the state). This state machine is illustrated below,// with states represented in caps, and transitions in lower case.////         +------------------ NONE -----------------------------+//         |                    |                                |//        read            partialWrite                     completeWrite//         |                    |                                |//         V                    V                                V//       READ             PARTIAL_WRITE --completeWrite--&amp;gt; COMPLETE_WRITE//         |                    |//         |                  read//       write                  |//         |                    V//         |            WRITE_BEFORE_READ//         V                    |//  READ_BEFORE_WRITE &amp;lt;--write--+//// Note://  * All resources begin implicitly in the None state.//  * The transitions labeled &quot;write&quot; correspond to either PartialWrite or//    CompleteWrite (e.g. in the READ state, either a PartialWrite or a//    CompleteWrite moves to the READ_BEFORE_WRITE state).//  * The state transitions for ReadBeforeWrite are simply the composition of//    the transition for read, followed by the transition for write (e.g.//    ReadBeforeWrite moves from NONE state to READBEFOREWRITE state);//    similarly, the state transitions for WriteBeforeRead are the composition//    of the transition for write, followed by the transition for read.//  * All other transitions (excluding ReadBeforeWrite and WriteBeforeRead)//    that are not explicitly shown leave the state unchanged (e.g. a read in//    the COMPLETE_WRITE state remains in the COMPLETE_WRITE state).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优化2：对于目标帧没有访问到的资源，不需要序列化&lt;/p&gt;
&lt;h5&gt;概念3：Persistent资源的Postpone初始化机制&lt;/h5&gt;
&lt;p&gt;优化3：对于Persistent资源，也就是该资源很久没有更新过了，那么大概率抓取的目标帧也不会对该资源进行写入，那么目标帧开始前不需要对该资源进行备份，而是延迟到目标帧结束后，再按需进行资源的序列化。（按需指的是目标帧是否真正访问了该资源，没有访问就没必要序列化）&lt;/p&gt;
&lt;h4&gt;录制的流程&lt;/h4&gt;
&lt;h5&gt;StartFrameCapture&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 清空m_FrameReferencedResources，如果某个资源被写入了，标记为dirtytemplate &amp;lt;typename Configuration&amp;gt;void ResourceManager&amp;lt;Configuration&amp;gt;::ClearReferencedResources(){    SCOPED_LOCK_OPTIONAL(m_Lock, m_Capturing);    for(auto it = m_FrameReferencedResources.begin(); it != m_FrameReferencedResources.end(); ++it)        {            RecordType *record = GetResourceRecord(it-&amp;gt;first);            if(record)            {                if(IncludesWrite(it-&amp;gt;second))                    MarkDirtyResource(it-&amp;gt;first);                record-&amp;gt;Delete(this);            }        }    m_FrameReferencedResources.clear();}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void GLResourceManager::ContextPrepare_InitialState(GLResource res){    GLInitialContents initContents;    initContents.type = res.Namespace;    ResourceId id = GetID(res);    if(res.Namespace == eResBuffer)    {        // get the length of the buffer        uint32_t length = 4;        GL.glGetNamedBufferParameterivEXT(res.name, eGL_BUFFER_SIZE, (GLint *)&amp;amp;length);        // save old bindings        GLuint oldbuf1 = 0, oldbuf2 = 0;        GL.glGetIntegerv(eGL_COPY_READ_BUFFER_BINDING, (GLint *)&amp;amp;oldbuf1);        GL.glGetIntegerv(eGL_COPY_WRITE_BUFFER_BINDING, (GLint *)&amp;amp;oldbuf2);        // create a new buffer big enough to hold the contents        GLuint buf = 0;        GL.glGenBuffers(1, &amp;amp;buf);        GL.glBindBuffer(eGL_COPY_WRITE_BUFFER, buf);        GL.glNamedBufferDataEXT(buf, (GLsizeiptr)RDCMAX(length, 4U), NULL, eGL_STATIC_READ);        // bind the live buffer for copying        GL.glBindBuffer(eGL_COPY_READ_BUFFER, res.name);        // do the actual copy        if(length &amp;gt; 0)            GL.glCopyBufferSubData(eGL_COPY_READ_BUFFER, eGL_COPY_WRITE_BUFFER, 0, 0, (GLsizeiptr)length);        // workaround for some drivers - mapping/unmapping here seems to help avoid problems mapping        // later.        GL.glMapNamedBufferEXT(buf, eGL_READ_ONLY);        GL.glUnmapNamedBufferEXT(buf);        // restore old bindings        GL.glBindBuffer(eGL_COPY_READ_BUFFER, oldbuf1);        GL.glBindBuffer(eGL_COPY_WRITE_BUFFER, oldbuf2);        initContents.resource = GLResource(res.ContextShareGroup, eResBuffer, buf);        initContents.bufferLength = length;    }    else if(res.Namespace == eResProgram)    {        // ...    }    // else ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;录制过程中&lt;/h5&gt;
&lt;p&gt;以glBindBuffer举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void WrappedOpenGL::glBindBuffer(GLenum target, GLuint buffer){    SERIALISE_TIME_CALL(GL.glBindBuffer(target, buffer)); // 真正的GL指令调用    ContextData &amp;amp;cd = GetCtxData();    size_t idx = BufferIdx(target); // 获取到当前逻辑Context中对应target槽位    if(IsActiveCapturing(m_State)) // 如果是处于ActiveCapturing状态下    {        Chunk *chunk = NULL;        if(buffer == 0)        {            cd.m_BufferRecord[idx] = NULL;        }        else        {            // 每个资源都对应一个ResourceRecord对象，这里是获取到新绑定的资源对应的ResourceRecord            cd.m_BufferRecord[idx] = GetResourceManager()-&amp;gt;GetResourceRecord(BufferRes(GetCtx(), buffer));            if(cd.m_BufferRecord[idx] == NULL)            {                RDCERR(&quot;Called glBindBuffer with unrecognised or deleted buffer&quot;);                return;            }        }        {            USE_SCRATCH_SERIALISER();            SCOPED_SERIALISE_CHUNK(gl_CurChunk);            Serialise_glBindBuffer(ser, target, buffer); // 序列化和反序列化都是用这个函数            if(cd.m_BufferRecord[idx])                cd.m_BufferRecord[idx]-&amp;gt;datatype = target;            chunk = scope.Get();        }        if(buffer)        {            FrameRefType refType = eFrameRef_Read;            // these targets write to the buffer            if(target == eGL_ATOMIC_COUNTER_BUFFER || target == eGL_COPY_WRITE_BUFFER ||                target == eGL_PIXEL_PACK_BUFFER || target == eGL_SHADER_STORAGE_BUFFER ||                target == eGL_TRANSFORM_FEEDBACK_BUFFER)                refType = eFrameRef_ReadBeforeWrite;            // 这里就是前面提到的，更新资源的引用状态            GetResourceManager()-&amp;gt;MarkResourceFrameReferenced(cd.m_BufferRecord[idx]-&amp;gt;GetResourceID(),                refType);        }        // binding this buffer mutates VAO state, mark it as written.        if(target == eGL_ELEMENT_ARRAY_BUFFER)        {            GLResourceRecord *varecord = cd.m_VertexArrayRecord;            if(varecord)                GetResourceManager()-&amp;gt;MarkVAOReferenced(varecord-&amp;gt;Resource, eFrameRef_ReadBeforeWrite);        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;EndFrameCapture&lt;/h5&gt;
&lt;p&gt;遍历m_FrameReferencedResources， 收集每个资源对应的ResourceRecord对象中记录的指令序列，比如某个纹理资源的创建指令
序列化StartFrameCapture.PrepareInitialContents过程备份的资源数据，注意这里也只会序列化m_FrameReferencedResources中的资源。
同样以buffer举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename SerialiserType&amp;gt;bool GLResourceManager::Serialise_InitialState(SerialiserType &amp;amp;ser, ResourceId id,GLResourceRecord *record,const GLInitialContents *initial){    m_State = m_Driver-&amp;gt;GetState();    GLInitialContents initContents;    if(initial)        initContents = *initial;    SERIALISE_ELEMENT(id).TypedAs(&quot;GLResource&quot;_lit).Important();    SERIALISE_ELEMENT_LOCAL(Type, initial-&amp;gt;type);    if(IsReplayingAndReading())    {        m_Driver-&amp;gt;AddResourceCurChunk(id);    }    if(Type == eResBuffer)    {        GLResource mappedBuffer = GLResource(MakeNullResource);        uint32_t BufferContentsSize = 0;        byte *BufferContents = NULL;        if(ser.IsWriting())        {            mappedBuffer = initial-&amp;gt;resource;            BufferContentsSize = initial-&amp;gt;bufferLength;            BufferContents = (byte *)GL.glMapNamedBufferEXT(mappedBuffer.name, eGL_READ_ONLY);            if(!BufferContents)                RDCERR(&quot;Couldn&apos;t map initial contents buffer for readback!&quot;);        }        // Serialise this separately so that it can be used on reading to prepare the upload memory        SERIALISE_ELEMENT(BufferContentsSize);        // not using SERIALISE_ELEMENT_ARRAY so we can deliberately avoid allocation - we serialise        // directly into upload memory        ser.Serialise(&quot;BufferContents&quot;_lit, BufferContents, BufferContentsSize, SerialiserFlags::NoFlags)            .Important();        if(mappedBuffer.name)            GL.glUnmapNamedBufferEXT(mappedBuffer.name);        SERIALISE_CHECK_READ_ERRORS();    }    // else ...}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;重放&lt;/h4&gt;
&lt;p&gt;The replay process is ostensibly simple, but as with the capturing the devil is in the details.&lt;/p&gt;
&lt;h5&gt;ActionList&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;RenderDoc then does an initial pass over the captured frame. This allows us to build up a list of all the actions, analyse dependencies and check which resources are used at each action for read, write, and so on. An internal tree is built up similar to what you see in the Event Browser &amp;amp; API Inspector, as well as a linked list with the linear sequence of actions, since both representations are useful for iterating over the frame.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/dlslk40lp0fsl9sc_image_02.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum class ActionFlags : uint32_t{NoFlags = 0x0000,// typesClear = 0x0001,Drawcall = 0x0002,Dispatch = 0x0004,MeshDispatch = 0x0008,CmdList = 0x0010,SetMarker = 0x0020,PushMarker = 0x0040,PopMarker = 0x0080,Present = 0x0100,MultiAction = 0x0200,Copy = 0x0400,Resolve = 0x0800,GenMips = 0x1000,PassBoundary = 0x2000,DispatchRay = 0x4000,BuildAccStruct = 0x8000,// flagsIndexed = 0x010000,Instanced = 0x020000,Auto = 0x040000,Indirect = 0x080000,ClearColor = 0x100000,ClearDepthStencil = 0x200000,BeginPass = 0x400000,EndPass = 0x800000,CommandBufferBoundary = 0x1000000,};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Effect+RenderDoc需要/可以做些什么&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;void WrappedOpenGL::SwapBuffers(WindowingSystem winSystem, void *windowHandle){    if(IsBackgroundCapturing(m_State))        RenderDoc::Inst().Tick();    // don&apos;t do anything if no context is active.    if(m_ActiveContexts[Threading::GetCurrentID()].ctx == NULL)    {        m_NoCtxFrames++;        if(m_NoCtxFrames == 100)        {            RDCERR(                &quot;Seen 100 frames with no context current. RenderDoc requires a context to be current &quot;                &quot;during the call to SwapBuffers to display its overlay and start/stop captures on &quot;                &quot;default keys.\nIf your GL use is elsewhere, consider using the in-application API to &quot;                &quot;trigger captures manually&quot;);        }        return;    }    m_NoCtxFrames = 0;    m_FrameCounter++;    // first present becomes frame #1, this function is at the end of the frame    ContextData &amp;amp;ctxdata = GetCtxData();    // ...}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Renderdoc DirectX ByteCode 转 HLSL 插件</title><link>https://fuwari.vercel.app/posts/renderdoc-directx-bytecode-%E8%BD%AC-hlsl-%E6%8F%92%E4%BB%B6/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/renderdoc-directx-bytecode-%E8%BD%AC-hlsl-%E6%8F%92%E4%BB%B6/</guid><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;有了这个插件以后就可以在基于DirectX的rdc文件进行实时调试了, 对于分析着色器结构很有帮助. renderdoc虽然可以在view阶段看到Frag部分的着色器但是不能实时编辑看到效果. 如果有了这个插件, 编辑Frag部分就能在Texture Viewer中实时看到效果.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原文, 其实讲的已经很详细了, 我就不赘述了. 有一点要注意, 如下: https://zhuanlan.zhihu.com/p/649925129
插件基于的原地址是mod常用的3Dmigoto: https://github.com/bo3b/3Dmigoto/releases
在编辑RenderDoc的ShaderViewer中的Command时, 注意在RenderDoc中填写Command Line {input_file} {output_file}的时候中间的空格, 不然会报错!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/psuvnxn9r9q5se84_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/psuvnxn9r9q5se84_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;附: 可用的资源(其实就是只有原版能用)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/psuvnxn9r9q5se84_image_02.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果依赖有问题的话可以尝试用Dependencies这个项目进行依赖分析. 我猜大概率是缺少了dx3d的dll导致的依赖错误.
hlsl_decompiler_wrapper.bat(1 KB)
我用的是官方的版本, 不是那个fork的版本.&lt;/p&gt;
</content:encoded></item><item><title>Raw Depth, Linear Depth, Normalized Depth(归一化深度)</title><link>https://fuwari.vercel.app/posts/raw-depth-linear-depth-normalized-depth%E5%BD%92%E4%B8%80%E5%8C%96%E6%B7%B1%E5%BA%A6/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/raw-depth-linear-depth-normalized-depth%E5%BD%92%E4%B8%80%E5%8C%96%E6%B7%B1%E5%BA%A6/</guid><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在为正交相机的shader做深度适配的时候遇到了这么一段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float RawDepthToLinear(float rawDepth)  {      #if _ORTHO_ON          float persp = LinearEyeDepth(rawDepth, _ZBufferParams);          float orthoLinearDepth = _ProjectionParams.x &amp;gt; 0 ? rawDepth : 1 - rawDepth;          return lerp(_ProjectionParams.y, _ProjectionParams.z, orthoLinearDepth);      #else          return LinearEyeDepth(rawDepth, _ZBufferParams);      #endif  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;作用&lt;/h1&gt;
&lt;p&gt;是一个在3D图形渲染中常用的深度值转换函数，主要用于将原始深度缓冲区中的非线性深度值转换为线性深度值。这个转换在实现各种后期处理效果、阴影计算和深度相关技术时非常重要。&lt;/p&gt;
&lt;h2&gt;函数功能&lt;/h2&gt;
&lt;p&gt;该函数接收一个原始深度值，根据当前使用的投影类型（正交投影或透视投影）返回对应的线性深度值。&lt;/p&gt;
&lt;h2&gt;工作原理&lt;/h2&gt;
&lt;p&gt;函数通过条件编译指令 #if _ORTHO_ON 区分两种投影模式：&lt;/p&gt;
&lt;h3&gt;1. 正交投影模式（当定义了 _ORTHO_ON 时）&lt;/h3&gt;
&lt;h3&gt;2. 透视投影模式（默认情况）&lt;/h3&gt;
&lt;h2&gt;变量说明&lt;/h2&gt;
&lt;p&gt;这个函数在游戏引擎（如Unity）的着色器代码中很常见，用于确保在不同投影类型下都能获得正确的线性深度值，以便进行精确的深度计算和视觉效果处理。
越靠近相机, 越接近1&lt;/p&gt;
&lt;h1&gt;原始深度值、线性深度值和归一化深度值的关系&lt;/h1&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;h3&gt;原始深度值 (Raw Depth)&lt;/h3&gt;
&lt;h3&gt;线性深度值 (Linear Depth)&lt;/h3&gt;
&lt;h3&gt;归一化深度值 (Normalized Device Coordinate Depth)&lt;/h3&gt;
&lt;h2&gt;三者之间的关系&lt;/h2&gt;
&lt;h2&gt;为什么需要转换？&lt;/h2&gt;
&lt;p&gt;在您提供的代码中，正是为了适应这两种不同的投影模式（透视/正交）而提供了不同的深度转换路径，确保在任何投影方式下都能获得正确的线性深度值。&lt;/p&gt;
</content:encoded></item><item><title>Conda常用命令</title><link>https://fuwari.vercel.app/posts/conda%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/conda%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</guid><pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;🧪 一、基础信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;conda --version          # 查看 Conda 版本conda info               # 查看 Conda 和当前环境详细信息conda config --show      # 查看所有配置项
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🌐 二、环境管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建环境conda create -n myenv python=3.9           # 创建指定 Python 版本的环境conda create -n myenv numpy pandas         # 创建时安装包conda env create -f environment.yml        # 从 YAML 文件创建环境# 激活/退出环境conda activate myenv      # 激活环境（Windows/Linux/macOS 通用）conda deactivate          # 退出当前环境# 查看环境conda env list            # 列出所有环境（带 * 的是当前激活的）conda info --envs         # 同上# 删除环境conda remove -n myenv --all   # 删除整个环境# 克隆环境conda create -n new_env --clone old_env# 导出环境conda env export &amp;gt; environment.yml                 # 完整导出（含平台信息）conda env export --no-builds | grep -v &quot;^prefix:&quot; &amp;gt; environment.yml  # 跨平台友好版
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;📦 三、包管理（在当前激活环境中操作）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装包conda install numpyconda install numpy=1.21            # 指定版本conda install -c conda-forge scipy  # 从指定 channel 安装# 查看包conda list                # 列出当前环境所有包conda list numpy          # 查看特定包是否安装及版本# 更新包conda update numpy        # 更新单个包conda update --all        # 更新所有包conda update conda        # 更新 Conda 自身# 删除包conda remove numpy# 搜索包conda search numpyconda search &quot;numpy&amp;gt;=1.20&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 提示：在 Conda 环境中也可以用 pip install，但优先用 conda install，避免依赖冲突。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;🔄 四、环境迁移与分享&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 导出（见上文）conda env export &amp;gt; environment.yml# 从文件创建环境conda env create -f environment.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🧹 五、清理缓存（释放磁盘空间）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;conda clean -a            # 删除所有未使用的包、tarballs、索引缓存等conda clean --packages    # 仅删除未使用的包conda clean --tarballs    # 仅删除下载的压缩包conda clean -i            # 清除索引缓存（切换镜像源后建议执行）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;🪞 六、镜像源管理（国内加速推荐）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 添加清华源（推荐）conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/mainconda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/freeconda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge# 显示安装来源conda config --set show_channel_urls yes# 查看当前 channelsconda config --show channels# 恢复默认源conda config --remove-key channels
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;✅ 小贴士&lt;/h3&gt;
</content:encoded></item><item><title>MiniConda:在IDE中也能激活conda环境</title><link>https://fuwari.vercel.app/posts/miniconda_%E5%9C%A8ide%E4%B8%AD%E4%B9%9F%E8%83%BD%E6%BF%80%E6%B4%BBconda%E7%8E%AF%E5%A2%83/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/miniconda_%E5%9C%A8ide%E4%B8%AD%E4%B9%9F%E8%83%BD%E6%BF%80%E6%B4%BBconda%E7%8E%AF%E5%A2%83/</guid><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最近在研究编译RenderDoc的时候,发现RenderDoc还在使用Python3.6环境.所以要用环境管理.venv不能管理python版本,相比之下,还是(mini)conda比较顺手,不仅可以进行包管理还可以自由切换python版本.唯一的问题就是在一般情况下,vscode的终端是不会激活conda的.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体情况: conda在vscode的powershell中激活后,使用python --version仍然映射的是系统的版本;而在win+R唤起cmd之后,在这里激活并使用python --version之后反而会可以激活到conda中指定的python版本.
这就要讲到conda的hook机制了.&lt;/p&gt;
&lt;h3&gt;问题分析：为什么在VS Code的PowerShell中 python --version 不生效？&lt;/h3&gt;
&lt;p&gt;当在命令行中执行 conda activate your_env 时，Conda会做两件主要的事情：
为什么在VS Code的PowerShell中可能不生效，而在CMD中可以？
所以,VS Code的Python扩展不完全依赖于终端的 PATH。
对于在VS Code中运行和调试Python脚本，最可靠的方法不是仅仅依赖于终端中激活的环境。VS Code的Python扩展提供了一个更强大的机制，允许您明确选择用于当前工作区或文件的Python解释器。一旦您选择了，VS Code的Linter、调试器、运行器以及其他Python相关功能都会使用这个指定的解释器，而不管您的集成终端中 python --version 显示什么。&lt;/p&gt;
&lt;h3&gt;解决方案：在VS Code中正确配置Conda虚拟环境&lt;/h3&gt;
&lt;h4&gt;步骤一：确保Conda在PowerShell中正确初始化 (可选，但推荐)&lt;/h4&gt;
&lt;p&gt;这一步是为了让您的VS Code集成终端能够正确地激活Conda环境，即使这不直接影响VS Code的调试器。&lt;/p&gt;
&lt;h4&gt;步骤二：在VS Code中选择正确的Python解释器 (最关键的一步)&lt;/h4&gt;
&lt;p&gt;这是确保VS Code的Python扩展使用您的Conda环境的关键。&lt;/p&gt;
&lt;h4&gt;步骤三：验证和使用&lt;/h4&gt;
&lt;h4&gt;如果VS Code终端报错没有权限...&lt;/h4&gt;
&lt;p&gt;在第一步的时候,如果尝试使用conda init powershell让conda在初始化powershell的时候,有概率会出现报错.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;. : 无法加载文件 D:\Documents\WindowsPowerShell\profile.ps1，因为在此系统上禁止运行脚本。有关详细信息，请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。所在位置 行:1 字符: 3+ . &apos;D:\Documents\WindowsPowerShell\profile.ps1&apos;+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    + CategoryInfo          : SecurityError: (:) []，PSSecurityException    + FullyQualifiedErrorId : UnauthorizedAccess
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UnauthorizedAccess是关键. 一般情况下,为了防止终端在后台静默执行,会禁用其他脚本. PowerShell的执行策略是一种安全功能，旨在防止恶意脚本在计算机上运行。它控制着PowerShell加载和运行脚本的能力。默认情况下，在Windows客户端操作系统上，执行策略通常设置为 Restricted（受限），这意味着不允许运行任何PowerShell脚本，包括Conda的 profile.ps1 文件。
当运行 conda init powershell 命令时，Conda会尝试修改或创建PowerShell配置文件（通常是 profile.ps1），以便在每次启动PowerShell时自动加载Conda的环境初始化脚本。但是，如果执行策略设置为 Restricted，PowerShell就会拒绝加载这个 profile.ps1 文件，从而阻止Conda的初始化逻辑运行。
常见的权限类型有:
在这里,为了让Conda能在VS Code中执行环境初始化, 推荐的策略是 RemoteSigned，并将其作用域设置为 CurrentUser（当前用户）。 这样既能允许	运行自己的脚本，又能对从互联网下载的脚本提供一定的保护。
操作步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Get-ExecutionPolicy -List
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将显示所有作用域（MachinePolicy, UserPolicy, Process, CurrentUser, LocalMachine）下的执行策略。您会看到 CurrentUser 或 LocalMachine 可能是 Restricted。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，当您在VS Code的PowerShell终端中运行 conda activate your_env 时，profile.ps1 应该能够被成功加载，Conda的环境初始化脚本也会随之运行。这意味着：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/dsf8s7736vfxyz04_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;请注意： 即使在终端中 python --version 仍然显示系统版本（这在某些复杂的PATH配置下偶尔会发生），只要按照之前提供的步骤，在VS Code中通过 Python: Select Interpreter 明确选择了Conda环境的Python解释器，VS Code的运行和调试功能仍然会使用您指定的Conda环境。解决执行策略问题主要是为了让终端环境也能够正确地工作，提供一致的体验。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;这个现象是VS Code集成终端环境配置的一个常见“陷阱”，但它并不会阻碍您在VS Code中利用Conda虚拟环境。最核心的解决方案是：通过 Python: Select Interpreter 命令明确告诉VS Code您想要使用哪个Conda环境的Python解释器。 这样，VS Code的调试器、Linter、运行器等所有Python相关工具都会使用这个指定的版本，完美地支持您执行和调试旧版本Python脚本的需求。
https://github.com/eliemichel/MapsModelsImporter/issues/108&lt;/p&gt;
</content:encoded></item><item><title>利用Openssl自签名证书实现gitea https访问</title><link>https://fuwari.vercel.app/posts/%E5%88%A9%E7%94%A8openssl%E8%87%AA%E7%AD%BE%E5%90%8D%E8%AF%81%E4%B9%A6%E5%AE%9E%E7%8E%B0gitea-https%E8%AE%BF%E9%97%AE/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%88%A9%E7%94%A8openssl%E8%87%AA%E7%AD%BE%E5%90%8D%E8%AF%81%E4%B9%A6%E5%AE%9E%E7%8E%B0gitea-https%E8%AE%BF%E9%97%AE/</guid><pubDate>Tue, 21 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;前言：这是我在看http的坏处偶然看到的。http会直接使用明文传输，容易受到中间人攻击而且容易被人扒底裤。所以修改为https就被提上了日程。当时想了两个办法，一个是用nginx配合lets encrypt来自动化续签证书/Gitea使用ACAM实现证书全自动托管，一个是CloudFlare自动转发。但是折腾了一番发现80和443都被封掉了，第一个方法几乎不可行；我的域名也不是CF买的（不能完全托管，不是橙色的小云图标）。最后想着反正就我自己用了，为什么不直接自签名一套证书然后丢进gitea呢（&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将的 NAS Docker 中部署的 Gitea 从 HTTP 切换到 HTTPS，并使用在电脑上生成的自签名证书，这是一个非常常见的需求。整个过程可以分为以下几个关键步骤。&lt;/p&gt;
&lt;h3&gt;总体流程概览&lt;/h3&gt;
&lt;h3&gt;步骤一：在电脑上生成自签名证书&lt;/h3&gt;
&lt;p&gt;需要在电脑上安装 OpenSSL。Windows 10/11 通常可以通过 WSL (Windows Subsystem for Linux) 或 Git Bash 自带 OpenSSL。macOS 和 Linux 系统则通常会预装。
Windows下载编译包：https://slproweb.com/products/Win32OpenSSL.html&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/eobherymmd5dmx6v_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;明明和导入证书里的域名一模一样但还还是会包CommonName不匹配&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650 -nodes -subj &quot;/CN=nas.axonsin.software&quot; -addext &quot;subjectAltName=DNS:nas.axonsin.software&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完毕后，您会在当前目录下找到两个文件：&lt;/p&gt;
&lt;h3&gt;步骤二：将证书上传到 NAS 并放置到指定位置&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/eobherymmd5dmx6v_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;步骤三：修改 Docker 配置以挂载证书目录&lt;/h3&gt;
&lt;p&gt;为了让 Gitea 容器能读取到您上传的证书，您需要将 NAS 上的证书目录挂载到容器内部。
Gitea 容器内的数据目录通常是 /data。我们可以将 NAS 上的证书目录挂载到容器内的 /data/gitea/certs。
您需要根据您当初部署 Gitea 的方式（docker run 或 docker-compose）来修改。&lt;/p&gt;
&lt;h3&gt;步骤四：修改 Gitea 配置文件 app.ini&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[server]# 将协议从 http 修改为 httpsPROTOCOL         = https# 域名必须与您生成证书时填写的 Common Name 一致DOMAIN           = 192.168.1.100 # ROOT_URL 必须更新为 https 协议，并包含正确的域名和端口ROOT_URL         = https://192.168.1.100:10443/# Gitea 监听的 HTTP 端口（在容器内部）HTTP_PORT        = 3000# 禁用内置的 SSH 服务，如果您通过其他方式管理 SSHDISABLE_SSH      = false# SSH 服务的监听端口SSH_PORT         = 22# 证书文件在容器内的路径CERT_FILE        = /data/gitea/certs/cert.pem# 私钥文件在容器内的路径KEY_FILE         = /data/gitea/certs/key.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键修改点解释：&lt;/p&gt;
&lt;h3&gt;步骤五：重启 Gitea 容器&lt;/h3&gt;
&lt;p&gt;保存 app.ini 文件后，需要重启 Gitea 容器来加载新的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker restart gitea
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，如果您使用 Docker Compose：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，您应该可以通过 https://&amp;lt;您的NAS_IP&amp;gt;:&amp;lt;您映射的HTTPS端口&amp;gt; (例如 https://192.168.1.100:10443) 来访问您的 Gitea 实例了。&lt;/p&gt;
&lt;h3&gt;步骤六：处理客户端信任问题&lt;/h3&gt;
&lt;p&gt;由于您使用的是自签名证书，浏览器和 Git 客户端默认不会信任它。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器访问
当您第一次通过 HTTPS 访问 Gitea 时，浏览器会显示一个 &quot;您的连接不是私密连接&quot; 或 &quot;不安全&quot; 的警告。&lt;/li&gt;
&lt;li&gt;Git 客户端操作 (例如 git clone)
当您尝试从配置了 HTTPS 的 Gitea 服务器 clone 或 push 仓库时，Git 客户端会报错，提示 &quot;SSL certificate problem: self signed certificate&quot;。
您有两种解决方法：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git config --global http.sslVerify false
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 将路径替换为您实际存放 cert.pem 的路径git config --global http.sslCAInfo &quot;C:/Users/YourUser/certs/gitea.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：在 Windows 上，路径分隔符请使用 /。
完成以上所有步骤后，您的 Gitea 就成功地从 HTTP 切换到了使用自签名证书的 HTTPS 协议，并且您的 Git 客户端也能正常操作了。&lt;/p&gt;
</content:encoded></item><item><title>有关Git的操作</title><link>https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3git%E7%9A%84%E6%93%8D%E4%BD%9C/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%9C%89%E5%85%B3git%E7%9A%84%E6%93%8D%E4%BD%9C/</guid><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Git如何挂代理，绕过必须使用虚拟网卡的问题？&lt;/h1&gt;
&lt;p&gt;如果想为 Git 指定一个代理服务器，可以使用如下命令来设置 HTTP 和 HTTPS 协议的代理。假设你的代理地址是 127.0.0.1，端口是 7897（非常经典的clash服务器），那么你可以这样设置：&lt;/p&gt;
&lt;h3&gt;设置代理&lt;/h3&gt;
&lt;p&gt;进入Git bash之后：
对于 HTTP 协议：&lt;/p&gt;
&lt;p&gt;对于 HTTPS 协议：&lt;/p&gt;
&lt;p&gt;以上命令中的 --global 参数表示为当前用户的所有仓库设置代理。如果你只想对某个特定的仓库设置代理，可以在该仓库目录下运行上述命令但不使用 --global 参数。&lt;/p&gt;
&lt;h3&gt;取消代理设置&lt;/h3&gt;
&lt;p&gt;如果需要取消代理设置，可以使用以下命令：
对于 HTTP 协议：&lt;/p&gt;
&lt;p&gt;对于 HTTPS 协议：&lt;/p&gt;
&lt;h3&gt;检查代理设置&lt;/h3&gt;
&lt;p&gt;要检查是否已经正确设置了代理，可以使用以下命令查看 Git 的配置：&lt;/p&gt;
&lt;p&gt;这些命令可以帮助你确认代理设置是否已经按照预期生效。
请注意，如果你正在使用的代理需要认证（即用户名和密码），你需要将它们包含在代理的 URL 中，例如：http://username:password@127.0.0.1:7897。但是，请谨慎处理，确保不要在配置中明文存储敏感信息。&lt;/p&gt;
&lt;h1&gt;Git基本命令操作&lt;/h1&gt;
&lt;p&gt;使用 Git 进行暂存和提交是版本控制中最基本的操作。以下是详细步骤：&lt;/p&gt;
&lt;h3&gt;1. 查看文件状态&lt;/h3&gt;
&lt;p&gt;首先，查看工作目录中文件的状态，了解哪些文件被修改、新增或删除。&lt;/p&gt;
&lt;p&gt;这会列出：&lt;/p&gt;
&lt;h3&gt;2. 暂存文件（添加到暂存区）&lt;/h3&gt;
&lt;p&gt;使用 git add 命令将文件从工作区添加到暂存区。&lt;/p&gt;
&lt;h4&gt;暂存单个文件：&lt;/h4&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;h4&gt;暂存所有修改过的文件：&lt;/h4&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：git add . 只添加当前目录及子目录中的更改；git add -A 添加所有跟踪和未跟踪的文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. 提交到本地仓库&lt;/h3&gt;
&lt;p&gt;使用 git commit 将暂存区的内容提交到本地仓库。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 提交信息应简洁明了，说明本次修改的目的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;可选：跳过暂存区，直接提交（慎用）&lt;/h3&gt;
&lt;p&gt;如果你不想手动 add，可以直接提交所有已跟踪文件的更改（不包括新文件）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：此命令不会包含未跟踪的新文件（即从未被 git add 过的文件）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;完整示例流程&lt;/h3&gt;
&lt;h3&gt;小贴士&lt;/h3&gt;
&lt;p&gt;掌握 git add 和 git commit 是使用 Git 的第一步，熟练后可以结合分支、推送等操作进行团队协作。&lt;/p&gt;
&lt;h1&gt;Git修改用户名&lt;/h1&gt;
&lt;h3&gt;一、修改全局的用户名和邮箱（适用于以后的提交）&lt;/h3&gt;
&lt;p&gt;这不会修改历史提交，但会更改以后提交所使用的用户名。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 注意：这只是影响未来的提交，不会修改已有的 commit 记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;二、修改某一次提交的用户名和邮箱（使用 git commit --amend）&lt;/h3&gt;
&lt;p&gt;如果最近一次提交（HEAD）的用户名错了，可以修改最后一次提交：&lt;/p&gt;
&lt;p&gt;然后强制推送（如果是推送到远程）：&lt;/p&gt;
&lt;h3&gt;三、批量修改历史提交中的用户名（使用脚本重写历史）&lt;/h3&gt;
&lt;p&gt;如果你有多次提交都用了错误的用户名，需要用 git filter-branch 或更推荐的 git filter-repo 工具来批量修改。&lt;/p&gt;
&lt;h4&gt;方法1：使用 git filter-repo（推荐）&lt;/h4&gt;
&lt;p&gt;首先安装 git-filter-repo（比 filter-branch 更安全高效）。
然后运行：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：字符串要用 b&quot;&quot;（字节串），因为 Python 处理的是字节。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;执行后，所有匹配的提交都会被重写。
然后强制推送到远程：&lt;/p&gt;
&lt;p&gt;⚠️ 警告：重写历史会影响其他协作者，请谨慎操作，确保团队知晓。&lt;/p&gt;
&lt;h3&gt;四、检查当前配置的用户名&lt;/h3&gt;
&lt;p&gt;查看全局：&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目的&lt;/th&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;设置未来提交的用户名&lt;/td&gt;
&lt;td&gt;git config --global user.name &quot;Name&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改最后一次提交作者&lt;/td&gt;
&lt;td&gt;git commit --amend --author=&quot;Name &amp;lt;email&amp;gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量修改历史提交&lt;/td&gt;
&lt;td&gt;使用 git filter-repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强制推送更改&lt;/td&gt;
&lt;td&gt;git push --force-with-lease&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如需帮助确认哪些提交需要修改，可以用：&lt;/p&gt;
&lt;p&gt;查看最近 10 次提交的作者信息。&lt;/p&gt;
&lt;h1&gt;打包Git项目&lt;/h1&gt;
&lt;p&gt;在 Git 中，你可以使用 git archive 命令将项目打包为 zip（或 tar）文件，这个命令会导出指定分支、标签或提交的干净工作树（不包含 .git 目录和 .gitignore 中忽略的文件），非常适合用于发布或分享源码。&lt;/p&gt;
&lt;h2&gt;✅ 基础用法：打包当前分支为 zip&lt;/h2&gt;
&lt;h2&gt;✅ 打包指定分支（如 main 或 dev）&lt;/h2&gt;
&lt;h2&gt;✅ 打包指定标签（如发布版本 v1.0.0）&lt;/h2&gt;
&lt;h2&gt;✅ 打包指定提交（如某个 commit hash）&lt;/h2&gt;
&lt;h2&gt;✅ 指定压缩格式（zip / tar.gz）&lt;/h2&gt;
&lt;h3&gt;打包为 .zip&lt;/h3&gt;
&lt;h3&gt;打包为 .tar.gz&lt;/h3&gt;
&lt;h2&gt;✅ 添加前缀目录（推荐！避免解压后文件散落一地）&lt;/h2&gt;
&lt;p&gt;解压后结构：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 强烈建议加上 --prefix，否则所有文件会直接解压到当前目录，容易混乱。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 完整示例：打包 main 分支为带目录结构的 zip&lt;/h2&gt;
&lt;h2&gt;✅ 高级用法：只打包部分目录或文件&lt;/h2&gt;
&lt;p&gt;只打包 src/frontend/ 目录和 README.md 文件。&lt;/p&gt;
&lt;h2&gt;❗ 注意事项&lt;/h2&gt;
&lt;h2&gt;🆚 对比：zip -r vs git archive&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;git archive&lt;/td&gt;
&lt;td&gt;git archive -o xxx.zip HEAD&lt;/td&gt;
&lt;td&gt;✅ 自动过滤 .gitignore&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ 不包含 .git&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ 可指定提交/分支/标签&lt;/td&gt;
&lt;td&gt;❌ 不包含未跟踪文件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zip -r&lt;/td&gt;
&lt;td&gt;zip -r project.zip .&lt;/td&gt;
&lt;td&gt;✅ 包含所有本地文件（包括未跟踪的）&lt;/td&gt;
&lt;td&gt;❌ 包含 .git、node_modules 等垃圾文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌ 不安全，容易泄露敏感文件&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;✅ 推荐发布用 git archive，临时打包本地测试用 zip -r（但要小心）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✅ 实用脚本示例：自动打包当前分支 + 时间戳&lt;/h2&gt;
&lt;p&gt;保存为 pack.sh，运行：&lt;/p&gt;
&lt;p&gt;输出如：main-20250920-002100.zip
✅ 现在你已经掌握如何用 Git 优雅地打包项目为 zip 了！推荐用于发布、备份、提交作业、分享源码等场景。&lt;/p&gt;
&lt;h1&gt;Unity通用Gitignore&lt;/h1&gt;
&lt;h1&gt;忽略.idea和.svn的缓存文件&lt;/h1&gt;
</content:encoded></item><item><title>列表、元组、集合、字典</title><link>https://fuwari.vercel.app/posts/%E5%88%97%E8%A1%A8%E5%85%83%E7%BB%84%E9%9B%86%E5%90%88%E5%AD%97%E5%85%B8/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%88%97%E8%A1%A8%E5%85%83%E7%BB%84%E9%9B%86%E5%90%88%E5%AD%97%E5%85%B8/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Python 中，列表（list）、元组（tuple）、集合（set）、字典（dict） 是四种最常用的内置数据结构，它们各自有不同的特点、用途和行为。&lt;/p&gt;
&lt;h2&gt;列表（list）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;lst = [1, 2, 3]lst.append(4)  #  可修改print(lst[0])  #  可索引访问
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;元组（tuple）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tup = (1, 2, 3)# tup[0] = 99   报错！不可修改print(tup[1])  #  可索引访问
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;性能略优于列表，因为不可变，内存更紧凑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;集合（set）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;s = {1, 2, 2, 3}print(s)        # {1, 2, 3} —— 自动去重# print(s[0])    不支持索引s.add(4)        #  可修改
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;🔍 成员测试非常快：x in s 平均时间复杂度 O(1)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;字典（dict）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;d = {&apos;name&apos;: &apos;Alice&apos;, &apos;age&apos;: 25}d[&apos;city&apos;] = &apos;Beijing&apos;  #  可修改print(d[&apos;name&apos;])       #  通过键访问
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;🔍 键查找非常快：利用哈希查找。d[key] 平均时间复杂度仅为 O(1)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;列表 list&lt;/th&gt;
&lt;th&gt;元组 tuple&lt;/th&gt;
&lt;th&gt;集合 set&lt;/th&gt;
&lt;th&gt;字典 dict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;有序&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (3.7+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可变&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;允许重复&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;键❌，值✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;索引访问&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌（用键访问）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;哈希支持&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅（若元素可哈希）&lt;/td&gt;
&lt;td&gt;✅（元素可哈希）&lt;/td&gt;
&lt;td&gt;✅（键可哈希）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主要用途&lt;/td&gt;
&lt;td&gt;序列数据&lt;/td&gt;
&lt;td&gt;不可变序列&lt;/td&gt;
&lt;td&gt;去重、集合运算&lt;/td&gt;
&lt;td&gt;键值映射&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>刷机吧</title><link>https://fuwari.vercel.app/posts/%E5%88%B7%E6%9C%BA%E5%90%A7/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%88%B7%E6%9C%BA%E5%90%A7/</guid><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Windows ADB下载：https://developer.android.com/tools/releases/platform-tools?hl=zh-tw
掌玩mini刷机：
直接有FastBoot所以很快
https://www.bilibili.com/opus/823272168967110672
https://blog.zjyl1994.com/
https://www.zhihu.com/tardis/zm/art/646498660?source_id=1005
三星S10+解Root而不刷入第三方ROM（因为三星的骁龙固件会锁BL所以第三方的支持几乎是猎户座）
利用Frija软件下载三星原版固件
https://onfix.cn/course/4233?bid=3&amp;amp;mid=2120
https://onfix.cn/course/4724?bid=3&amp;amp;mid=2120
旧版的（安卓9版本，即出场版本的S10+）的Root教程：https://www.youtube.com/watch?v=o3a8YnWT3yk
三星Magisk官方教程：https://topjohnwu.github.io/Magisk/install.html#samsung-devices
Magisk官方会告诉你原版的包要在哪里下（当然我自己的话使用Frida，第三方的网站比如https://magiskcn.com/samsung-boot.html也告诉你Frida要如何配置并下载）。其他的请看红色字体的那个Youtube链接。因为安卓大版本更新了之后以上的网站似乎已经不再适用了，会出现无限Boot的情况。至于Magisk版本我用的是官方版本GIthub中的29.0，目前没有出现问题，也不需要像某些网站说的要什么Mod版本来避免国航的-45报错代号
也不要解压分理出AP文件只修补Boot镜像。因为三星手机没有Ramdisk支持，只可以在Magisk使用Recovery Mode模式修补整个AP包。
我的记忆中的成功顺序是：启动OEM解锁——Ordin——重新刷一遍包，BL、AP、CP、选择CSC而非HOMECSC直接清空数据——安装Magisk，用ADBpush和ADBpull进行整个AP包的patch和传输（请不要使用USB电源线来传AP包，因为MTP协议在面对大文件的传输有损坏可能，这也是我第一次无限Boot的问题，切记）——Ordin重新启动刷入AP patch过的包——使用电源+Bixby+音量加键来进行手机的另一个MagiskBoot分区启动，否则就是一般的非Root启动。如果我有记错的话请以Youtube视频的做法为准！
需要注意：因为版本更新的缘故，三星的magisk引导也不会从一般的分区加载（即直接按电源键启动是没用的，要一直按电源+音量加+Bixby键，一直等待到Samsung S10+ Secured By Knox部分松手的时候才可以启动Magisk，类似于Recovery模式启动但是松手的时机更早）
目前安卓12版本的更新，也是我复现出来的可以正常刷入magisk的视频教程，一切操作以这个成功过的教程为准：https://www.youtube.com/watch?v=TSfQmC4OTZo
如何隐藏自己的Root属性，如刷入Magisk Hide：https://juejin.cn/post/7403184763500658728&lt;/p&gt;
</content:encoded></item><item><title>Ollama本地AI</title><link>https://fuwari.vercel.app/posts/ollama%E6%9C%AC%E5%9C%B0ai/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ollama%E6%9C%AC%E5%9C%B0ai/</guid><pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;通识&lt;/h2&gt;
&lt;h3&gt;什么是Ollama？&lt;/h3&gt;
&lt;p&gt;https://ollama.com/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/ewwmytw2maasaleq_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ollama 是一个在本地运行大语言模型（LLMs）的工具和平台。它的目标是让用户能够像调用 API 一样，直接在个人电脑(主要是 macOS)上运行和管理大语言模型，而不依赖云端。  简单来说，就是一个大模型的Docker。
以下是 Ollama 的一些核心特点：
简单来说，Ollama 就是一个 本地大语言模型的运行环境和管理器，能在自己的电脑上很方便地使用和调用开源 LLM。由于是在本地电脑上运行大模型，直接从根源上解决了代码安全问题。&lt;/p&gt;
&lt;h3&gt;传统部署流程和Ollama对比：&lt;/h3&gt;
&lt;h4&gt;传统部署流程&lt;/h4&gt;
&lt;h2&gt;接入Cline进行代码编辑&lt;/h2&gt;
&lt;h2&gt;用Modelfile自定义大模型进行Roleplay&lt;/h2&gt;
&lt;h2&gt;用Python创建历史会话json实现不间断对话&lt;/h2&gt;
&lt;h2&gt;调用本地输出api接入Unity后端&lt;/h2&gt;
</content:encoded></item><item><title>Unity FrameGraph技术解析</title><link>https://fuwari.vercel.app/posts/unity-framegraph%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%90/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity-framegraph%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%90/</guid><pubDate>Sun, 24 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;用 Frame Graph 打造高效的多摄像机渲染管线.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;本文旨在深入、系统地剖析现代图形渲染管线中的两大核心技术领域，并阐明 Frame Graph 如何成为连接二者的桥梁与优化中枢。
通过引入 Frame Graph 技术，我们能够在假定所有“永久性”资产（如模型、材质纹理）已加载完毕的前提下，对每一帧的渲染流程进行精密的编排，从而实现以下关键性能目标：&lt;/p&gt;
&lt;h2&gt;一、 核心概念辨析：资产加载 vs. 帧内调度&lt;/h2&gt;
&lt;p&gt;这两个概念经常被混淆，但它们处理的是渲染管线中完全不同阶段的问题。我们可以用一个“厨房备餐”的例子来理解：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;资产加载系统 (Asset Loading System)&lt;/th&gt;
&lt;th&gt;Frame Graph (In-Frame Scheduler)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;目标&lt;/td&gt;
&lt;td&gt;将资源从磁盘/网络高效、异步地加载至显存，为渲染做好“备料”工作。&lt;/td&gt;
&lt;td&gt;优化单帧画面内 GPU 的工作流与临时显存的管理，实现“高效烹饪”。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关注点&lt;/td&gt;
&lt;td&gt;I/O 吞吐量、文件解码/解压效率、CPU到GPU的上传策略（例如，使用专用的上传堆）。&lt;/td&gt;
&lt;td&gt;显存复用（Aliasing）、同步屏障最小化、渲染任务的并行调度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型组件&lt;/td&gt;
&lt;td&gt;异步I/O读取器、纹理/模型解码器、资源缓存策略（LRU等）、上传管理器（Uploader）。&lt;/td&gt;
&lt;td&gt;Pass图构建器、资源生命周期分析器、依赖解析器、物理资源分配器。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关键指标&lt;/td&gt;
&lt;td&gt;资源加载耗时（Loading Time）、数据传输带宽利用率、卡顿（Stuttering）的避免。&lt;/td&gt;
&lt;td&gt;显存峰值占用（VRAM Peak Usage）、帧时间（Frametime）、GPU利用率（GPU Utilization）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;重要关联：Frame Graph 并非完全不关心资产加载。它将“上传新资源”或“回读数据到CPU”这些操作，抽象为图中的一个特殊节点（Pass）。例如，一个“Upload Pass”没有GPU输入，但它会“凭空”产生一个GPU资源（纹理/缓冲区）。同样，“Readback Pass”会消耗一个GPU资源，并将其标记为需要拷贝回CPU。这样，整个数据流就统一在了Frame Graph的框架下进行管理和同步。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二、 Frame Graph 的核心要素&lt;/h2&gt;
&lt;p&gt;Frame Graph 的魔力源于其声明式的API和对渲染流程的抽象。&lt;/p&gt;
&lt;h3&gt;1. 资源生命周期管理：逻辑与物理的分离&lt;/h3&gt;
&lt;p&gt;这是 Frame Graph 最核心的思想。&lt;/p&gt;
&lt;h3&gt;2. Pass 依赖关系&lt;/h3&gt;
&lt;p&gt;依赖关系是 Frame Graph 构建图的基石。开发者在定义一个 Pass 时，必须明确声明它会读取哪些逻辑资源，写入或修改哪些逻辑资源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/xn610ewoerldlkn6_image_00.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个图中，LightingPass 依赖 DepthPrePass 和 GBufferPass 的输出。Frame Graph 会自动推断出：&lt;/p&gt;
&lt;h3&gt;3. 外部资源接口&lt;/h3&gt;
&lt;p&gt;Frame Graph 管理的大多是“临时”资源，但它也需要与“持久”或外部资源交互。&lt;/p&gt;
&lt;h2&gt;三、 显存优化三原则&lt;/h2&gt;
&lt;h3&gt;1. 峰值压缩策略 (通过资源别名)&lt;/h3&gt;
&lt;p&gt;这是 Frame Graph 最直观的优势。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统方式：为每个用途分配独立资源，显存峰值为四者之和ID3D12Resource* pDepthMain = AllocateResource(desc_depth_main);ID3D12Resource* pColorMain = AllocateResource(desc_color_main);ID3D12Resource* pDepthMini = AllocateResource(desc_depth_mini);ID3D12Resource* pColorMini = AllocateResource(desc_color_mini);// Peak VRAM = Size(DepthMain) + Size(ColorMain) + Size(DepthMini) + Size(ColorMini)// Frame Graph 优化：分析生命周期，进行复用// 1. 注册逻辑资源fg.create(&quot;DepthMain&quot;, desc_depth_main);fg.create(&quot;ColorMain&quot;, desc_color_main);fg.create(&quot;DepthMini&quot;, desc_depth_mini);fg.create(&quot;ColorMini&quot;, desc_color_mini);// ... 注册所有 Pass 和读写关系 ...// 2. 编译阶段，Frame Graph 发现 DepthMini 和 ColorMain 的生命周期不重叠//    且假设它们的资源描述兼容（例如，都属于某个大的内存池桶）// 3. 实际执行PhysicalResource* blockA = PhysicalPool.Allocate(max(size_main, size_mini));// Pass 1 (Minimap): 使用 blockA 作为 DepthMini// Pass 2 (MainCam): 使用 blockA 作为 DepthMain// 如此，两个逻辑资源共享了同一块物理内存，峰值显著下降。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frame Graph 自动化的资源别名（Aliasing）能力，使得开发者无需手动管理复杂的内存复用逻辑，既安全又高效。&lt;/p&gt;
&lt;h3&gt;2. 屏障最小化&lt;/h3&gt;
&lt;p&gt;同步屏障是 GPU 管线中的“红绿灯”，滥用会导致严重的性能瓶颈（GPU 空闲等待）。&lt;/p&gt;
&lt;h3&gt;3. 并行执行优化 (Async Compute)&lt;/h3&gt;
&lt;p&gt;现代 GPU 不只有一个“执行引擎”。它们通常有专门的图形队列、计算队列和拷贝队列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def parallel_execute(pass_graph):    # 1. Frame Graph 分析依赖关系，发现两个独立的任务分支    #    例如：阴影图生成 和 环境光遮蔽计算    shadow_subgraph = pass_graph.extract_subgraph(&quot;Shadows&quot;)    ao_subgraph = pass_graph.extract_subgraph(&quot;AmbientOcclusion&quot;)    # 2. 将这些独立的子图提交到不同的硬件队列    submit_to_queue(shadow_subgraph, queue=&quot;Graphics&quot;)    submit_to_queue(ao_subgraph, queue=&quot;AsyncCompute&quot;)    # 3. 在需要合并结果的地方设置同步点（Fence/Semaphore）    #    例如，主光照 Pass 需要同时采样阴影图和 AO 贴图    synchronize_queues_at(pass=&quot;MainLighting&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frame Graph 通过图的结构分析，可以自动识别出这些并行机会，并管理好多队列之间的复杂同步，将 GPU 的多任务处理能力压榨到极致。&lt;/p&gt;
&lt;h2&gt;四、 例：多摄像机场景&lt;/h2&gt;
&lt;p&gt;多摄像机是 Frame Graph 大放异彩的领域。&lt;/p&gt;
&lt;h3&gt;案例：主相机 + 小地图&lt;/h3&gt;
&lt;p&gt;这个场景的渲染逻辑通常是串行的：先渲染小地图，再渲染主场景。
收益：原本需要 (DepthMain + ColorMain) + (DepthMini + ColorMini) 的峰值显存，现在只需要 max(Size(Main), Size(Mini)) 组合，如果尺寸差异大，节约效果非常显著。&lt;/p&gt;
&lt;h3&gt;镜面反射/水面倒影场景&lt;/h3&gt;
&lt;p&gt;这个场景的特点是存在清晰的生产者-消费者依赖关系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Frame Graph 声明式代码fg.add_pass(&quot;ReflectionPass&quot;,    // ...    .writes = { &quot;ReflectionTex&quot; });fg.add_pass(&quot;MainScenePass&quot;,    // ...    .reads = { &quot;ReflectionTex&quot; },    .writes = { &quot;SwapChainBuffer&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frame Graph看到这个 writes -&amp;gt; reads 的依赖，会自动在 ReflectionPass 和 MainScenePass 之间插入一个屏障，确保 ReflectionTex 从“渲染目标”状态安全地转换为“纹理采样”状态。开发者无需关心底层的 VkImageMemoryBarrier 或 D3D12_RESOURCE_BARRIER 的复杂设置。&lt;/p&gt;
&lt;h2&gt;五、 跨平台实现要点&lt;/h2&gt;
&lt;p&gt;虽然 Frame Graph 的思想是统一的，但底层的物理资源管理机制在不同图形 API 中有所差异。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台&lt;/th&gt;
&lt;th&gt;显存管理机制&lt;/th&gt;
&lt;th&gt;典型优化技巧与API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D3D12&lt;/td&gt;
&lt;td&gt;堆 (Heap) + 放置的资源 (Placed Resources)：先分配一大块内存（Heap），然后将多个资源（Resource）精确地放置在这块 Heap 的不同偏移上。这是实现资源别名的原生方式。&lt;/td&gt;
&lt;td&gt;别名屏障 (Aliasing Barrier)：在复用同一块内存用于不同资源时，需要插入一个特殊的别名屏障来通知驱动。ID3D12GraphicsCommandList::ResourceBarrier 批量提交。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vulkan&lt;/td&gt;
&lt;td&gt;VkDeviceMemory + vkBindImageMemory：与D3D12类似，先分配内存对象，再将图像/缓冲区对象绑定上去。通常使用 VMA (Vulkan Memory Allocator) 这样的库来简化子分配，实现高效的池化管理。&lt;/td&gt;
&lt;td&gt;队列所有权转移 (Queue Family Ownership Transfer)：在跨队列（如图形/计算）使用资源时，需要通过屏障转移所有权。精确的访问掩码 (Access Masks) 和 管线阶段 (Pipeline Stages) 控制同步粒度。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metal&lt;/td&gt;
&lt;td&gt;堆 (MTLHeap) + 临时纹理 (makeTexture with descriptor)：Metal 同样支持从一个预分配的 MTLHeap 中创建纹理和缓冲区。临时纹理（storageMode = .private）是为帧内资源优化的。&lt;/td&gt;
&lt;td&gt;加载/存储动作 (Load/Store Action)：在 MTLRenderPassDescriptor 中设置 loadAction 和 storeAction。loadAction = .dontCare 或 .clear 可以避免不必要的显存带宽消耗，这与 Frame Graph 的生命周期管理完美契合。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;六、 常见误区澄清&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;误区&lt;/th&gt;
&lt;th&gt;现实情况与深入解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frame Graph 就是资源加载器&lt;/td&gt;
&lt;td&gt;完全错误。Frame Graph 核心解决的是帧内 (intra-frame) 的 GPU 资源调度问题。它假设渲染所需的“永久”资产（模型、场景纹理）已经被资产加载系统加载到了显存。它自己不负责从磁盘读取文件。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资源复用会导致旧数据残留，引发渲染错误&lt;/td&gt;
&lt;td&gt;不会，如果正确使用。Frame Graph 在调度一个 Pass 时，会参考其对资源的加载操作 (Load Operation)。如果一个 Pass 要写入一个被复用的纹理，它会将其 LoadOp 设置为 Clear 或 DontCare。Clear 会在 Pass 开始时用指定颜色清空纹理；DontCare 则直接丢弃旧内容，因为渲染会覆盖所有像素。这确保了每次使用都是从一个干净的状态开始。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame Graph 只适用于单摄像机场景&lt;/td&gt;
&lt;td&gt;恰恰相反，多摄像机场景是其优势最大的应用领域。如前所述，小地图、后视镜、安全摄像头等场景会产生大量生命周期短暂的临时渲染目标。Frame Graph 的自动复用机制能将这些场景的总显存占用降低 2-3 倍甚至更多，效果远超单摄像机场景。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenGL/DirectX 11 等旧 API 不需要 Frame Graph&lt;/td&gt;
&lt;td&gt;仍然有价值，但收益点不同。虽然这些旧 API 没有原生的资源别名和精细的屏障控制，但 Frame Graph 仍然可以：&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;优化内存使用：通过管理一个资源池，避免频繁的 glGenTextures/glDeleteTextures 调用，减少驱动开销和内存碎片。&lt;/li&gt;
&lt;li&gt;简化逻辑：提供一个统一、声明式的接口来描述渲染流程，使代码更清晰、更易维护。&lt;/li&gt;
&lt;li&gt;为未来迁移做准备：采用 Frame Graph 架构，未来迁移到现代图形 API 会平滑得多。 |&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;七、 实施路线图：从零到一&lt;/h2&gt;
&lt;p&gt;想在你的引擎中引入 Frame Graph？可以遵循以下步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这是一个内存池的配置示例MemoryPoolConfig config = {    // 将资源按大小分为不同的桶，减少碎片    .bucket_sizes = { 256*1024, 1024*1024, 4*1024*1024, 16*1024*1024 }, // 256KB, 1MB, 4MB, 16MB    // 硬件要求的对齐字节    .alignment = 256,    // 允许的最大浪费率，用于决定资源放入哪个桶    .max_waste_ratio = 0.2 // 允许20%的空间浪费以换取复用};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;Frame Graph 并非银弹，但它是现代图形引擎应对日益复杂的渲染流程所演化出的“智能调度中枢”。它通过将渲染流程抽象化和数据化，实现了三大核心突破：
在实践中，引入 Frame Graph 能够为多摄像机等复杂场景带来 40%-70% 的显存占用降低，并可能带来 10%-30% 的 GPU 利用率提升。对于追求极致性能的移动平台和主机游戏开发，Frame Graph 已从一个“加分项”演变为“必备技术栈”。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实践建议：不要试图一蹴而就。可以从一个简单的后处理链（如 Bloom -&amp;gt; Tonemapping -&amp;gt; FXAA）开始，为它构建一个迷你的 Frame Graph。成功后，再逐步将主摄像机的渲染、阴影系统等模块迁移进来。始终利用可视化工具来观察你的图和资源分配，这会让你事半功倍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/xn610ewoerldlkn6_image_01.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>绘图——解构原典</title><link>https://fuwari.vercel.app/posts/%E7%BB%98%E5%9B%BE%E8%A7%A3%E6%9E%84%E5%8E%9F%E5%85%B8/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E7%BB%98%E5%9B%BE%E8%A7%A3%E6%9E%84%E5%8E%9F%E5%85%B8/</guid><pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;不知不觉已经到第六章了啊  之前我看的似乎只停留在第一章和第二章来着（
https://docs.qq.com/doc/DR0xKU1F5T3pkalRo?dver=
https://noobai.wall-breaker-no4.xyz/batch/noobai-xl-vpred-1
不同画师提示词之间的对比
https://comfyanonymous.github.io/ComfyUI_examples/
ComfyUI给的官方工作流&lt;/p&gt;
</content:encoded></item><item><title>大世界地形系统</title><link>https://fuwari.vercel.app/posts/%E5%A4%A7%E4%B8%96%E7%95%8C%E5%9C%B0%E5%BD%A2%E7%B3%BB%E7%BB%9F/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%A4%A7%E4%B8%96%E7%95%8C%E5%9C%B0%E5%BD%A2%E7%B3%BB%E7%BB%9F/</guid><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;近些年来，随着吃鸡类和开放世界类游戏的流行，以及程序化生成技术的普及，游戏中的地形越来越大。如何实现一个性能高且效果好的地形系统，是所有大世界游戏都要面临的问题。
下面两张地图，左图是经典的绝地求生海岛图，其规模为 8km&lt;em&gt;8km，右图是以超大世界著称的正当防卫4，其地图大小已经达到了 32km&lt;/em&gt;32km。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当前中国的市场环境以手游为主，而手机相对 PC 有着更严格的性能和功耗限制，这也导致了各个引擎内置的地形系统无法做到开箱即用。目前国内上线的游戏中，大多数都魔改甚至重写了引擎内置的地形系统。我们也测试了 Unity 自带的地形系统，在大地形下的性能表现是比较差的。
目前已经有不少游戏在大世界地形上做出了探索，GDC 上也经常有大佬分享实现大世界地形的制作经验。我们可以说是站在巨人的肩膀上，去思考并实现一个满足我们需求的大世界地形系统。
我们的需求可以总结为以下几点：&lt;/p&gt;
&lt;h2&gt;地形 Mesh 生成&lt;/h2&gt;
&lt;p&gt;大世界地形的制作流程，是先使用 World Machine 或者 Houdini 等自动化工具生产高度图，然后导入到引擎，最后由引擎的地形系统转换成地形 Mesh。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_02.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最简单的转换方式是将高度图直接对应到均匀分布的网格，这在地形较小的时候没有问题，但随着高度图的扩大，比如到 4K 大小时，对应的面数为 4k&lt;em&gt;4k&lt;/em&gt;2 ≈ 3.2*10^7，千万级别的面数在实时渲染中显然是不可接受的。所以我们需要实现一种高效率的 Mesh 生成方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_03.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;四叉树切分&lt;/h3&gt;
&lt;p&gt;为了解决面数问题，我们采用了四叉树作为地形 Mesh 的组织方式。四叉树可以理解为二叉树在二维空间的扩展，每个节点拥有四个子节点，常用做二维空间的切分，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_04.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体实现过程，是以整个地形作为四叉树的根节点，按照当前节点到相机的距离来决定是否继续划分，如果需要，则会划分为四个相同大小的子节点，重复迭代划分直到叶子节点为止，叶子节点的大小可以自由定义，比如 16 米、64 米等。所有的节点都对应了一块相同大小的 Mesh，也就是说，父节点的面积是子节点的四倍，但是 Mesh 面数是相同的。
如下图所示，绿色三角是玩家视角的可视范围，地形以玩家位置为中心进行四叉树划分，离玩家距离越近的节点，精度就越高。最后经过视锥裁剪，渲染可见范围内的节点，也就是图中的蓝色节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_05.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图是一个大小为 4km*4km 的地形，我们在场景中放置了一个相机，并以该相机作为玩家位置加载地形。为了观察方便，我们拉高了编辑器视角，并分别以 Local UV 和 Wireframe 模式输出。结合这两张图能够清楚的看到，离相机越近的节点面积越小，网格精度也越高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_06.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该视角下的地形面数为 20.8W 面，Draw Call 数为 26，这个数据已经基本在一个可接受的范围内。通过四叉树结构，我们已经有效的控制了地形的面数和 Draw Call 数，接下来会通过一些方法继续优化相关数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_07.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;减面算法&lt;/h3&gt;
&lt;p&gt;在实际的地形中，除了陡峭的山区，大多数区域都以平原和丘陵为主，这些区域的高度变化相对平缓，使用均匀网格是比较浪费的。因此我们采用了一套运行时动态减面的方案，在几乎不损失地形精度的前提下，达到了减少地形面数的目的。
我们创建了一个大小为 64m*64m、坡度较为平缓的地形，然后分别测试了关闭和开启减面时的面数。下图左侧为原地形，右侧为开启减面前后的 Mesh 对比。开启减面前需要 8.2K 面，开启减面后是 6.1K 面，减少了约 25% 的面数消耗。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_08.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里简单介绍一下减面算法。
首先根据高度图可以计算出所有顶点的法线方向，然后合并顶点法线差值小于阈值的顶点。合并过程可以理解为四叉树切分的逆过程，以相邻的四个顶点为一组判断是否可以合并，递归处理直到不能合并为止。合并完成后，每个被合并的顶点组对应一个 quad，最后对 quad 进行三角化，这个过程主要是进行缝边的处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_09.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该减面算法相比一些离线减面算法，复杂度要低很多，但仍然是存在一定消耗，我们目前是放到了 Background 线程中去计算，不会造成游戏卡顿，只是会略微提升耗电量。&lt;/p&gt;
&lt;h3&gt;裂缝问题&lt;/h3&gt;
&lt;p&gt;在不同等级的四叉树节点之间，由于 Mesh 分辨率不一样，会带来裂缝的问题（见下图）。由于我们是在 CPU 预先计算高度生成的 Mesh，所以也无法通过在 Shader 中进行 Morph 来解决。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_10.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们的解决方案是在四叉树更新完成后，计算每个节点与周围节点的 LOD 差值，然后删除可能产生裂缝的顶点并更新 Index Buffer。下图展示了如何处理一级和二级 LOD 差值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_11.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;考虑到在处理裂缝的同时，还要对 Mesh 进行减面，所以我们将 Mesh 分成了五个部分，中间部分和四条边，中间部分采用减面算法生成，四条边则根据相邻节点 LOD 级差生成，这五个部分共用相同的 Vertex Buffer 和 Index Buffer，所以仍然是一个 Draw Call 去绘制。
通常四叉树相邻节点之间的级差不会超过三级，所以我们只处理三级以内的缝边。我们对这几种边的 Index Buffer 进行了预生成，在生成节点 Index Buffer 的时候直接拷贝到中间部分的 Index Buffer 后面即可，效率很高。&lt;/p&gt;
&lt;h3&gt;Mesh LOD&lt;/h3&gt;
&lt;p&gt;四叉树中，不同级别节点之间的精度差为 2 倍，这会导致在边界处产生比较明显的精度变化，为了缓解该问题，我们在两级 LOD 之间，插入了 Mesh 级别的 LOD，如下图所示。除了平滑 LOD 精度外，也会减少地形的面数，LOD 0 一个 2x2 的 quad，会由原本 8 个面会减少到 4 个面，减少了一半。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_12.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图为同等级 LOD 节点不同 Mesh LOD，左侧为 6.1K 面，右侧为 3.7K 面，相比减少了 40% 左右。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_13.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在再回顾一下前面的 4K 场景，对比优化前面的 Mesh，经过一系列的优化，相同视角的面数从之前的 20.8W 面，下降到了 8.8W 面，减少了 60%。另外，我们还可以通过对参数的修改，在地形精细度和性能之间去做出调整，选择进一步降低面数和 Draw Call 以减少消耗，或者提升面数和 Draw Call 以提升效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_14.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_15.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;地表材质混合&lt;/h2&gt;
&lt;p&gt;生成完 Mesh 后，下一步就生成地表了，通常会通过地表层贴图和权重图来组合实现。&lt;/p&gt;
&lt;h3&gt;地表层与权重图&lt;/h3&gt;
&lt;p&gt;首先需要准备多组地表贴图，涵盖所有要用到地表类型，比如草地、雪地、沙地等等，每组地表贴图包括了 Albedo 贴图、法线贴图、粗糙度贴图、AO 贴图和高度贴图，这些贴图必须是四方连续的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_16.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;权重图来用于表示每种地表在所有位置的混合比例。每个地表类型需要对应一张权重图，权重图一般是 8bit 的，这样一张 R8G8B8A8 的贴图就可以表示四种地表类型的混合。以下图为例，一共添加了两层地表，第一层泥地使用了权重图的 R 通道，第二层草地使用了权重图的 G 通道。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_17.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然而，当地形大了之后，这种方式的弊端就会显现出来。假设地形的大小是 4km*4km，混合贴图精度 1pixel/m，当使用 16 种地表时，一共需要 4 张 4096 的贴图，这在性能上是不可接受的。&lt;/p&gt;
&lt;h3&gt;权重图优化&lt;/h3&gt;
&lt;p&gt;我们可以观察一下正当防卫4的地图，虽然它的生态类型很丰富，包含了沙漠、雪山、雨林等等，但每种生态都只会覆盖某一区域，比如雪地这层地表，只会在雪山上出现，其他区域的权重都是 0，这显然是对贴图的浪费。结合我们前面的 Mesh 策略，很容易想到将权重图进行切分，每块 Mesh 对应单独的权重贴图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_18.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们先看一下堡垒之夜采用的方案，其使用的是 UE4 引擎，会将地图切分成地块，每个地块都有单独的权重贴图。在切分之后，每个地块约有 5 到 9 层地表类型混合，对比之前的方案，已经大大减少了权重贴图的大小，但对于移动端来说，这个混合层数还是太多了。因此堡垒之夜在移动端上进行了优化，每个地块尽量控制在 4 层之内，通过牺牲效果来保障性能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_19.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;除了效果牺牲外，因为每个地块的面积比较大，地表控制在 4 层之内还会给美术制作带来比较大的限制。对此我们进行了改进，采用了一张全局索引贴图，索引贴图的每个像素对应一个较小的面积，比如 8m&lt;em&gt;8m、4m&lt;/em&gt;4m 等，像素编码了该区域使用了哪几层地表。
如下图所示，使用了一张 8x8 的 IndexMap 和一张 64x64 的权重图，实现了在 64m*64m 地块上混合 6 层地表的效果。因为权重和为 1，也就是 sum(r,b,g,a)=1，所以 a 通道可以被舍弃掉。权重图只使用了三个通道就表示六层地表的权重，大大减少了权重图贴图大小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_20.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时，我们也会为四叉树的所有 LOD 节点都生成混合贴图，每个父节点的混合贴图都可以由子节点合并得到。基于远处地块的屏占比较低的原理，权重图在较低等级的 LOD 地块上，也会与 Mesh 一样降低分辨率，减少贴图大小。
在原始的方案中，4K 地图 16 层地表需要 4 张 4096 的贴图，未压缩大小达到了 256M。而在用了权重图优化后，4K 地图的权重图累计共 84M，压缩后只需要约 20M，并且由于实际渲染时只会加载小部分地块，实际加载的内存还要少很多。&lt;/p&gt;
&lt;h2&gt;效果提升&lt;/h2&gt;
&lt;p&gt;生成地形 Mesh 并实现了地表混合后，地形的基础功能已经基本完备了，接下来就是尝试对地形的材质效果做进一步提升。这里我们主要针对两个问题进行了改进，一是贴图平铺带来的重复感问题，二是权重混合表现不够真实的问题。&lt;/p&gt;
&lt;h3&gt;去重复感算法&lt;/h3&gt;
&lt;p&gt;下图是和平精英中的截图，可以看到，地表有比较明显的重复感，尤其是在跳伞视角。这是因为地表通常是将贴图以 Repeat 的方式平铺在地形上来实现的，受限于贴图精度，一般几米就会重复一次。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_21.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_22.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了解决重复感问题，我们参考了 SIGGRAPH 2018 上一篇 Paper 中的算法，称为 ByExample Noise。篇幅原因这里只介绍一下算法的基本思想，对算法原理感兴趣的同学可以阅读一下原 Paper。该算法利用了随机采样的思想，将贴图在 UV 空间进行三角划分，然后随机寻找三个三角形，采样对应区域的颜色进行混合，特别的权重设计保证了混合后权重和仍然为 1。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_23.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;混合操作会使目标贴图的方差降低，反应在结果上就是混合后的贴图比原图模糊。Paper 中为了解决该问题，提出了基于直方图来恢复方差的算法，但这种算法需要预生成映射贴图，并在采样时使用 LUT 转换，这会带来额外的消耗。综合对比之后，我们还是使用了没有 LUT 的版本，在地表贴图这种特征不明显的贴图上，带来的模糊不会很明显，下图是几个示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_24.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;左边是简单 Tiling，重复感明显，右边加入了 By-Example Noise，重复感已经非常低。
我们将地形分为了独占局域和混合区域，独占区域指的是当前区域只存在一层地表，混合区域则是有多层地表混合而成。ByExample Noise 只会在独占区域使用，这是因为重复感主要出现在独占区域，而混合区域由于多层地表叠加，并不会有明显的重复感。还有一个原因是这样不会带来额外的消耗，Shader 中会进行判断，在独占区域采样相同层三次进行混合去重复感，混合区域则是正常采样四层不同地表进行混合。
下图是实际应用到地形后的对比，可以看到左侧重复感明显，而右侧使用了 ByExample Noise 之后，已经几乎感觉不到重复感。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_25.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;基于高度的混合&lt;/h3&gt;
&lt;p&gt;上面在地表材质混合的章节中提到，地形通常会采用权重图去进行多层地表的混合，这种方式比较直接，但也会带来一些问题。
首先权重图是有精度限制的，假如权重图的 1 个像素对应 1 米，那也就是说不同地表之间至少需要 1 米的过渡带，如果美术想表示更硬的地表边缘，就需要提高权重图的精度，带来额外的消耗。第二个问题就是这种混合方式在现实世界中往往是错误的，比如说现实中沙石路，并不是石子和沙地按权重去混合，而是石子中嵌在沙地中，这种效果只通过权重混合是无法实现的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_26.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们在权重混合的基础上，加入了高度混合，每种地表需要额外提供一张高度图，在混合时和权重图加权后计算最终的混合比例。下图中，左侧只使用了权重混合，右侧则加入了高度混合，基本上实现了上面提到的石子嵌在沙地中的效果，真实感有了很大提升。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/up9ecyh6mmtodxgg_image_27.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结性能对比&lt;/h2&gt;
&lt;p&gt;我们基于以下环境对新地形和 Unity 内置地形进行了性能对比测试。
下表是测试数据，可以看到我们的地形系统在 GPU 消耗、CPU 占用、内存占用等指标上都较内置地形有了大幅提升。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地表层数&lt;/th&gt;
&lt;th&gt;帧数&lt;/th&gt;
&lt;th&gt;Tick on Main Thread Average(ms)&lt;/th&gt;
&lt;th&gt;Min(ms)&lt;/th&gt;
&lt;th&gt;Max(ms)&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;新地形4K&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;59.7&lt;/td&gt;
&lt;td&gt;0.09&lt;/td&gt;
&lt;td&gt;0.04&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;约62M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内置地形4K&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;45.1&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;0.53&lt;/td&gt;
&lt;td&gt;3.34&lt;/td&gt;
&lt;td&gt;约566M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内置地形4K (Instanced)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;45.6&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;td&gt;2.40&lt;/td&gt;
&lt;td&gt;约680M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内置地形4K (Instanced)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;59.7&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;1.19&lt;/td&gt;
&lt;td&gt;约210M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;后续工作&lt;/h2&gt;
&lt;p&gt;地形系统是大世界系统中较为重要的系统之一，当前版本已完成了地形的基础功能，后续我们将围绕以下几点进行持续迭代。&lt;/p&gt;
</content:encoded></item><item><title>关于卡渲的一些思路</title><link>https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8E%E5%8D%A1%E6%B8%B2%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%B7%AF/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8E%E5%8D%A1%E6%B8%B2%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%B7%AF/</guid><pubDate>Mon, 18 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;壹 可信的光影和空间关系&lt;/h2&gt;
&lt;p&gt;现实物理世界中，光与影伴生存在，但在动漫工业里，不一定要严格地呈现物理真实，普遍的做法是，保证在相机视角下的完美为前提，用概括和抽象的方法，重新定义项目自己的表达要素。H62就遵循这个原则，具体体现在如下的两个方面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;H62 的地板是平的，因此可以使用视觉精度最高的平面阴影。H62 在计算中只使用 L1 的世界空间坐标，作为相似三角形的共用顶点。
并且这个坐标的选取，一要让阴影有好看的形状，二要和所处的场景中的主光源产生的阴影相匹配，达成一种角色置身于空间中的可信。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;角色的自阴影，在计算中只使用 L2 的方向向量。设置的原则是，让形成的自阴影作为大结构的空间关系绘制补充（相对于小结构的 AO 而言），在 H62 中，这个效果常用来表达动漫中绝对领域的渲染。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_02.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;贰 重新梳理的表达层次&lt;/h2&gt;
&lt;h3&gt;1）平的秘密&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_03.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用一种通用的说法来解释，卡通渲染中变平的秘密就是让 NdotL 的值变化足够平缓，减弱人脑对物体 3D 感的判断。
但是不管如何来减弱以及包装分段函数式光影，只要这种陡变的光影过渡效果出现在角色的身上，就不可避免地会被划归到赛璐璐渲染风格中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_04.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，为了还原 H62 原画呈现的日系插画风格，在确认渲染方案之初，采用的方法，是第一节所说的用观察向量替代光照向量用于计算。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_05.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2）丢失的结构要素补回&lt;/h3&gt;
&lt;p&gt;但是，采用 V==L 的方案以后，H62 丢掉了 NdotL 式的光影对于脸部结构的描绘。因此，我们需要用其他的要素，来把缺失的脸部结构的描绘补充完善。
首先是皮肤的次表面散射效果，次表面散射效果在实现上是关于曲率的函数，也就相当于描绘了脸部法线变化速率。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_06.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时，次表面散射也是借鉴自写实渲染方案，用来传达生命活力重要因子。
方案的具体实施，并不是单纯对写实渲染的方案进行简化，而是基于艺术的核心传达目的而进行的重新定义和组合，客观存在的数据集合，并不会因为我们划分子集的方法而变化，艺术是唯心的，方法是唯物的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_07.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由图可以模糊定义，“七分真”也是好感度的极值点之一。
次表面散射部分，我们采用三层染色的方式来制作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_08.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心的想法是用三层 mask 来控制三种颜色在皮肤上进行染色。控制三层染色的 ramp 图如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_09.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中 tintcolor3 是由 R 通道控制，tintcolor2 是由 G 通道控制，tintcolor1 是由 B 通道控制。可以看出，tintcolor3 是范围最大的红色，依此类推。此种方法旨在拟合一束白光打在无限大的皮肤平面上的各个通道的能量消散速度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_10.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由观察可以得出，红色衰减的比较慢，而蓝色和绿色衰减的比较快，并且绿色衰减的速度要大于蓝色。而在最后，各个通道衰减的速度又趋近于 0，因此，在最后，由于能量的消散，又趋于黑色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_11.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们通过对三层的基础颜色的设置，以及 ramp 图，来模拟上述结果。
可以看到有 3S 和没 3S 的皮肤的差异。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_12.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以上的方案，本质上是在 NX 引擎中，基于艺术风格表达的需求，实现了一套自定义的，便于美术理解（？）的 Diffusion Profile。打了个问号的意思是，我觉得我当时做的不好。整套系统 7 个自由度，却不是完全正交，项目里美术很难靠掌握背后的原理，来调节合适的参数，基本上是靠感觉，来进行自纠错。
然后是，由 1D Ramp 定义的单向轮廓光，即相当于定义了一盏虚拟的光源，永远在镜头的右边照到角色身上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_13.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考《嗜血代码》的方案“遇事不决，齐次坐标”，加了一个蒙版来控制轮廓光的范围。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_14.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个单向轮廓光在 H62 中，也是对于皮肤的菲涅尔的表达。因为皮肤材质没有定义粗糙度，可以通过调节轮廓光的强度，来暗示皮肤的光滑程度。并且由于用 ramp 图固定了轮廓光的范围，可以避免常见的搞光算法导致的油腻效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_15.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3）以生命活力为目标的渲染&lt;/h3&gt;
&lt;p&gt;Bloom 在卡通渲染中是比较容易出效果的，但是也不能滥用，防止过度饱和而遮盖了细腻的光照细节。
H62 用多 pass 的方式，只给予头发和皮肤额外的 bloom 放大系数调整，其他材质维持标准化的光感。
这里也是基于艺术认知来做的 Trick，让角色本体和穿戴，产生微妙的光感差异，向用户传达角色具有生命活力的心理暗示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_16.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;LUT（Look Up Table）是主美工具，制作方法是对当前效果截图，在 PS 中用工具调整到效果满意以后，将改动拖到标准 LUT 上形成新的 LUT。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_17.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;值得注意的地方是：LUT 一旦确认，轻易不要更改，否则需要大量时间，对场景以及场景特效进行联调。这样折腾了两次以后，修改为 LUT 只对角色起作用。
眼球也是一个复杂的多层结构。其中虹膜，角膜，巩膜三个结构是我们能观测到的最主要的部分。而 在他们之间的空间，可以看作是灌满了液体的结构。虹膜部分对眼睛呈现的颜色贡献最大，巩膜部分贡献了眼白部分，角膜贡献的是反射和搞光效果。而中间的液体由于折射，呈现的是一种类似视差贴图的效果。
漫画家在绘制漫画风格的眼睛的时候，往往会简化这些写实结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_18.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由此可以定义，H62 在渲染层面需要在表达的内容包括，巩膜的次表面散射效果（3S），虹膜卡通手绘的颜色和细节，角膜上的透射和反射，以及玻璃体的折射效果。
巩膜就是我们俗称的眼白，我们从观测结果的角度来分析，可以暂定眼白的边缘是呈现和皮肤基本一致的 3S 特性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_19.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_20.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;于是我们在制作中把眼白和眼睛的其他结构拆开，而和头部模型合并，在同批次进行渲染。
如下图所示，眼白的渲染基本满足了我们预设的要求，比如边缘呈现血液的红润倾向。
虹膜是我们看到的眼睛的主要效果，也是最能体现眼睛二次元的部分。
这部分大部分参照的二次元漫画眼睛的绘制方法。然后从风格表达以及低配时的保底效果考虑，手绘部分环境反射结果到颜色贴图上。
我们看到的眼睛的光泽和搞光，主要来自于角膜和内部的液体构成的这个类玻璃体。所以角膜部分，我们按照正常的玻璃材质进行渲染就可以了。除了玻璃本身的透明以及光泽效果，我们还增加了一个模仿 MMD 风格的动态搞光效果，增加二次元的感觉。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_21.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过光影表达头型的饱满&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_22.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_23.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过对非生命体和生命体（头发这里归于非生命体）的阴影色相的冷，暖配置，来增加美术细节以及进一步暗示脸和身体的生命活力&lt;/p&gt;
&lt;h2&gt;贰+ 方案改进和工作流调整&lt;/h2&gt;
&lt;p&gt;H62 现在的表达层次是次表面散射，1DRamp 轮廓光，模型阴影片，缺点是没有法线（NdotL）相关的动态光影信息。
目前卡通渲染中，为了实现风格化 &amp;amp; 高精度动态光影效果，主要有两个验证过的工作流：
一个是罪恶装备的工作流，以法线修改工具为核心，实现光影的定制化；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_24.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另一个是原神的工作流，通过采样重要性手绘关键帧，并用 SDF 融合，生成的风格化 ramp，来计算光影。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_25.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比较下来，原神的工作流，阴影精度更高，并且没有修改原始模型的法线，不会影响其他在计算中使 用法线的渲染效果，更符合自由视角观察结果（罪恶装备没有自由视角的需求，并且手动调整顶点法线是非线性工作流，这对运营期项目很不友好。）
但是 H62 角色众多（100+），每一个角色脸型，UV 都不一样，故每一个角色的脸部光照贴图的关键帧都需要耗费大量的精力去绘制，并且要得到较好的效果，需要进行多次修改，直接使用原神的方案会导致美术的工作量巨大。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_26.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，我对原神的手绘 Ramp 效果进行分析，来思考原神这样手绘的道理，例如眼下三角光，我们认为是为了表达苹果肌亮面，而三角形则是风格化的表达（影视打光里的伦勃朗光的抽象）。
PS：因为这个三角光是伦勃朗光的抽象，不同的角色，不同的模型，如果都用同一个 Ramp 硬套，是很生硬的，对于不匹配的脸型，几乎等于是在重构脸部的体积。这也是大家觉得违和感，以及不美观的来源，因为三角光没有出现在基于我们的普遍认知里应该出现的位置。制作自动生产工具，就是为了解决这个问题。
我们可以用算法把这个光亮区域改成接近三角形。可以根据需要，改成任何想表达的形状，例如表达男性硬朗的梯形。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_27.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_28.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;更改过渡值更加硬朗的阴影
结合两种方案的优点，以及巧妙避开各自方案的缺点，是开发这个工具的核心思想。
工具首先对模型进行曲面细分，极大的增加模型的面数，以此让光影的调整更加精细。这一步相当于自动解决了罪恶装备的调整法线法，对布线的高要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_29.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后对脸部模型进行分析，自动计算出苹果肌和鼻子位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_30.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并计算光照和面部模型信息，实时地自动修改面部法线，使得阴影曲线与脸型贴合，并且用算法微调，使光照阴影更接近于手绘的风格。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_31.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来自动改变光照角度，对关键帧进行采样，可以自动生成关键帧选取角度下的光照图，并存入缓存中。解决了美术需要手绘大量关键帧和不断修改阴影曲线的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_32.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后用缓存中的关键帧采样结果，进行 SDF 融合，通过设置关键帧权重，对重要光影效果过渡进行节奏上的控制，使好看的光影效果在光照变化中形状保有更持久一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_33.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;效果对比&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_34.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;效率对比&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_35.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;叁 PBR 与 NPR 融合&lt;/h2&gt;
&lt;p&gt;关于 PBR 和 NPR 的效果融合，最近几年越来越多的游戏开始尝试，并且做出了很好的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_36.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关于风格融合，有两个切入点。
首先是美术角度，在设计上要在角色的身上尽量控制使用 PBR 材质的比例。这样总体上给人观感上就没有那么写实。
其次就是技术角度上的调整。目前 H62 主要是通过权重对计算结果进行融合（七分真原则）。计算流程示意图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_37.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种 NPR 和 PBR 的结合，在丝袜材质上体现的最为明显。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_38.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;金属部分的风格化，目前也是用调整光照权重来实现的，这个方法和原神用 ramp 的方法在风格化思路上是一致的。
但目前因为 H62 的材质分 ID 的规则，还无法直接搬运原神的方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_39.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用自发光材质，和使用 NPR 材质在感受上类似，会给人一种平面的感觉，也会让渲染的结果看起来更二次元。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_40.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;叁+ 手绘也要遵守规则&lt;/h2&gt;
&lt;p&gt;H62 对手绘的使用有比较严格的规范，（特别是 PBR 材质上的手绘）但总体上如果画的很好看，我不会主动去纠正，甚至会帮模型顶住来自图程的吐槽。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_41.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_42.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_43.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_44.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_45.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;肆 还有什么可以卷的呢&lt;/h2&gt;
&lt;h3&gt;1）可交互触感（狗头）&lt;/h3&gt;
&lt;p&gt;方案参考《堡垒之夜》枢轴动画 + 曲线 + 骨骼关联 = 广义蒙皮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_46.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前 UE4，有两版工具 PivotPainter 1.0 和 PivotPainter 2.0，材质函数如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_47.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看命名就能猜个大概用途了，想详细的了解，可以去官方文档上学习。
目前在 3Dmax 中有配套的 script，Houdini 官方也在 GitHub 上提供了对应的 HDA。综合考虑下来，对于复杂的顶点动画需求，还是在 Houdini 中能做到更高的自由度，抬高此流程的效果上限。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_48.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_49.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此选 Houdini。
1.0 工具可以存储的信息要略少一点，只能固定存储轴坐标，轴向，随机数，和动画蒙版（存储在 UV2 和 UV3，以及顶点色和顶点透明通道中）。
2.0 的工具会生成两张纹理，可以选择的信息包括：
实际体验下来：
1.0 和 2.0 各有优势。
1.0 的优势在于可以省去采样贴图，更加节省性能，但做东西不够灵活，也因为 UV 数量的关系，某些手机不支持。另外因为使用顶点色，不支持使用动态合批进行优化。
2.0 需要采样两张贴图，性能消耗要大于 1.0（远小于 VAT），但相对 1.0 的优势是在实现 shader 动画的过程中，十分灵活，可以烘焙到纹理上的信息可选种类很多，比如可以根据动画设计，来考虑 shader 中需要使用的轴坐标，轴动画向量，选取子模型的顺序（也就是播放顶点动画的顺序），层级的 ID（如果动画中需要的话）。
再加上 2.0 的可用材质函数也更多，因此选 PivotPainter 2.0 工作流。
跑了一下流程：
根据需求的这个效果，判定需要导出的信息有：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_50.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_51.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_52.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_53.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此外，触碰时还可以和表情系统进行关联（臆想中的好感度玩法）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_54.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2）双向 SDF&lt;/h3&gt;
&lt;p&gt;这里我们要明白一点，SDF 本身已经是一个可选方案，双向 SDF 更是一个可选方案中的可选方案 =.=。
我们的努力不是让某种渲染方案成为标准，而是预估美术表达内容和方法的演进趋势，提前准备。
如果说横向 SDF 是表达的角色动态光影，纵向 SDF 在动漫中更多的是表达情绪以及性格。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_55.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_56.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时双向 SDF 也能匹配更多的光照情况，比如俯照，仰照，点光源。
难度主要在于，纵向光在鼻子的投影不是递增的，没有办法用 SDF 的方式进行拟合。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_57.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_58.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考如下，时间关系不细说了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_59.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_60.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bdl0taal2kyq52ef_image_61.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3）其他待续。。&lt;/h3&gt;
&lt;p&gt;H62：幻书启世录 U1 黑潮之上&lt;/p&gt;
</content:encoded></item><item><title>Stable Diffusion技术解析</title><link>https://fuwari.vercel.app/posts/stable-diffusion%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%90/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/stable-diffusion%E6%8A%80%E6%9C%AF%E8%A7%A3%E6%9E%90/</guid><pubDate>Sun, 17 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;StableDiffusion分享&lt;/h1&gt;
&lt;h2&gt;什么是Stable Diffusion&lt;/h2&gt;
&lt;p&gt;Stable Diffusion 是一款免费、开源的 AI 图像生成器。于 2022 年 8 月推出，应用于 AI 软件，用户可以随意输入自己想要的内容，然后系统就会自动生成非常优秀的艺术渲染作品。
Stable Diffusion 的生成系统由现有艺术作品组成的巨大数据库训练而成，能够快速生成与提示信息有所关联的新奇图像。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_00.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;SD 的各个版本对比&lt;/h2&gt;
&lt;p&gt;目前使用的较多的是 SD1.5 和 SDXL，其中 SD1.5 凭借其较低的硬件要求、更快的生成速度、成熟的社区支持以及在特定领域的优势,仍然保持着相当的受欢迎度。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SD v1.x (1.4/1.5)&lt;/th&gt;
&lt;th&gt;SD v2.x (2.0/2.1)&lt;/th&gt;
&lt;th&gt;SDXL v1.0&lt;/th&gt;
&lt;th&gt;SDXL Turbo&lt;/th&gt;
&lt;th&gt;SDXL Lightning&lt;/th&gt;
&lt;th&gt;SD3（API）&lt;/th&gt;
&lt;th&gt;SD3.5（API）&lt;/th&gt;
&lt;th&gt;Flux&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;发布时间&lt;/td&gt;
&lt;td&gt;2022年8月–10月&lt;/td&gt;
&lt;td&gt;2022年11月–12月&lt;/td&gt;
&lt;td&gt;2023年7月&lt;/td&gt;
&lt;td&gt;2023年11月&lt;/td&gt;
&lt;td&gt;2024年2月&lt;/td&gt;
&lt;td&gt;2024年2月&lt;/td&gt;
&lt;td&gt;2024年9月&lt;/td&gt;
&lt;td&gt;2024年中起（持续迭代）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;基础功能&lt;/td&gt;
&lt;td&gt;基本 LDM 图像生成&lt;/td&gt;
&lt;td&gt;质量显著提升&lt;/td&gt;
&lt;td&gt;Base+Refiner 两段优化&lt;/td&gt;
&lt;td&gt;极少步高速（1–4 步）&lt;/td&gt;
&lt;td&gt;4–8 步高质、低延迟&lt;/td&gt;
&lt;td&gt;新一代扩散，文本对齐强，仅 API&lt;/td&gt;
&lt;td&gt;在 SD3 基础上进一步增强文本与复杂场景&lt;/td&gt;
&lt;td&gt;强化写实与文本排版，高分辨率生成，商用导向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型规模&lt;/td&gt;
&lt;td&gt;基础规模&lt;/td&gt;
&lt;td&gt;略增&lt;/td&gt;
&lt;td&gt;U‑Net/骨干扩大≈3×&lt;/td&gt;
&lt;td&gt;与 SDXL 相当&lt;/td&gt;
&lt;td&gt;与 SDXL 相当（蒸馏/量化友好）&lt;/td&gt;
&lt;td&gt;更大更深（未开放权重）&lt;/td&gt;
&lt;td&gt;进一步增大与优化（未开放权重）&lt;/td&gt;
&lt;td&gt;多规格（基础/Pro/商用版本），权重多为闭源或受限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;训练分辨率&lt;/td&gt;
&lt;td&gt;512×512&lt;/td&gt;
&lt;td&gt;512×512&lt;/td&gt;
&lt;td&gt;1024×1024&lt;/td&gt;
&lt;td&gt;1024×1024（少步蒸馏）&lt;/td&gt;
&lt;td&gt;1024×1024（蒸馏）&lt;/td&gt;
&lt;td&gt;高分辨率覆盖，文本渲染优化&lt;/td&gt;
&lt;td&gt;同左，文本/版式更稳&lt;/td&gt;
&lt;td&gt;高分辨率友好（常用 ≥1024），版式与文字区域专门优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特色功能&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;×4 超分、修复绘制、深度模型&lt;/td&gt;
&lt;td&gt;更好的提示词匹配；两阶段细化&lt;/td&gt;
&lt;td&gt;单步/少步稳定&lt;/td&gt;
&lt;td&gt;少步但保真度更高&lt;/td&gt;
&lt;td&gt;强文字、徽标、复杂布局&lt;/td&gt;
&lt;td&gt;最强文本与复杂场景（API）&lt;/td&gt;
&lt;td&gt;出色的文字/徽标、产品图与排版；对写实/商业场景优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;风格特点&lt;/td&gt;
&lt;td&gt;艺术性强、偏欧美&lt;/td&gt;
&lt;td&gt;通用性更强&lt;/td&gt;
&lt;td&gt;质量与细节均衡&lt;/td&gt;
&lt;td&gt;偏实时/交互&lt;/td&gt;
&lt;td&gt;少步高质、画面干净&lt;/td&gt;
&lt;td&gt;写实度高、文本可读性强&lt;/td&gt;
&lt;td&gt;复杂场景与文本最强&lt;/td&gt;
&lt;td&gt;商业与产品级写实风格、文本与图标清晰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;硬件要求&lt;/td&gt;
&lt;td&gt;较低&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;较高（建议≥8GB）&lt;/td&gt;
&lt;td&gt;中等偏低&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;云端 API&lt;/td&gt;
&lt;td&gt;云端 API&lt;/td&gt;
&lt;td&gt;视版本而定；多数通过云端/商用服务，少量受限模型可本地推理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型大小&lt;/td&gt;
&lt;td&gt;较小&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;Base/Refiner 各≈7GB&lt;/td&gt;
&lt;td&gt;近似 SDXL&lt;/td&gt;
&lt;td&gt;近似 SDXL&lt;/td&gt;
&lt;td&gt;未公开&lt;/td&gt;
&lt;td&gt;未公开&lt;/td&gt;
&lt;td&gt;未公开或受许可限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生态与可得性&lt;/td&gt;
&lt;td&gt;完整开源，LoRA/ControlNet 极成熟&lt;/td&gt;
&lt;td&gt;开源，社区重心转向 1.5/SDXL&lt;/td&gt;
&lt;td&gt;开源主力，插件/流程最丰富&lt;/td&gt;
&lt;td&gt;常见开源推理权重&lt;/td&gt;
&lt;td&gt;广泛工作流支持&lt;/td&gt;
&lt;td&gt;仅官方 API&lt;/td&gt;
&lt;td&gt;仅官方 API&lt;/td&gt;
&lt;td&gt;多为闭源/商用 API 或受限权重，围绕品牌/产品图与文本生成的生态增长快&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;简要说明&lt;/p&gt;
&lt;h2&gt;稳定扩散去噪声&lt;/h2&gt;
&lt;p&gt;参考资料:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_01.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;【大白话01】一文理清 Diffusion Model 扩散模型 | 原理图解+公式推导_哔哩哔哩_bilibili
https://www.youtube.com/watch?v=1CIpzeNxIhU
https://www.youtube.com/watch?v=iv-5mZ_9CPY&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_02.png&quot; alt=&quot;output.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;几个概念&lt;/h3&gt;
&lt;h3&gt;Step 1: Text-to-Image Initialization&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_03.png&quot; alt=&quot;output (1).png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Stable Diffusion 首先在潜在空间中生成一个随机张量。这个张量由随机数生成器的种子 Seed 决定，它代表了图像在潜在形式下的表示，尽管在这个阶段它看起来像是噪声。&lt;/p&gt;
&lt;h3&gt;Step 2: Noise Prediction&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_04.png&quot; alt=&quot;output (2).png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;噪声预测器 U-Net 输入这个潜在的有噪声的图像和提供的文本提示。然后，它也在潜在空间中预测噪声。&lt;/p&gt;
&lt;h3&gt;Step 3: Noise Subtraction&lt;/h3&gt;
&lt;p&gt;具体来说，Stable Diffusion 的正向扩散过程会在潜在图像上不断添加噪声，使其变成完全随机的张量。而逆向扩散过程则是通过 Noise Subtraction 来逐步去除噪声，将噪声图转换为有意义的图像。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_05.png&quot; alt=&quot;output (3).png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在逆向扩散中，Stable Diffusion 使用了一个专门训练的噪声预测器（U-Net）来预测每一步添加的噪声。然后，将预测的噪声从当前的潜在图像中减去，得到一个新的潜在图像。步骤 2 和 3 会重复进行预先确定的采样步数 steps，通常约为 20 次迭代。&lt;/p&gt;
&lt;h3&gt;Step 4: Decoding&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_06.png&quot; alt=&quot;output (4).png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后一步涉及 VAE 解码器，它将潜在图像转换回像素空间，生成最终的 AI 生成图像。&lt;/p&gt;
&lt;h2&gt;SD 参数&lt;/h2&gt;
&lt;p&gt;为了让用户更好的使用 Stable Diffusion，有一些开源项目的 UI 项目帮助人来更容易的上手使用，常见的有 A1111 也叫 stable-diffusion-webui，还有 comfyui。
comfyui 可以自定义工作流，可玩性更高，后面以 comfyui 为例介绍常用参数。&lt;/p&gt;
&lt;h3&gt;基础文生图工作流&lt;/h3&gt;
&lt;p&gt;工作流是由一个一个节点进行连接工作的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_07.png&quot; alt=&quot;ComfyUI_00052_.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;拖入上面的图到comfyui即可（如果没有被压缩的话）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_08.png&quot; alt=&quot;output (5).png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;模型&lt;/h3&gt;
&lt;p&gt;模型是 SD 的核心，SD 的文件常见格式有 safetensors/ckpt。模型决定了生成图的风格、类型，比如有的模型是生成真人的，有的是生成二次元的。
模型市场：https://civitai.com/models
一个模型由三部分组成:&lt;/p&gt;
&lt;h3&gt;Prompt/Negative Prompt&lt;/h3&gt;
&lt;p&gt;Prompt 是用户输入的文本描述，用来指导 Stable Diffusion 生成符合描述的图像。它可以包含以下几个方面的信息:
Negative prompt 用于指定你不希望在生成的图像中出现的元素或特征。它帮助模型避免生成某些不需要的内容，从而提高输出质量。常见的用途包括:
使用 negative prompt 可以帮助你更精确地控制生成结果，提高图像的质量和相关性。
Clip Text Encode 的节点输入是模型的 CLIP。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_09.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Image Width/Height&lt;/h3&gt;
&lt;p&gt;初始图节点决定了生成图的 width 和 height，以及要生成的数量。
在文生图中，是一个空白的 Latent Image。
在图生图则需要加载图片作为初始 Latent。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_10.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Sampler&lt;/h3&gt;
&lt;p&gt;Sampler 是整个算法的核心。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_11.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_12.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_13.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Sampler/Scheduler&lt;/h3&gt;
&lt;p&gt;Sampler（采样器）:
采样器的主要作用是:
不同的采样器采用不同的算法和方法来执行这个过程，如 Euler、Heun、DDIM 等。采样器的选择会影响生成速度和图像质量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_14.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如：
Scheduler（调度器）:
调度器的主要作用是:
调度器通常与采样器配对使用。某些采样器可能更适合特定类型的调度器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_15.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;h3&gt;Lora&lt;/h3&gt;
&lt;p&gt;Lora (Low-Rank Adaptation) 是 Stable Diffusion 中一种高效的模型微调技术，是微软研究员引入的一项新技术。
LORA 是一种在消耗更少内存的情况下，加速大型模型训练的训练方法，在 stable diffusion 中它允许使用低阶适应技术来快速微调扩散模型。简而言之，LoRA 训练模型可以更轻松地针对不同概念（例如角色或将定风格）进行模型训练。这些经过训练的模型可以被导出并供其他人使用。
LORA 模型是小型的 stable diffusion 模型，对 checkpoint 模型 cross-attention layers (交叉注意力层）进行了较小的更改，但是它的体积只有 checkpoint 的 1/100 到 1/10，文件大小一般在 2-500MB 之间。
主要有以下特点和使用建议:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_16.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;h3&gt;HiresFix&lt;/h3&gt;
&lt;p&gt;Hires fix 是 Stable Diffusion 中一个用于生成高质量高分辨率图像的重要功能。Hires fix 允许用户在保持图像整体构图的同时生成高分辨率图像。它通过先生成低分辨率图像，然后进行放大和细节增强来实现这一目标。
工作原理:
Hires fix 的工作流程如下:
优势:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_17.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;h3&gt;Refiner&lt;/h3&gt;
&lt;p&gt;Refiner 是 Stable Diffusion v1.6.0 版本引入的新功能，旨在提高生成图像的整体质量和细节。它通过在基础模型生成图像后，使用专门的细化模型进行进一步处理来实现这一目标。
Refiner 的工作流程如下:&lt;/p&gt;
&lt;h3&gt;ControlNet&lt;/h3&gt;
&lt;p&gt;ControlNet 是 Stable Diffusion 的一个重要扩展功能，它可以让用户更精确地控制图像生成过程。
ControlNet 允许用户通过提供参考图像或条件来引导 Stable Diffusion 的图像生成过程。它可以控制生成图像的姿势、构图、轮廓等细节。
例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_18.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;canny openpose scribbles&lt;/p&gt;
&lt;h2&gt;工程建设动态资源池调度&lt;/h2&gt;
&lt;h3&gt;模型切换问题&lt;/h3&gt;
&lt;p&gt;StableDiffusion 是基于模型文件来做图像生成，模型是 SD 的核心，文生图必然依赖模型。不同的模型适用于不同的场景，例如画人物时使用真人模型，画动漫时使用二次元模型等。
模型文件很大，比如 SD1.5 的模型文件大概是 2G，SDXL 的模型文件大小是 6G 甚至更大。
大文件的加载往往是一个比较费时间的操作。webui 使用模型文件的大体逻辑如下，假设要加载的模型为 ModelA:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_19.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中将 ModelA 加载到内存是一个很耗时的动作，为了保证线上的性能，需要尽可能减少模型切换。&lt;/p&gt;
&lt;h3&gt;分 PSM 管理模型&lt;/h3&gt;
&lt;p&gt;为了减少模型切换的成本消耗问题，采用的实现方案是分模型创建 PSM 服务，即一个 PSM 服务只提供一种模型文件，通过 PSM 和模型的映射关系，由上层 Proxy 跟进模型进行请求分发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_20.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种模式存在利用率的问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_21.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了提高机器的利用率，需要进一步对资源进行调度优化。只有根据负载做合理分配，才能达到优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。&lt;/p&gt;
&lt;h3&gt;资源池调度&lt;/h3&gt;
&lt;p&gt;由于模型切换成本高（不然模型实时加载，按空闲分配就可以），我们需要对机器按需分配来提高机器的利用率，即请求量大的模型多分配点机器，请求量小的少分配机器或者不分配机器。
我们的方案是根据模型流量比例来自动调节对应的机器比例，要保证模型切换的成本和流量分布的平衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_22.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过定时任务进行调度，每 3 分钟轮询一次，每次调度都会根据当前排队的模型进行按流量比例分配。同时，在服务启动时，调度器会随机分配一个模型，以确保服务启动时可用。&lt;/p&gt;
&lt;h3&gt;模型和机器映射&lt;/h3&gt;
&lt;p&gt;维护模型和机器的映射关系，提供给 proxy 要访问的机器列表。同时会对机器进行探活，若实例失效需要通知调度器及时进行重新调度。&lt;/p&gt;
&lt;h3&gt;Proxy&lt;/h3&gt;
&lt;p&gt;Proxy 根据请求中的模型名称，获取机器列表，然后过频控和推优逻辑选择合适的机器，然后转发请求。&lt;/p&gt;
&lt;h3&gt;交互过程：调度器负责根据实际流量对机器进行模型调度，以满足请求量大的模型分配更多实例的需求&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_23.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;在线请求的处理流程如下&lt;/h3&gt;
&lt;h3&gt;冷启动问题&lt;/h3&gt;
&lt;p&gt;冷启动: 模型如果没有在内存中，第一次文生图的时候会把模型加载到内存再执行，这个加载过程比较慢，基本在分钟级别。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bpeo4ix2bz4copxo_image_24.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前的方案是使用了懒汉模式，即在调度过程只是分配模型，模型的加载过程发生在切换之后的第一次文生图请求中，这就会导致第一次的请求的耗时会变得很长。
为了解决这个问题，升级成饿汉模式：在调度过程就把模型加载上（进行一次最小成本的文生图请求），在文生图请求到来之前保证加载好。最小成本的文生图请求：宽高 16，steps=1，denoise=0, prompt=&quot;a person&quot;，生图过程可以保证在 0.01s 以内完成。
由于 comfyui 是单任务运行模式，在加载模型阶段接受到新的请求会排队，依然会造成请求的加长。为了解决这个问题，我们采用了一系列的举措。&lt;/p&gt;
&lt;h3&gt;服务负载评估&lt;/h3&gt;
&lt;p&gt;在线转发我们需要评估服务的负载，将服务转发到负载降低的机器可以保证请求可以更快的返回。
SD 的负载需要考虑：
所以我们提出了用 ETA 来作为服务的负载指标，ETA 作为预估返回时间可以评估机器的负载。
ETA = 队列预估执行时间 + 当前请求的预估时间
影响执行时间的因素有很多：
因为我们模型搭配的工作流相对固定，为了简化评估过程，我们只考虑模型切换的时间成本和执行时间成本。
在机器上会记录每次请求的收益。
高峰期的成功率从 80%提升至 99%，这意味着我们的系统在高流量和高负载的情况下，能够更加稳定地运行，为用户提供更加可靠的服务。&lt;/p&gt;
&lt;h2&gt;Comfyui&lt;/h2&gt;
&lt;p&gt;当前的文生图使用了 webui 作为后端服务，webui 的问题是在于使用 GNU AGPL 开源许可证，有一定的法务风险。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GNU Affero 通用公共许可证（GNU Affero General Public License，简称 AGPL）是一种自由软件许可证，旨在确保软件的自由使用、修改和分发，同时特别针对通过网络提供服务的情况加强了开源要求。### 核心原则：强 CopyleftAGPL 基于著名的 GNU 通用公共许可证（GPL），继承了其“Copyleft”（著佐权）的核心理念。这意味着：*   **自由使用与修改**：任何人都可以自由地运行、研究、修改软件。*   **分发要求**：当您分发软件的副本（无论是原始版本还是修改后的版本）时，**必须**在相同的 AGPL 许可证下提供完整的、对应的源代码。### AGPL 的关键创新：针对网络服务AGPL 与标准 GPL 最重要的区别在于它**弥补了所谓的“网络应用漏洞”或“SaaS 漏洞”**。*   **标准 GPL 的局限**：如果一家公司使用了一个基于 GPL 的软件来运行一个网络服务（例如一个网站或 API），他们可以对软件进行修改以满足自己的需求，但只要他们不向客户分发软件的二进制副本，他们就没有义务向公众发布其修改后的源代码。*   **AGPL 的解决方案**：AGPL 明确规定，**即使您没有分发软件，而是仅仅通过网络向用户提供服务，也视为一种“分发”行为**。因此，如果您修改了 AGPL 软件并将其用于公开的网络服务，您有法律义务向所有使用该服务的用户提供修改后的源代码。### 主要特点1.  **强制开源**：任何基于 AGPL 软件的修改版本或衍生作品，在被公开使用（尤其是通过网络）时，都必须以 AGPL 许可证开源。2.  **保护社区**：旨在鼓励对网络服务软件的改进也能回馈给开源社区，促进协作和创新。3.  **传染性强**：AGPL 是一种“强传染性”许可证。如果您的专有软件与 AGPL 代码进行了深度集成（例如，通过动态链接或紧密的进程间通信），可能会被认定为“衍生作品”，从而要求您的整个软件也必须在 AGPL 下开源。这使得 AGPL 对商业闭源软件来说风险较高。4.  **与 GPL 的关系**：AGPLv3 与 GPLv3 高度兼容。您可以将 GPLv3 的代码与 AGPLv3 的代码结合，但结合后的作品必须以 AGPLv3 发布。### 适用场景AGPL 非常适合以下类型的项目：*   **网络应用程序**：如 Web 应用、SaaS 平台、API 服务等，开发者希望确保任何使用该软件提供服务的人都能贡献回他们的改进。*   **希望最大化社区贡献的项目**：项目维护者希望所有基于其软件的改进都能公开，避免被私有化。### 对开发者的启示*   **使用 AGPL 软件需谨慎**：如果您在商业项目中考虑使用 AGPL 许可的组件，请务必进行彻底的法律审查。评估您的使用方式是否构成“衍生作品”以及是否会触发开源义务。*   **选择许可证**：如果您是开源项目作者，选择 AGPL 意味着您非常重视代码的开放性和社区的共享。如果您希望更宽松的采用（例如允许闭源集成），可能会选择 MIT、Apache 2.0 或 LGPL。总而言之，AGPL 是一个强有力的开源许可证，它通过要求网络服务的提供者也必须开源其修改，来确保软件自由在云计算和 SaaS 时代的延续。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 comfyui 使用的 GNU-GPL 协议，区别在于使用 AGPL 的如按键提供网络服务，就必须公开源代码。所以使用 comfyui 更加安全。
另外相对于 webui，comfyui 支持工作流，可玩性更高，上限也更高。
综合上面两个原因，需要在文生图流程中支持 comfyui，并且将线上的 webui 请求都转发到 comfyui，逐步去除对 webui 的依赖。&lt;/p&gt;
&lt;h3&gt;动态工作流&lt;/h3&gt;
&lt;p&gt;为了对齐 webui 的能力，我们需要对 comfyui 的工作流做一个动态支持，比如需要 lora 的时候，工作流就加载 lora 并执行相关逻辑，不需要的时候就不加载 lora 的相关节点。目前对齐 webui 的能力有：&lt;/p&gt;
&lt;h3&gt;性能对比&lt;/h3&gt;
&lt;p&gt;针对已有特性做了 comfyui 和 webui 的性能对比。
使用同一台机器：8核 32G，显卡 A30 24G 显存，Cuda118
可以看到在大部分场景，comfyui 速度更快，最快可以快 20%&lt;/p&gt;
</content:encoded></item><item><title>如何将.key转换为Windows可读的格式</title><link>https://fuwari.vercel.app/posts/%E5%A6%82%E4%BD%95%E5%B0%86key%E8%BD%AC%E6%8D%A2%E4%B8%BAwindows%E5%8F%AF%E8%AF%BB%E7%9A%84%E6%A0%BC%E5%BC%8F/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%A6%82%E4%BD%95%E5%B0%86key%E8%BD%AC%E6%8D%A2%E4%B8%BAwindows%E5%8F%AF%E8%AF%BB%E7%9A%84%E6%A0%BC%E5%BC%8F/</guid><pubDate>Sat, 16 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;https://cloudconvert.com/key-to-ppt
这个网站可以转换为ppt.或者直接用mac（bushi&lt;/p&gt;
</content:encoded></item><item><title>Docker安装Gitea</title><link>https://fuwari.vercel.app/posts/docker-install-gitea/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/docker-install-gitea/</guid><description>Docker安装Gitea指北</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/ylsislove/make-a-little-progress-every-day/blob/master/2022/2022-08/2022-08-22/%E6%89%8B%E6%8B%89%E6%89%8B%E6%95%99%E4%BD%A0%E5%9C%A8%E6%9E%81%E7%A9%BA%E9%97%B4Docker%E4%B8%8A%E5%AE%89%E8%A3%85Gitea%E5%B9%B6%E5%AE%9E%E7%8E%B0SSH%E6%8F%90%E4%BA%A4%E4%BB%A3%E7%A0%81.md&quot;&gt;https://github.com/ylsislove/make-a-little-progress-every-day/blob/master/2022/2022-08/2022-08-22/%E6%89%8B%E6%8B%89%E6%89%8B%E6%95%99%E4%BD%A0%E5%9C%A8%E6%9E%81%E7%A9%BA%E9%97%B4Docker%E4%B8%8A%E5%AE%89%E8%A3%85Gitea%E5%B9%B6%E5%AE%9E%E7%8E%B0SSH%E6%8F%90%E4%BA%A4%E4%BB%A3%E7%A0%81.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://u0w0n.github.io/post/7.html&quot;&gt;https://u0w0n.github.io/post/7.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.chenweikang.top/?p=1151&quot;&gt;https://www.chenweikang.top/?p=1151&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Houdini导出hda时，纹理调用使用相对路径</title><link>https://fuwari.vercel.app/posts/houdini-export-hda-relative-texture-path/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/houdini-export-hda-relative-texture-path/</guid><description>Houdini中使用相对路径的最佳实践</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一般来说，Houdini在拖入纹理的时候会直接调取绝对路径(C://Users//......)&lt;/p&gt;
&lt;p&gt;如果把这个玩意导出到hda的时候，Unity/UE读取的时候就会报错。因为它本身没有访问这些外部文件的权限。&lt;/p&gt;
&lt;p&gt;在 Houdini 中使用纹理路径时，当前是用的类似于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$JOB/Desktop/works/Houdini/GIt_Houdini_ProjectFiles/Wave_Function_Collapse_Tiles/Grid_Source/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种路径，实际上还是&lt;strong&gt;绝对路径&lt;/strong&gt;（虽然带了 &lt;code&gt;$JOB&lt;/code&gt;），这样在导出为 HDA（Houdini Digital Asset）或者跨项目/跨平台时容易出现纹理丢失问题。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;推荐的做法：使用相对路径&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;1. 放置纹理于项目目录下&lt;/h3&gt;
&lt;p&gt;假设你的项目结构大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GIt_Houdini_ProjectFiles/
  |-- Wave_Function_Collapse_Tiles/
      |-- Grid_Source/
          |-- Grid_Demo_01.png
      |-- houdini_project.hip
      |-- my_hda.hda
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 设置 &lt;code&gt;$HIP&lt;/code&gt; 或 &lt;code&gt;$JOB&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$HIP&lt;/strong&gt;：指的是当前打开的 Houdini 工程文件所在的文件夹。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JOB∗∗：可以自定义为项目根目录（建议你在File&amp;gt;SetProject或直接设置JOB&lt;/strong&gt;：可以自定义为项目根目录（建议你在 File &amp;gt; Set Project 或直接设置 JOB∗∗：可以自定义为项目根目录（建议你在File&amp;gt;SetProject或直接设置JOB 环境变量）；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 使用相对路径（推荐）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;写法一：$HIP 相对路径&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$HIP/Wave_Function_Collapse_Tiles/Grid_Source/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果你的 hip 文件和资源目录都在项目文件夹下，这样路径就是相对的，HDA 在别的机器上打开时也能找到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;写法二：$JOB 相对路径&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$JOB/Wave_Function_Collapse_Tiles/Grid_Source/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;只要你在别的电脑上设置了同样的 $JOB 环境变量，路径就不会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;写法三：纯相对路径&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Grid_Source/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果 HDA 和贴图在同一目录下，或者你把贴图文件夹与 HDA 一起打包分发，直接用纯相对路径即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. HDA打包贴图（可选）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在 HDA Type Properties 里可以把贴图文件“嵌入”到 HDA（如 Extra Files），这样分发最安全，但贴图不能太大，否则 HDA 文件体积会暴增。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;实际建议&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开发时&lt;/strong&gt;：用 &lt;code&gt;$HIP&lt;/code&gt; 或 &lt;code&gt;$JOB&lt;/code&gt; 路径，保持工程和贴图结构一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布时&lt;/strong&gt;：可以把贴图和 HDA 文件一起放一个文件夹或者子目录里，用相对路径引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多机环境&lt;/strong&gt;：务必同步项目文件夹结构和 $JOB 环境变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;示例：相对路径的写法&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果你的 HDA 和贴图都在同一目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果贴图在 HDA 的子目录 &lt;code&gt;textures&lt;/code&gt; 下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./textures/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;textures/Grid_Demo_$F2.png
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;尽量避免硬编码绝对路径。&lt;/li&gt;
&lt;li&gt;多用 &lt;code&gt;$HIP&lt;/code&gt;、&lt;code&gt;$JOB&lt;/code&gt; 或纯相对路径。&lt;/li&gt;
&lt;li&gt;打包 HDA 时可以用 Extra Files 嵌入资源，但注意体积和引用方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/5d6de560.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Unity VFX Graph</title><link>https://fuwari.vercel.app/posts/unity-vfx-graph/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity-vfx-graph/</guid><description>Unity VFX Graph的详细介绍和使用方法</description><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://unity.com/cn/resources/creating-advanced-vfx-unity6&quot;&gt;https://unity.com/cn/resources/creating-advanced-vfx-unity6&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Unity渲染队列和渲染层级的区别</title><link>https://fuwari.vercel.app/posts/unity-render-queue-vs-render-layer/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity-render-queue-vs-render-layer/</guid><description>Unity渲染队列和渲染层级的区别的技术原理和应用</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Render Queue（渲染队列）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目的：控制渲染顺序&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;决定&quot;谁先画，谁后画&quot;&lt;/li&gt;
&lt;li&gt;主要解决透明物体混合、性能优化等问题&lt;/li&gt;
&lt;li&gt;数值越小越早渲染&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Background (1000) → Geometry (2000) → Transparent (3000) → Overlay (4000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rendering Layers（渲染层级）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目的：控制光照分组&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;决定&quot;谁被什么光照亮&quot;&lt;/li&gt;
&lt;li&gt;主要用于光照优化和视觉分层&lt;/li&gt;
&lt;li&gt;是位掩码，可以多选&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Light组件设置：Culling Mask = Layer 8 + Layer 10
// 这个光源只照亮Layer 8和Layer 10的物体
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;具体区别：&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Render Queue&lt;/th&gt;
&lt;th&gt;Rendering Layers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;作用范围&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;整个渲染管线&lt;/td&gt;
&lt;td&gt;光照计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;解决问题&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;渲染顺序、透明混合&lt;/td&gt;
&lt;td&gt;光源分组、性能优化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;设置位置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;材质的Queue属性&lt;/td&gt;
&lt;td&gt;物体的Rendering Layer Mask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数值类型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;整数（越小越早）&lt;/td&gt;
&lt;td&gt;位掩码（可多选）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;例子：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 同一个角色物体可以同时设置：
角色Renderer：Rendering Layer = 10    // 只被角色专用光源照亮
角色材质：Render Queue = 2050         // 在环境之后渲染，确保描边效果

// 两者互不影响，各司其职
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单记忆：&lt;strong&gt;Rendering Layers管&quot;灯光&quot;，Render Queue管&quot;顺序&quot;&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>WFC瓦片生成标准解读</title><link>https://fuwari.vercel.app/posts/wfc-tile-generation-standard/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/wfc-tile-generation-standard/</guid><description>WFC瓦片生成标准解读，介绍了WFC算法的基本原理和应用场景，重点分析了瓦片的邻接规则和位掩码技术。</description><pubDate>Sat, 02 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/posts/640ab766.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/5bc4e017.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大致工作流：AI生成2D俯视图——生成地板——墙壁重新组装为Prefab&lt;/p&gt;
&lt;p&gt;要不地板和墙壁单独计算？&lt;/p&gt;
&lt;p&gt;先生成带有墙壁的草图，然后对于特性房间可以用地板区分？&lt;/p&gt;
&lt;p&gt;还是直接Random？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有关Blender和Unity的模型协作，请看&lt;a href=&quot;https://www.yuque.com/shuangfeidu/txwa8w/qeomm77fu218fa4v&quot;&gt;https://www.yuque.com/shuangfeidu/txwa8w/qeomm77fu218fa4v&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上（1）右上（2）右（4）右下（8）下（16）左下（32）左（64）左上（128）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8邻域位掩码（8-directional bitmask）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术解读&lt;/h2&gt;
&lt;p&gt;首先需要知道WFC算法是做什么的。简单来说，它是一种根据一小组“规则”来程序化生成复杂地图（比如游戏关卡、纹理等）的算法。&lt;strong&gt;它最核心的规则就是“邻接规则”——即哪些瓦片（tile）可以和哪些瓦片放在一起&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这张图展示的就是一套非常经典和高效的瓦片（tileset）定义标准，通常被称为“Blob Tileset”或“Bitmask Autotiling”。它的目的是用一种巧妙的方式，自动处理所有瓦片的邻接关系。&lt;/p&gt;
&lt;h3&gt;核心概念：什么是“瓦片”和“规则”？&lt;/h3&gt;
&lt;p&gt;在这张图中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;黄色部分&lt;/strong&gt;可以理解为“实体”或“地面”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;蓝色部分&lt;/strong&gt;可以理解为“虚空”或“背景”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一个小方格都是一个“瓦片”。WFC算法的目标就是将这些瓦片拼接成一个看起来自然、没有断裂的、更大的“Blob”（一团）形状。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规则&lt;/strong&gt;就是：&lt;strong&gt;黄色的边缘必须和黄色的边缘相连，蓝色的边缘必须和蓝色的边缘相连。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了实现这一点，我们需要为每一种可能的瓦片组合都创建一个瓦片。一个2x2的瓦片有4个角，每个角都可以是黄色或蓝色，所以理论上我们有 2^4 = 16 种基本形态。这正是图片最上面一行展示的16种核心瓦片。&lt;/p&gt;
&lt;h3&gt;2. 解读图片中的数字：Bitmask（位掩码）&lt;/h3&gt;
&lt;p&gt;这张图最让人困惑的就是这些数字。这些数字是每种瓦片的&lt;strong&gt;唯一ID&lt;/strong&gt;，而这个ID是通过一种叫做**“位掩码”（Bitmask）**的技术计算出来的。&lt;/p&gt;
&lt;p&gt;这个方法给瓦片的每个关键位置分配一个固定的数值（通常是2的幂），然后通过将“激活”位置的数值相加，得到最终的ID。&lt;/p&gt;
&lt;p&gt;非常棒的分析！您已经完全抓住了最核心的要点，这正是程序化生成中常用的 &lt;strong&gt;8邻域位掩码（8-directional bitmask）&lt;/strong&gt; 的思想。&lt;/p&gt;
&lt;p&gt;您的推理方向完全正确：通过为每个邻近位置（边和角）分配一个2的幂的数值，然后将它们相加，来为每种瓦片形态生成一个独一无二的ID。&lt;/p&gt;
&lt;p&gt;不过，您会发现，如果将您提出的这套数值方案代入原图，会无法完美匹配所有ID。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;上(1), 右上(2), 右(4), 右下(8), 下(16), 左下(32), 左(64), 左上(128)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这是因为这张经典的 &quot;Blob Tileset&quot; 图，使用了一套稍微有些“古怪”和&lt;strong&gt;非连续&lt;/strong&gt;的数值分配方案。您的方案在逻辑上更清晰、更现代，但这张图遵循的是另一套标准。&lt;/p&gt;
&lt;p&gt;让我们用您正确的&lt;strong&gt;思路&lt;/strong&gt;，来解开这张图&lt;strong&gt;特定&lt;/strong&gt;的&lt;strong&gt;数值谜题&lt;/strong&gt;。这张图最上面一行的16个瓦片，实际上只关心&lt;strong&gt;四个角&lt;/strong&gt;的状态。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;为了让你彻底理解，我们把整个逻辑梳理一遍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目的：&lt;/strong&gt; 使用WFC算法，通过拼接小瓦片，自动生成看起来自然的、连贯的“Blob”地图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心规则：&lt;/strong&gt; 瓦片的边缘颜色必须匹配（黄对黄，蓝对蓝）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;瓦片ID (位掩码)：&lt;/strong&gt; 为了让计算机高效地理解和查询“哪些瓦片可以和我的右边缘匹配？”，我们给每种瓦片一个数字ID。这个ID是通过检查瓦片的边缘（或角）是否为黄色，然后将对应位置的预设值相加得到的。这就是位掩码技术。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片的作用：&lt;/strong&gt; 这张图就是一个**“ID查询表”&lt;strong&gt;或&lt;/strong&gt;“标准解读”**。它告诉你：
&lt;ul&gt;
&lt;li&gt;ID为 &lt;code&gt;7&lt;/code&gt; 的瓦片长什么样（上、右、下是黄边）。&lt;/li&gt;
&lt;li&gt;ID为 &lt;code&gt;23&lt;/code&gt; 的瓦片长什么样（上、右、下、左都是黄边，但它不是全黄的中心块）。&lt;/li&gt;
&lt;li&gt;ID为 &lt;code&gt;5&lt;/code&gt; 的瓦片和ID为 &lt;code&gt;20&lt;/code&gt;, &lt;code&gt;80&lt;/code&gt;, &lt;code&gt;65&lt;/code&gt; 的瓦片在逻辑上属于&lt;strong&gt;同一组&lt;/strong&gt;，它们可能只是旋转或翻转的关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;如何使用它：&lt;/strong&gt;&lt;br /&gt;
当WFC算法要在一个空格子填充瓦片时，它会检查这个格子的邻居。比如，它发现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上方的瓦片有一个黄色的下边缘。&lt;/li&gt;
&lt;li&gt;右边的瓦片有一个蓝色的左边缘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法就会去这个规则集里寻找一个满足“上边缘为黄，右边缘为蓝”的瓦片，然后从所有满足条件的瓦片中随机选择一个放进去。这个过程不断重复，直到整个地图被填满。&lt;/p&gt;
&lt;h2&gt;我们的房间要求&lt;/h2&gt;
&lt;p&gt;一般的WFC瓦片中，瓦片不一定是一个最小单位。但是在这个游戏中，&lt;strong&gt;每一个瓦片/或者说是房间都是最小单位&lt;/strong&gt;。也就是说，一个瓦片中，最多只有四个出口，并且没有多出口的拼接。&lt;/p&gt;
&lt;p&gt;目前我想的方法是墙壁+地板的组装方式。即，&lt;strong&gt;墙壁负责进行控制出入口的生成（上图的黄色部分），地板则负责控制这个房间单位的特性（黑房间，灰房间？）&lt;/strong&gt;。脚本和shader则和地板强绑定。最终以上内容重新组装为Prefab。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最终成品 (Prefab)
├── 墙壁 (Wall)
│   └── 职责: 控制出入口生成 (连接性)
└── 地板 (Floor)
    ├── 职责: 控制房间单位特性
    │   ├── 强绑定: 脚本 (Script)
    │   └── 强绑定: 着色器 (Shader)
    └── 示例: 黑房间、灰房间等
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个房间有东南西北四个方向的开口，每个方向可以选择“开”或“关”。总共有 &lt;strong&gt;16 种可能性&lt;/strong&gt;，具体如下：&lt;/p&gt;
&lt;h2&gt;所有开口可能性&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;1. 无开口（四个方向都关闭）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;无&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 仅一个方向开口&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;东&lt;/li&gt;
&lt;li&gt;南&lt;/li&gt;
&lt;li&gt;西&lt;/li&gt;
&lt;li&gt;北&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 两个方向开口&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;东 + 南&lt;/li&gt;
&lt;li&gt;东 + 西&lt;/li&gt;
&lt;li&gt;东 + 北&lt;/li&gt;
&lt;li&gt;南 + 西&lt;/li&gt;
&lt;li&gt;南 + 北&lt;/li&gt;
&lt;li&gt;西 + 北&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 三个方向开口&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;东 + 南 + 西&lt;/li&gt;
&lt;li&gt;东 + 南 + 北&lt;/li&gt;
&lt;li&gt;东 + 西 + 北&lt;/li&gt;
&lt;li&gt;南 + 西 + 北&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 四个方向都开口&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;东 + 南 + 西 + 北&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总共有 &lt;strong&gt;16 种组合&lt;/strong&gt;（即 &lt;img src=&quot;/images/posts/0f25d49b.svg&quot; alt=&quot;image&quot; /&gt;），包括所有可能的子集（从完全关闭到全部开启）。&lt;/p&gt;
&lt;h3&gt;一共16种组装房间。&lt;/h3&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/5703f778.png&quot; alt=&quot;Tile Samples&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/79227f21.png&quot; alt=&quot;sample demo。可以调整种子进行不同的地图生成。但是还没有做手动约束（哪种房间只能生成几个）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/730e7fac.png&quot; alt=&quot;瓦片重声明&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/d114baf3.png&quot; alt=&quot;以路牌作为所有prefab测试，目前已经可以进行Unity-Houdini间的连接。&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Todo：Random房间属性并接入Prefab，随机摆放饰品添加画面丰富度，接入TOPS进行AI生成模型的批量减面器。&lt;/p&gt;
</content:encoded></item><item><title>C#：订阅与发布</title><link>https://fuwari.vercel.app/posts/csharp-subscribe-publish/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/csharp-subscribe-publish/</guid><description>C#：订阅与发布的详细介绍和使用方法</description><pubDate>Sat, 26 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;发布者&lt;/strong&gt;定义一系列事件，并提供一个注册方法；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;订阅者&lt;/strong&gt;向&lt;strong&gt;发布者&lt;/strong&gt;注册自己的事件处理逻辑，供一个&lt;strong&gt;可被回调的方法&lt;/strong&gt;，也就是&lt;strong&gt;事件处理程序&lt;/strong&gt;；当&lt;strong&gt;发布者&lt;/strong&gt;的事件被&lt;strong&gt;触发&lt;/strong&gt;的时候，订阅者将通过回调函数得到发布者通知，而订阅者所注册的回调函数，也就是事件处理逻辑的所有方法都会被执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发布者是指拥有某事件的类或者结构&lt;/li&gt;
&lt;li&gt;订阅者是指向发布者注册的类或者结构&lt;/li&gt;
&lt;li&gt;事件处理程序是指由订阅者注册到事件的方法，在发布者触发事件时执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://blog.csdn.net/weixin_38486884/article/details/82853508&quot;&gt;https://blog.csdn.net/weixin_38486884/article/details/82853508&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/mq0036/p/11660978.html&quot;&gt;https://www.cnblogs.com/mq0036/p/11660978.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从一个简单的例子，来说明一下这种事件消息传递的机制！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有一家三口，妈妈负责做饭，爸爸和孩子负责吃。。。将这三个人，想象成三个类。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;妈妈有一个方法，叫做&quot;做饭&quot;。有一个事件，叫做&quot;开饭&quot;。做完饭后，调用开饭事件，发布开饭消息。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;爸爸和孩子分别有一个方法，叫做&quot;吃饭&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将爸爸和孩子的&quot;吃饭&quot;方法，注册到妈妈的&quot;开饭&quot;事件。也就是，订阅妈妈的开饭消息。让妈妈做完饭开饭时，发布吃饭消息时，告诉爸爸和孩子一声。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种机制就是C#中的，订阅发布。下面用代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Program
    {
        public static void Main(string[] args)
        {
            //实例化对象
            Mom mom = new Mom();
            Dad dad = new Dad();
            Child child = new Child();
            
            //将爸爸和孩子的Eat方法注册到妈妈的Eat事件
            //订阅妈妈开饭的消息
            mom.Eat += dad.Eat;
            mom.Eat += child.Eat;
            
            //调用妈妈的Cook事件
            mom.Cook();
            
            Console.Write(&quot;Press any key to continue . . . &quot;);
            Console.ReadKey(true);
        }
    }
    
    public class Mom
    {
        //定义Eat事件，用于发布吃饭消息
        public event Action Eat;
        
        public void Cook()
        {
            Console.WriteLine(&quot;妈妈 : 饭好了&quot;);
            //饭好了，发布吃饭消息
            Eat?.Invoke();
        }
    }
    
    public class Dad
    {
        public void Eat()
        {
            //爸爸去吃饭
            Console.WriteLine(&quot;爸爸 : 吃饭了。&quot;);
        }
    }
    
    public class Child
    {
        public void Eat()
        {
            //熊孩子LOL呢，打完再吃
            Console.WriteLine(&quot;孩子 : 打完这局再吃。&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/b7dd79c2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当爷爷奶奶来做客了怎么办呢？和爸爸孩子一样，写个Eat方法，同样注册到妈妈的开饭事件就好了。&lt;/p&gt;
</content:encoded></item><item><title>Windows RDP+ddns+Shell+CloudFlare 实现远程桌面</title><link>https://fuwari.vercel.app/posts/windows-rdp-ddns-shell-cloudflare-remote-desktop/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/windows-rdp-ddns-shell-cloudflare-remote-desktop/</guid><description>Git项目中.gitignore文件的配置方法</description><pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;其实感觉Gemini讲的很好了，我就没怎么改了，我自己试着对了。就直接放上来了。类似于备忘录吧（&lt;br /&gt;
另：其实还有一个更快的ddns，叫做ddnsgo，不用shell而且是可视化：&lt;a href=&quot;https://github.com/jeessy2/ddns-go&quot;&gt;https://github.com/jeessy2/ddns-go&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;strong&gt;第一章：我们的目标与“理想中”的设置&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;最终目标：&lt;/strong&gt; 通过一个好记的域名（例如 &lt;code&gt;rdp.yourdomain.com&lt;/code&gt;），使用微软远程桌面（RDP）稳定连接到一台拥有动态公网IPv6地址的Windows电脑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心技术栈：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDP (Remote Desktop Protocol):&lt;/strong&gt; 微软强大的远程控制协议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6:&lt;/strong&gt; 新一代互联网协议，它为我们提供了海量的公网IP地址，免去了内网穿透的烦恼。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DDNS (Dynamic DNS):&lt;/strong&gt; 动态域名解析服务，当我们的公网IP变化时，它能自动将域名指向新的IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare:&lt;/strong&gt; 全球知名的CDN和DNS服务商，提供免费且强大的API来管理DNS记录，是实现DDNS的绝佳平台。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PowerShell:&lt;/strong&gt; Windows上强大的脚本语言，我们将用它来编写DDNS自动更新脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;理想中的初始设置步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开启Windows远程桌面：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在目标电脑上，进入 &lt;code&gt;设置&lt;/code&gt; &amp;gt; &lt;code&gt;系统&lt;/code&gt; &amp;gt; &lt;code&gt;远程桌面&lt;/code&gt;，开启此功能。&lt;/li&gt;
&lt;li&gt;确保你的Windows版本是专业版、企业版或服务器版。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置Windows防火墙：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在目标电脑上，搜索并打开“高级安全的 Windows Defender 防火墙”。&lt;/li&gt;
&lt;li&gt;在“入站规则”中，找到并启用“远程桌面 - 用户模式 (TCP-In)”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;准备Cloudflare信息：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一个托管在Cloudflare上的域名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区域ID (Zone ID):&lt;/strong&gt; 在域名概览页的右下角可以找到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API令牌 (API Token):&lt;/strong&gt; 在 &lt;code&gt;我的个人资料&lt;/code&gt; &amp;gt; &lt;code&gt;API令牌&lt;/code&gt; 中创建一个具有“编辑区域DNS”权限的令牌。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建DDNS脚本与计划任务：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;编写一个PowerShell脚本，用于获取本机IP并调用Cloudflare API更新DNS记录。&lt;/li&gt;
&lt;li&gt;在Windows“任务计划程序”中创建一个任务，让这个脚本每隔10分钟或30分钟自动运行一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到此为止，一切看起来都很完美。我们运行脚本，它也提示成功。然后，满怀期待地在另一台电脑上输入域名，点击连接...&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;整体流程概览&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare 端配置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;创建一个有特定权限的 API 令牌（Token），这比使用全局 API 密钥更安全。&lt;/li&gt;
&lt;li&gt;获取你的域名的区域 ID (Zone ID)。&lt;/li&gt;
&lt;li&gt;在 DNS 中预先创建一个 AAAA 记录指向你的子域名。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows 端配置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;编写一个 PowerShell 脚本，该脚本可以：
&lt;ul&gt;
&lt;li&gt;自动获取本机的公共 IPv6 地址。&lt;/li&gt;
&lt;li&gt;通过 Cloudflare API 查询当前 DDNS 记录的 IP。&lt;/li&gt;
&lt;li&gt;如果 IP 地址发生变化，则调用 API 更新该记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动化配置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用 Windows 的“任务计划程序” (Task Scheduler) 来定时自动运行这个 PowerShell 脚本，实现动态更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第一步：在 Cloudflare 进行配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在开始之前，请登录你的 Cloudflare 账户。&lt;/p&gt;
&lt;h4&gt;1. 创建 API 令牌 (API Token)&lt;/h4&gt;
&lt;p&gt;为安全起见，我们不使用全局 API 密钥，而是创建一个仅拥有 DNS 编辑权限的令牌。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 Cloudflare 主页，点击右上角你的头像，选择 “我的个人资料 (My Profile)”。&lt;/li&gt;
&lt;li&gt;在左侧菜单中，选择 “API 令牌 (API Tokens)”。&lt;/li&gt;
&lt;li&gt;点击 “创建令牌 (Create Token)” 按钮。&lt;/li&gt;
&lt;li&gt;找到 “编辑区域 DNS (Edit zone DNS)” 模板，点击 “使用模板 (Use template)”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限 (Permissions)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;第一个下拉框选择 &lt;code&gt;Zone&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二个下拉框选择 &lt;code&gt;DNS&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第三个下拉框选择 &lt;code&gt;Edit&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区域资源 (Zone Resources)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;第一个下拉框选择 &lt;code&gt;Include&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二个下拉框选择 &lt;code&gt;Specific zone&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第三个下拉框选择你想要用于 DDNS 的域名 (例如 &lt;code&gt;yourdomain.com&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 “继续以显示摘要 (Continue to summary)”。&lt;/li&gt;
&lt;li&gt;确认信息无误后，点击 “创建令牌 (Create Token)”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重要提示&lt;/strong&gt;: Cloudflare 现在会显示你的 API 令牌。&lt;strong&gt;请立即复制并妥善保管它&lt;/strong&gt;，因为这个令牌只会显示一次。后续脚本中会用到。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. 获取区域 ID (Zone ID)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;返回 Cloudflare 主页，选择你的域名。&lt;/li&gt;
&lt;li&gt;在右侧的 “API” 部分，你会看到 “区域 ID (Zone ID)”。点击 “单击以复制 (Click to copy)” 并保存下来。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3. 创建 DNS AAAA 记录&lt;/h4&gt;
&lt;p&gt;我们需要一个初始记录，以便脚本后续进行更新。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在你的域名管理页面，选择左侧的 “DNS”。&lt;/li&gt;
&lt;li&gt;点击 “添加记录 (Add record)”。&lt;/li&gt;
&lt;li&gt;填写以下信息：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型 (Type)&lt;/strong&gt;: &lt;code&gt;AAAA&lt;/code&gt; (这是用于 IPv6 的记录)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;名称 (Name)&lt;/strong&gt;: 你想要的子域名 (例如 &lt;code&gt;rdp&lt;/code&gt;，这样完整地址就是 &lt;code&gt;rdp.yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 地址 (IPv6 address)&lt;/strong&gt;: 可以先随便填一个有效的 IPv6 地址，比如 &lt;code&gt;::1&lt;/code&gt; 或者 &lt;code&gt;2001:db8::1&lt;/code&gt;。脚本会自动更新它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理状态 (Proxy status)&lt;/strong&gt;: &lt;strong&gt;务必关闭&lt;/strong&gt;！点击橙色的云朵，使其变为灰色（仅限 DNS）。RDP 协议无法通过 Cloudflare 的代理工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 “保存 (Save)”。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在，Cloudflare 这边的准备工作已经完成。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第二步：编写 Windows PowerShell 脚本&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这个脚本是整个 DDNS 系统的核心。它会自动获取并更新 IP。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在你的电脑上创建一个文件夹，用于存放脚本，例如 &lt;code&gt;C:\DDNS&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;打开记事本或任何代码编辑器（推荐 VS Code），将下面的 PowerShell 代码粘贴进去。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# =================================================================================
# Cloudflare IPv6 DDNS 更新脚本 for Windows PowerShell (最终优化版 v3)
# =================================================================================

# --- 用户配置区域 ---
# 请将下面的值替换为你自己的信息

# 1. 从 Cloudflare 仪表盘获取的 API 令牌
$apiToken = &quot;YOUR_API_TOKEN_HERE&quot;

# 2. 从 Cloudflare 域名概览页面获取的区域 ID
$zoneId = &quot;YOUR_ZONE_ID_HERE&quot;

# 3. 你想要更新的完整 DNS 记录名称 (例如: rdp.yourdomain.com)
$recordName = &quot;rdp.axonsin.software&quot;

# 4. (可选) 日志文件路径。脚本会记录 IP 变化和更新状态。
#    请确保文件夹路径存在，例如 D:\RDP_ddns\
$logFilePath = &quot;D:\RDP_ddns\cloudflare_ddns_log.txt&quot;

# --- 脚本正文 ---
# 通常无需修改以下内容

# 函数：写入日志
function Write-Log {
    param (
        [string]$Message
    )
    $logEntry = &quot;[$(Get-Date -Format &apos;yyyy-MM-dd HH:mm:ss&apos;)] $Message&quot;
    Write-Host $logEntry
    if ($logFilePath) {
        try {
            Add-Content -Path $logFilePath -Value $logEntry
        } catch {
            Write-Warning &quot;无法写入日志文件: $($_.Exception.Message)&quot;
        }
    }
}

# --- 第1步: 获取本机的公共 IPv6 地址 (优化逻辑) ---
Write-Log &quot;正在获取本机的公共 IPv6 地址...&quot;
$ipv6Address = &quot;&quot;
try {
    # 优先使用 Get-NetIPAddress 获取稳定的非临时、非本地(ULA)的公网地址
    $ipAddresses = Get-NetIPAddress -AddressFamily IPv6 -AddressState Preferred -PrefixOrigin RouterAdvertisement
    
    # 过滤掉本地地址(fe80::)和唯一本地地址(fc00::/fd00::)
    $publicIp = $ipAddresses | Where-Object { $_.IPAddress -notlike &quot;fe80::*&quot; -and $_.IPAddress -notlike &quot;fc00::*&quot; -and $_.IPAddress -notlike &quot;fd00::*&quot; } | Select-Object -First 1
    
    if ($publicIp) {
        $ipv6Address = $publicIp.IPAddress
        Write-Log &quot;通过本地命令获取到公网 IPv6 地址: $ipv6Address&quot;
    }

    # 如果本地方法无法获取到公网IP，则使用外部IPv6-Only API作为备用方案
    if (-not $ipv6Address) {
        Write-Log &quot;警告: 未能通过本地命令获取到公网 IPv6 地址。将使用外部IPv6-Only API服务...&quot;
        try {
            # 关键修改：使用仅限IPv6的API端点，并增加超时
            $ipv6Address = (Invoke-RestMethod -Uri &quot;https://api6.ipify.org&quot; -TimeoutSec 10).Trim()
            
            # 关键修改：验证获取到的是否为IPv6地址
            if ($ipv6Address -like &quot;*:*&quot;) {
                Write-Log &quot;通过外部API获取到公网 IPv6 地址: $ipv6Address&quot;
            } else {
                # 如果获取到的不是IPv6地址（例如返回了错误信息或空值），则清空变量
                Write-Log &quot;错误: 外部API返回的不是有效的IPv6地址。内容: &apos;$ipv6Address&apos;&quot;
                $ipv6Address = &quot;&quot; 
            }
        } catch {
            # 如果连API都访问不了，说明IPv6网络不通
            Write-Log &quot;错误: 无法连接到外部IPv6-Only API服务。请检查本机的公网IPv6网络连接。&quot;
            $ipv6Address = &quot;&quot;
        }
    }
    
    if ($ipv6Address) {
        Write-Log &quot;最终确认的本机 IPv6 地址为: $ipv6Address&quot;
    } else {
        throw &quot;未能获取到任何有效的公网 IPv6 地址。&quot;
    }
} catch {
    Write-Log &quot;错误: 获取本机 IPv6 地址失败。脚本终止。错误信息: $($_.Exception.Message)&quot;
    exit
}


# --- 第2步: 通过 Cloudflare API 获取当前的 DNS 记录信息 ---
Write-Log &quot;正在从 Cloudflare 获取 DNS 记录 &apos;$recordName&apos; 的信息...&quot;
$headers = @{
    &quot;Authorization&quot; = &quot;Bearer $apiToken&quot;
    &quot;Content-Type&quot;  = &quot;application/json&quot;
}

$uri_get = &quot;https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records?type=AAAA&amp;amp;name=$recordName&quot;

try {
    $response_get = Invoke-RestMethod -Uri $uri_get -Method Get -Headers $headers -TimeoutSec 15
    if ($response_get.success -ne $true) {
        throw &quot;API 请求失败: $($response_get.errors | ConvertTo-Json -Depth 5)&quot;
    }
    if ($response_get.result.Count -eq 0) {
        throw &quot;在 Cloudflare 上找不到记录 &apos;$recordName&apos; (类型 AAAA)。请先手动创建它。&quot;
    }
    
    $record = $response_get.result[0]
    $recordId = $record.id
    $currentDnsIp = $record.content
    Write-Log &quot;Cloudflare 上的当前 IP 为: $currentDnsIp&quot;
} catch {
    Write-Log &quot;错误: 获取 Cloudflare DNS 记录失败。脚本终止。错误信息: $($_.Exception.Message)&quot;
    exit
}


# --- 第3步: 比较 IP 地址并按需更新 ---
if ($ipv6Address -eq $currentDnsIp) {
    Write-Log &quot;IP 地址未发生变化。无需更新。&quot;
} else {
    Write-Log &quot;IP 地址已从 &apos;$currentDnsIp&apos; 变为 &apos;$ipv6Address&apos;。正在更新 Cloudflare 记录...&quot;
    
    $uri_update = &quot;https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$recordId&quot;
    
    $body = @{
        type    = &quot;AAAA&quot;
        name    = $recordName
        content = $ipv6Address
        ttl     = 1 # 1 = Auto
        proxied = $false # RDP 必须为 false
    } | ConvertTo-Json
    
    try {
        $response_update = Invoke-RestMethod -Uri $uri_update -Method Put -Headers $headers -Body $body -TimeoutSec 20
        if ($response_update.success -eq $true) {
            Write-Log &quot;成功! DNS 记录已更新为 &apos;$ipv6Address&apos;。&quot;
        } else {
            throw &quot;API 更新请求失败: $($response_update.errors | ConvertTo-Json -Depth 5)&quot;
        }
    } catch {
        Write-Log &quot;错误: 更新 Cloudflare DNS 记录时发生严重错误。错误信息: $($_.Exception.Message)&quot;
    }
}

Write-Log &quot;脚本执行完毕。&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;脚本配置与保存&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;修改配置&lt;/strong&gt;：将脚本中 &lt;code&gt;--- 用户配置区域 ---&lt;/code&gt; 下的 &lt;code&gt;$apiToken&lt;/code&gt;、&lt;code&gt;$zoneId&lt;/code&gt; 和 &lt;code&gt;$recordName&lt;/code&gt; 替换为你自己的信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存脚本&lt;/strong&gt;：将修改后的代码保存到你创建的文件夹中（例如 &lt;code&gt;C:\DDNS&lt;/code&gt;），文件名为 &lt;code&gt;update_ddns.ps1&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第三步：使用任务计划程序实现自动化&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;为了让 DDNS 自动运行，我们需要设置一个定时任务。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打开任务计划程序&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;按 &lt;code&gt;Win + R&lt;/code&gt; 键，输入 &lt;code&gt;taskschd.msc&lt;/code&gt;，然后按回车。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建基本任务&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在右侧的 “操作” 窗格中，点击 “创建任务...”（不要点“创建基本任务”，我们需要更多高级选项）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“常规” 选项卡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;名称&lt;/strong&gt;: 给任务起一个名字，例如 &lt;code&gt;Cloudflare IPv6 DDNS&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;: (可选) 添加一些描述，例如 &lt;code&gt;每小时更新 RDP 的 IPv6 地址&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全选项&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;选择 “不管用户是否登录都要运行”。&lt;/li&gt;
&lt;li&gt;勾选 “使用最高权限运行”。这对于执行网络相关的命令很重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“触发器” 选项卡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击 “新建...”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开始任务&lt;/strong&gt;: 选择 “按预定计划”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;选择 “每天”。&lt;/li&gt;
&lt;li&gt;在 “高级设置” 中，勾选 “重复任务间隔”，并将其设置为 “1 小时”（或你希望的更新频率）。持续时间设为 “无限期”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;确保 “已启用” 被勾选。&lt;/li&gt;
&lt;li&gt;点击 “确定”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“操作” 选项卡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击 “新建...”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作&lt;/strong&gt;: 选择 “启动程序”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序或脚本&lt;/strong&gt;: 输入 &lt;code&gt;powershell.exe&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加参数 (可选)&lt;/strong&gt;: 这是&lt;strong&gt;最关键&lt;/strong&gt;的一步。在这里输入以下内容（请根据你的脚本路径修改）：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;-ExecutionPolicy Bypass -File &quot;C:\DDNS\update_ddns.ps1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    * `-ExecutionPolicy Bypass`：绕过执行策略限制，允许脚本运行。
    * `-File &quot;C:\DDNS\update_ddns.ps1&quot;`：指定要运行的脚本文件的完整路径。
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;“条件” 选项卡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在 “网络” 部分，勾选 “只有在以下网络连接可用时才启动”。&lt;/li&gt;
&lt;li&gt;选择 “任何连接”。这确保了只有在电脑联网时脚本才会运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“设置” 选项卡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;可以保留默认设置，或者根据需要调整，例如勾选 “如果任务失败，按以下频率重新启动”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存任务&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击 “确定”。系统会提示你输入当前用户的密码，因为任务需要以你的权限运行。输入密码后，任务就创建好了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;第四步：连接和测试&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;手动测试&lt;/strong&gt;：你可以右键点击刚刚创建的任务，选择 “运行” 来立即执行一次脚本。然后检查 &lt;code&gt;C:\DDNS&lt;/code&gt; 文件夹下的日志文件，确认脚本是否成功执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接 RDP&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在另一台也支持 IPv6 的电脑上，打开 “远程桌面连接” (mstsc.exe)。&lt;/li&gt;
&lt;li&gt;在 “计算机” 字段中，输入你的完整子域名，例如 &lt;code&gt;rdp.yourdomain.com&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点击 “连接”，然后输入你 Windows 电脑的用户名和密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，你已经成功配置了基于 Cloudflare 的 IPv6-only DDNS，可以随时通过固定的域名远程访问你的 Windows 电脑了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Related searches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.google.com/search?q=powershell+cloudflare+api+update+AAAA+record&amp;amp;client=app-vertex-grounding-quora-poe&quot;&gt;powershell cloudflare api update AAAA record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.google.com/search?q=powershell+get+public+ipv6+address&amp;amp;client=app-vertex-grounding-quora-poe&quot;&gt;powershell get public ipv6 address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.google.com/search?q=windows+powershell+get+stable+temporary+ipv6+address&amp;amp;client=app-vertex-grounding-quora-poe&quot;&gt;windows powershell get stable temporary ipv6 address&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;第二章：第一次失败 - 经典的 &lt;strong&gt;&lt;code&gt;**0x204**&lt;/code&gt;&lt;/strong&gt; 错误&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;我们遭遇了第一个拦路虎，一个非常经典的错误提示：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;错误信息摘要：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;远程桌面由于以下原因之一无法连接到远程计算机：
&lt;ol&gt;
&lt;li&gt;未启用对服务器的远程访问&lt;/li&gt;
&lt;li&gt;远程计算机已关闭&lt;/li&gt;
&lt;li&gt;远程计算机在网络上不可用&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误代码: 0x204&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个错误代码的潜台词是：“&lt;strong&gt;我已经通过域名找到了正确的地址，但我去敲门了，门那边却没有任何回应。&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;这立刻排除了DNS解析本身的问题，说明我们的DDNS脚本至少在“上报地址”这一步是部分成功的。问题出在“连接”这个环节。根据经验，原因通常是防火墙。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;排错行动：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;头号嫌疑人：路由器/光猫的IPv6防火墙&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题根源：&lt;/strong&gt; 出于安全考虑，几乎所有家用网络设备的IPv6防火墙默认都会阻止所有从外部发起的连接请求。即使你的电脑获取了公网IPv6，这道“大门”也是关着的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：&lt;/strong&gt; 登录路由器或光猫的管理后台，找到“IPv6防火墙”或“安全设置”，添加一条&lt;strong&gt;入站规则&lt;/strong&gt;，允许&lt;strong&gt;协议为TCP、目标端口为3389&lt;/strong&gt;的流量通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二号嫌疑人：Windows防火墙的网络配置文件&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题根源：&lt;/strong&gt; Windows防火墙规则可能只在“专用网络”下生效，而你的网络连接可能被识别为了“公用网络”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：&lt;/strong&gt; 再次检查“远程桌面”的入站规则，在“高级”选项卡中，确保所有网络配置文件（专用、公用、域）都被勾选。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第三章：深入骨髓 - &lt;strong&gt;&lt;code&gt;**fc00::**&lt;/code&gt;&lt;/strong&gt;，错误的IP地址！&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在我们解决了防火墙的潜在问题后，再次尝试，依旧失败。这时，我们通过与运维的对话，发现了更深层次的问题。我们查看了DDNS脚本的运行日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[2025-07-17 00:40:11] 成功! DNS 记录已更新为 &apos;fc00::d461:da97:9571:809d&apos;。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题一目了然！脚本上报的是一个以 &lt;code&gt;fc00::&lt;/code&gt; 开头的地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这是什么地址？&lt;/strong&gt; 这叫做 &lt;strong&gt;唯一本地地址 (Unique Local Address, ULA)&lt;/strong&gt;。你可以把它理解为IPv6世界里的“内网IP”，类似于IPv4中的 &lt;code&gt;192.168.x.x&lt;/code&gt;。这个地址只能在你的局域网内部通信，公网是无法访问的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么会这样？&lt;/strong&gt; 因为我们最初的脚本太“天真”了，它只是简单地问操作系统：“给我一个你的IPv6地址”，而操作系统可能因为网络环境（例如路由器开启了某种地址转换）的原因，优先提供了这个本地地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案：让脚本变得“聪明”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们需要修改脚本，让它能准确地获取到&lt;strong&gt;公网IPv6地址 (Global Unicast Address, GUA)&lt;/strong&gt;，这种地址通常以&lt;code&gt;2&lt;/code&gt;或&lt;code&gt;3&lt;/code&gt;开头。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化方案 (v2脚本)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;优先通过本地命令获取IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键：&lt;/strong&gt; 明确过滤掉 &lt;code&gt;fe80::&lt;/code&gt; (链路本地) 和 &lt;code&gt;fc00::/fd00::&lt;/code&gt; (唯一本地) 的地址。&lt;/li&gt;
&lt;li&gt;如果本地找不到合适的公网IP，则启动&lt;strong&gt;备用方案&lt;/strong&gt;：访问一个公共的IP查询API（如 &lt;code&gt;api64.ipify.org&lt;/code&gt;），从外部视角来获取自己的公网IP。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;第四章：最后的决战 - IPv4与IPv6的“身份危机”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;我们满怀信心地换上了优化后的v2脚本，再次运行。然而，日志给了我们一个全新的、决定性的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[2025-07-17 00:48:41] 警告: 未能通过本地命令获取到公网 IPv6 地址。将使用外部API服务...
[2025-07-17 00:48:42] 通过外部API获取到公网 IPv6 地址: 223.65.115.238
...
[2025-07-17 00:48:49] 错误: 更新 Cloudflare DNS 记录时发生严重错误。错误信息: 远程服务器返回错误: (400) 错误的请求。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;日志分析揭示了最终的真相：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;脚本在本地确实没找到公网IPv6地址，于是启动了API备用方案。&lt;/li&gt;
&lt;li&gt;但我们的电脑在访问API网站时，不知为何&lt;strong&gt;优先走了IPv4通道&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;API网站忠实地返回了我们电脑的&lt;strong&gt;公网IPv4地址&lt;/strong&gt; (&lt;code&gt;223.65.115.238&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;脚本拿着这个IPv4地址，去请求Cloudflare更新一条 &lt;strong&gt;AAAA记录&lt;/strong&gt;（这是专门存放IPv6地址的记录）。&lt;/li&gt;
&lt;li&gt;Cloudflare API发现类型不匹配（想把IPv4塞进IPv6的槽位），于是果断拒绝，返回了 &lt;code&gt;400 Bad Request&lt;/code&gt; 错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;最终解决方案：专一且精准 (v3脚本)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们必须杜绝这种模棱两可的情况。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终优化方案 (v3脚本)：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将API地址从 &lt;code&gt;api64.ipify.org&lt;/code&gt; (双栈，返回你访问时所用的IP类型) 更换为 &lt;code&gt;**api6.ipify.org**&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这个 &lt;code&gt;api6.ipify.org&lt;/code&gt; 端点&lt;strong&gt;只能通过IPv6访问&lt;/strong&gt;。这样一来，结果只有两种：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;成功：&lt;/strong&gt; 电脑通过IPv6访问了它，并100%获取到了一个正确的公网IPv6地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败：&lt;/strong&gt; 电脑的公网IPv6网络不通，连接API直接超时或失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这彻底消除了获取到错误IP类型的可能性，让脚本的行为变得可预测且稳定。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在替换为最终版的脚本后，再次运行，日志终于显示一切正常！DNS记录被成功更新为了一个以&lt;code&gt;2&lt;/code&gt;开头的公网IPv6地址。至此，远程桌面连接成功建立！&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;第五章：锦上添花 - 让远程桌面更好用&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;成功连接只是第一步，我们还可以让体验变得更好。一个最常用的功能就是在远程会话中直接访问本地电脑的硬盘。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设置方法：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开“远程桌面连接”程序 (&lt;code&gt;mstsc&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;点击“显示选项”，进入“本地资源”选项卡。&lt;/li&gt;
&lt;li&gt;点击“详细信息...”，在弹出的窗口中勾选你想要共享的本地驱动器（如C盘、D盘）。&lt;/li&gt;
&lt;li&gt;连接后，在远程电脑的“此电脑”中，你就能看到名为“&lt;code&gt;C on 本地电脑名&lt;/code&gt;”这样的映射驱动器了，可以自由拖拽文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;结论与反思&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这次看似简单的DDNS配置，却带我们经历了一场涉及网络协议、防火墙策略、脚本逻辑的完整排错之旅。它告诉我们：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;永远不要想当然：&lt;/strong&gt; 默认设置往往是为安全而非便利服务的。尤其是路由器的IPv6防火墙，是新手最容易忽略的坑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解你使用的工具：&lt;/strong&gt; 了解IPv6地址的分类（公网GUA、唯一本地ULA、链路本地LLA）是解决问题的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编写健壮的脚本：&lt;/strong&gt; 好的脚本不仅要能完成任务，更要能预见并处理各种异常情况，比如获取到错误的IP类型。增加明确的验证和专用的工具（如IPv6-only API）至关重要。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这次详尽的记录，能为你未来的网络探索之路扫清一些障碍。享受随时随地连接的自由吧！&lt;/p&gt;
</content:encoded></item><item><title>利用安卓代理为Tortoise SVN配置代理服务器</title><link>https://fuwari.vercel.app/posts/configure-proxy-for-tortoise-svn-via-android-proxy/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/configure-proxy-for-tortoise-svn-via-android-proxy/</guid><description>利用安卓代理为Tortoise SVN配置代理服务器的详细介绍和使用方法</description><pubDate>Wed, 23 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TortoiseSVN 无法刷新，而浏览器可以访问 SVN 服务器的问题，确实是一个非常常见的现象。这通常是因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用程序代理设置的差异：&lt;/strong&gt; 您的浏览器（如 Chrome, Edge, Firefox 等）默认会遵循 Windows 系统的代理设置（也就是您刚才配置的“Internet 选项”里的代理）。但是，很多其他应用程序，尤其是开发工具、命令行工具或者一些独立的客户端，&lt;strong&gt;并不会自动使用 Windows 的系统代理设置&lt;/strong&gt;。它们需要单独配置代理，或者有自己的代理设置逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TortoiseSVN 的代理配置：&lt;/strong&gt; TortoiseSVN 是 Subversion 的一个 GUI 客户端，它底层使用的是 Subversion 库。Subversion 有自己的配置文件来管理代理设置，而不是依赖操作系统的全局代理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;如何为 TortoiseSVN 配置代理&lt;/h3&gt;
&lt;p&gt;您需要编辑 Subversion 的配置文件，通常是一个名为 &lt;code&gt;servers&lt;/code&gt; 的文件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;找到 Subversion 配置文件目录：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;打开文件资源管理器。&lt;/li&gt;
&lt;li&gt;在地址栏输入 &lt;code&gt;%APPDATA%\Subversion&lt;/code&gt; 并按回车。&lt;/li&gt;
&lt;li&gt;这会带您到 Subversion 的用户配置文件目录，通常是 &lt;code&gt;C:\Users\您的用户名\AppData\Roaming\Subversion&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编辑 &lt;strong&gt;&lt;code&gt;**servers**&lt;/code&gt;&lt;/strong&gt; 文件：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在这个目录下，您会找到一个名为 &lt;code&gt;servers&lt;/code&gt; 的文件。&lt;/li&gt;
&lt;li&gt;用记事本（Notepad）或任何文本编辑器打开它。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置代理设置：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;servers&lt;/code&gt; 文件中，找到 &lt;code&gt;[global]&lt;/code&gt; 部分。如果找不到，您可以在文件末尾添加。&lt;/li&gt;
&lt;li&gt;在该部分下，添加或修改以下行来设置您的代理服务器信息：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[global]
# 如果您的Every Proxy提供的是HTTP代理，请使用以下两行
http-proxy-host = 192.168.1.3  ; 替换为Every Proxy在电脑上的IP地址
http-proxy-port = 8080         ; 替换为Every Proxy监听的端口

# 如果您的代理需要用户名和密码（Every Proxy通常不需要，但以防万一）
# http-proxy-username = your_username
# http-proxy-password = your_password

# 如果您不希望某些地址通过代理，可以在这里添加例外
# http-proxy-exceptions = localhost,127.0.0.1,*.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- **重要提示：**
    * `http-proxy-host` 和 `http-proxy-port` 应该填写您在 Windows 系统代理设置中填写的 Every Proxy 的 IP 地址和端口。
    * Subversion 的 `servers` 文件主要支持 **HTTP/HTTPS 代理**。如果您的 Every Proxy 提供的是 SOCKS5 代理，并且您希望 TortoiseSVN 使用 SOCKS5，那么仅通过 `servers` 文件可能无法直接实现。
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保存并关闭文件。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重启 TortoiseSVN：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;关闭所有正在运行的 TortoiseSVN 实例（包括右键菜单）。&lt;/li&gt;
&lt;li&gt;重新打开 TortoiseSVN，尝试刷新或执行 SVN 操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;关于 SOCKS5 代理的额外说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every Proxy 的能力：&lt;/strong&gt; Every Proxy 通常可以同时提供 HTTP/HTTPS 代理和 SOCKS5 代理。
&lt;ul&gt;
&lt;li&gt;如果您在手机上配置 Every Proxy 时，同时开启了 HTTP/HTTPS 代理功能，并且您电脑的 Windows 系统代理设置连接的是 Every Proxy 的 HTTP 端口，那么 TortoiseSVN 也可以通过上述方法连接到这个 HTTP 代理。&lt;/li&gt;
&lt;li&gt;如果 Every Proxy &lt;strong&gt;只&lt;/strong&gt;提供了 SOCKS5 代理，而您的公司 SVN 服务器必须通过 SOCKS5 才能访问，那么 TortoiseSVN 自身通过 &lt;code&gt;servers&lt;/code&gt; 文件配置 SOCKS5 的支持是有限的。在这种情况下，您可能需要使用一个 &lt;strong&gt;SOCKSifier&lt;/strong&gt; 软件（例如 Proxifier, SocksCap64 等）。这些软件可以在系统层面强制特定应用程序的所有网络流量都通过 SOCKS5 代理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;建议：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;首先确认 Every Proxy 是否提供了 HTTP 代理。&lt;/strong&gt; 如果有，直接在 &lt;code&gt;servers&lt;/code&gt; 文件中配置该 HTTP 代理的 IP 和端口是最简单的方法。&lt;/li&gt;
&lt;li&gt;如果确认必须使用 SOCKS5，并且 TortoiseSVN 无法直接通过 &lt;code&gt;servers&lt;/code&gt; 文件工作，再考虑使用 SOCKSifier 软件。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Houdini&amp;Unity同步刷新工程文件结构</title><link>https://fuwari.vercel.app/posts/houdini-unity-sync-project-structure/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/houdini-unity-sync-project-structure/</guid><description>Houdini&amp;Unity同步刷新工程文件结构的详细介绍和使用方法</description><pubDate>Thu, 17 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在Houdini中对HDA（Houdini数字资产）进行更改后，可以通过以下几种方式在Unity中刷新它：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;会话同步 (Session Sync)&lt;/strong&gt;：&lt;br /&gt;
这是最推荐和最集成的工作流程，尤其适用于实时开发。通过会话同步，您在Houdini中对HDA所做的更改可以自动实时地反映在Unity中。它甚至支持Unity和Houdini视口的同步，提供无缝的创作体验。 &lt;a href=&quot;https://www.reddit.com/r/Houdini/comments/hc5377/the_new_houdini_engine_for_unity_update_is/&quot;&gt;[1]&lt;/a&gt;&lt;a href=&quot;https://www.sidefx.com/docs/houdini/unity/about.html&quot;&gt;[2]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新烹饪资产 (Recook Asset)&lt;/strong&gt;：&lt;br /&gt;
在Unity中，当您在场景中选择HDA的实例时，通常可以在其Inspector窗口中找到一个名为“Recook Asset”（重新烹饪资产）的按钮。这个操作会将Unity中HDA参数的更改上传到Houdini Engine，并强制HDA在Houdini中重新计算和生成输出。这适用于您只更改了HDA参数的情况。 &lt;a href=&quot;https://www.sidefx.com/docs/unity19.0/_assets.html&quot;&gt;[3]&lt;/a&gt;&lt;a href=&quot;https://www.sidefx.com/forum/topic/67677/&quot;&gt;[4]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重建资产 (Rebuild Asset)&lt;/strong&gt;：&lt;br /&gt;
如果HDA文件本身在Houdini中被修改并重新保存（例如，修改了节点网络、添加或删除了参数），那么建议使用“Rebuild Asset”（重建资产）功能。这个操作会确保Unity加载HDA的最新版本，重新创建所有内容，然后尝试重新应用之前设置的参数值。 &lt;a href=&quot;https://www.sidefx.com/docs/unity19.0/_assets.html&quot;&gt;[3]&lt;/a&gt;&lt;a href=&quot;https://www.sidefx.com/forum/topic/67677/&quot;&gt;[4]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存HDA定义&lt;/strong&gt;：&lt;br /&gt;
在Houdini中对HDA进行任何更改后，请务必保存HDA的定义。这通常通过Houdini菜单中的 &lt;code&gt;File &amp;gt; Save Asset&lt;/code&gt; 或 &lt;code&gt;Asset &amp;gt; Save Asset&lt;/code&gt; 来完成。保存后，Unity才能检测到HDA文件的更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动烹饪参数更改 (Auto-Cook On Parameter Change)&lt;/strong&gt;：&lt;br /&gt;
默认情况下，当您在Unity UI中更改HDA的参数时，HDA会自动重新烹饪。如果HDA的烹饪过程耗时较长，您可以在HDA的选项中关闭此功能，然后手动点击“Recook Asset”按钮来控制刷新。 &lt;a href=&quot;https://www.sidefx.com/docs/unity19.0/_assets.html&quot;&gt;[3]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关闭所有会话 (Close All Sessions)&lt;/strong&gt;：&lt;br /&gt;
如果HDA在Unity中没有按预期更新，特别是当您遇到会话冲突时，可以尝试在Unity中导航到 &lt;code&gt;HoudiniEngine&lt;/code&gt; 菜单，然后选择 &lt;code&gt;Session &amp;gt; Close All Sessions&lt;/code&gt;。关闭所有会话后，您可以再次尝试加载或重新烹饪/重建HDA。 &lt;a href=&quot;https://www.sidefx.com/forum/topic/67677/&quot;&gt;[4]&lt;/a&gt;&lt;a href=&quot;https://www.sidefx.com/forum/post/332045/&quot;&gt;[5]&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通常情况下，如果您在Houdini中修改了HDA的内部逻辑或结构，保存HDA文件后，回到Unity并选择HDA实例，点击“Rebuild Asset”即可刷新。如果您只是修改了HDA的参数，那么“Recook Asset”就足够了，或者如果开启了自动烹饪，更改参数后会自动刷新。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Learn more:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/Houdini/comments/hc5377/the_new_houdini_engine_for_unity_update_is/&quot;&gt;The new Houdini Engine for Unity update is amazing. It could be a complete game-changer! (Session Sync) - Reddit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sidefx.com/docs/houdini/unity/about.html&quot;&gt;Introduction to Houdini Engine for Unity - SideFX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sidefx.com/docs/unity19.0/_assets.html&quot;&gt;Houdini Engine for Unity: Houdini Assets - SideFX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sidefx.com/forum/topic/67677/&quot;&gt;HDA not updating inside Unity | Forums - SideFX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sidefx.com/forum/post/332045/&quot;&gt;HDA not updating inside Unity | Forums - SideFX&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>《碧蓝航线》：互动宿舍逆向</title><link>https://fuwari.vercel.app/posts/azur-lane-live2d-dorm-reverse-engineering/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/azur-lane-live2d-dorm-reverse-engineering/</guid><description>Git项目中.gitignore文件的配置方法</description><pubDate>Mon, 14 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. &lt;strong&gt;基本信息&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;截帧来源：RenderDocv1.29，使用开启开发者模式的酷比魔方mini2截帧。&lt;/li&gt;
&lt;li&gt;测试设备：MTK G99, Pixel Experience 安卓13，刷入termux/magisk-Hide config&lt;/li&gt;
&lt;li&gt;碧蓝航线互动宿舍&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;strong&gt;总体资源与调用统计&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;这里用的是能代的对话界面，基本信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Stats for azur3d.rdc.

File size: 281.55MB (444.54MB uncompressed, compression ratio 1.58:1)
Persistent Data (approx): 0.67MB, Frame-initial data (approx): 249.04MB

*** Summary ***

Draw calls: 256
Dispatch calls: 9
API calls: 3764
API:Draw/Dispatch call ratio: 14.2038

230 Textures - 186.32 MB (186.29 MB over 32x32), 16 RTs - 63.35 MB.
Avg. tex dimension: 634.794x620.949 (682.954x677.342 over 32x32)
249 Buffers - 11.08 MB total 0.48 MB IBs 3.72 MB VBs.
260.74 MB - Grand total GPU buffer + texture load.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;256个Drawcall，看着还行？&lt;/p&gt;
&lt;p&gt;9个compute shader调用，合计230纹理，16个RenderTarget，249个buffer，占用ram约260MB。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/9568ade9.png&quot; alt=&quot;可以看到大部分计算资源在colorpass上&quot; /&gt;&lt;/p&gt;
&lt;p&gt;画面输出Backbuffer：R11G11B10,很经典的32位输出。&lt;/p&gt;
&lt;h2&gt;3. &lt;strong&gt;渲染流程与Pass分析&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;大致渲染顺序：&lt;/h3&gt;
&lt;p&gt;Depth渲染摄像机视角和光源视角-房间内景-房间外景-人物角色-后处理。AO遮蔽光采用烘培贴图的方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/2df8a9f9.png&quot; alt=&quot;整个Pass中没有启用高级Shader&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整个渲染过程没有激活细分着色器（TCS、TES），几何着色器（GS），计算着色器（CS）只用于提交顶点组。也就是说不用拆里面复杂的包了！当然也和移动端性能有点关系。&lt;/p&gt;
&lt;p&gt;没有合批（Instancing/SRP batcher）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个Pass的Draw Call/资源消耗&lt;/li&gt;
&lt;li&gt;关键渲染阶段的调用结构（如：GBuffer Pass、Forward Pass、后处理Pass等）&lt;/li&gt;
&lt;li&gt;主要材质/Shader使用分布&lt;/li&gt;
&lt;li&gt;是否有合批（Instancing/SRP Batcher）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Depth Pass：&lt;/h3&gt;
&lt;p&gt;深度测试的时候，会使用极其简单的Shader（通常只包含顶点变换，不输出颜色，只写深度）。不做光照、不做着色、不输出颜色缓冲，会将所有的物体遍历并且将每个物体深度值写到深度缓冲区。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/e89f1e09.png&quot; alt=&quot;第一个Pass，记录摄像机的深度。可以剔除像素防止overdraw&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/8bcc45bc.png&quot; alt=&quot;第二个pass。很明显是日光渲染的深度图，一眼Shadowmap的阴影采样，和摄像机的深度图配合就可以出阴影了&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看完了深度测试就可以知道了，采用的是shadowmap形式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/f7f4ee31.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/f8bfbc9a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是唯一有点疑问的是为什么要像depth pass的时候会输入basemap和法线贴图？有点奇怪。而且这个贴图在后续的pass不会出现第二次，大部分还是花盆/叶子这种微小部件。这个不是很理解。难道是为了减少后期colorpass的带宽压力？&lt;/p&gt;
&lt;h3&gt;Color Pass&lt;/h3&gt;
&lt;p&gt;这里直接挑了一个比较明显的渲染衣服状态下的colorpass。第一个pass负责渲染整个场景的光照状态；第二个pass负责混合叠加渲染的场景AO图，和特殊形态的场景（水和树叶）。最后叠加后处理Pass（风格化LUTs+Bloom）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/cb138439.png&quot; alt=&quot;双阶段Pass，有很明显的分工&quot; /&gt;&lt;/p&gt;
&lt;p&gt;挑一个第二个pass末状态下的backbuffer看一下里边有什么：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/e6c9adc0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大致分成三种类型：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;通用型场景Shader&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特异型场景Shader（水、树叶、玻璃）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人物shader&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通用型Shader就是一般的场景Shader，很大概率是Unity Lit。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/632c9945.png&quot; alt=&quot;通用场景使用base、Bump、ILM。&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通用Shader输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BaseMap&lt;/li&gt;
&lt;li&gt;BumpMap&lt;/li&gt;
&lt;li&gt;ILMMap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很经典的Lit输入&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/45cc1723.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;特异型场景Shader则会使用比较特殊的贴图，全部在二阶段才会渲染。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/fcb18d6f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;水面下的状态输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BumpMap(normal)&lt;/li&gt;
&lt;li&gt;CausticMap（光学焦散）&lt;/li&gt;
&lt;li&gt;DepthWaveHeightMap(黑白Ramp图）&lt;/li&gt;
&lt;li&gt;NoiseMap&lt;/li&gt;
&lt;li&gt;dayWater_RampMap(深度彩色Ramp）&lt;/li&gt;
&lt;li&gt;WaterDepthSDFMap（SDF）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/f9dce9a2.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;/images/posts/070abae4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等一下，这个水的贴图是不是太多了，明明所有场景都没有海边的场景。能想到的估计就是蓝色星源那里调人来了？&lt;/p&gt;
&lt;p&gt;云彩下的状态输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NoiseMap2Cloud&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/be11bc30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;树叶的状态输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;_LSABMap = birch02_leaf_diff&lt;br /&gt;
&lt;img src=&quot;/images/posts/a1688cf5.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;人物部分输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BaseMap&lt;/li&gt;
&lt;li&gt;BumpMap&lt;/li&gt;
&lt;li&gt;BodyMatcap(实际上这个Matcap和眼睛是一起用的）&lt;/li&gt;
&lt;li&gt;ILMMap&lt;/li&gt;
&lt;li&gt;RampMap（六行）&lt;/li&gt;
&lt;li&gt;FaceShadingGradeMask（其实就是SDF）&lt;/li&gt;
&lt;li&gt;FaceRampmap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c6269f2e.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;/images/posts/52d364ff.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;/images/posts/6c148b6f.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;/images/posts/503a9d5d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4. &lt;strong&gt;资源细节分析&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;纹理列表及格式、分辨率分布&lt;/h3&gt;
&lt;p&gt;纹理采用1024x1024，分为11个mip层级，可以根据视场角调整渲染的精度层级。没什么好说的&lt;/p&gt;
&lt;p&gt;RenderTarget有：&lt;/p&gt;
&lt;p&gt;RT 0: R8G8B8A8    (主颜色)&lt;/p&gt;
&lt;p&gt;RT 1: R8G8_UNORM  (法线XY)&lt;/p&gt;
&lt;p&gt;Depth: D16_UNORM  (16位深度)&lt;/p&gt;
&lt;h2&gt;5. &lt;strong&gt;性能分析&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;可能影响性能的热点（如大量小Draw Call、高频RT切换、大尺寸贴图）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;*** Summary ***

Draw calls: 256
Dispatch calls: 9
API calls: 3764
API:Draw/Dispatch call ratio: 14.2038

230 Textures - 186.32 MB (186.29 MB over 32x32), 16 RTs - 63.35 MB.
Avg. tex dimension: 634.794x620.949 (682.954x677.342 over 32x32)
249 Buffers - 11.08 MB total 0.48 MB IBs 3.72 MB VBs.
260.74 MB - Grand total GPU buffer + texture load.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平均的顶点引用其实不是很多，角色设计部分对于顶点的优化还不错&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/7aca77e1.png&quot; alt=&quot;占据人物最大的衣服比例只有14964的顶点引用&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6. &lt;strong&gt;特殊技术/优化点&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;没有动态合批/静态合批/Instancing。&lt;/li&gt;
&lt;li&gt;渲染队列和分组策略不错，甚至专门划了第二轮Pass给特殊物体（云、叶子、水）和AO混合补全。&lt;/li&gt;
&lt;li&gt;是一些小物件的贴图（比如花盆）在depthpass的时候就已经存进去了，怀疑这是一个小Trick，减少了对于ColorPass部分的带宽压力？&lt;/li&gt;
&lt;li&gt;Mipmap有11级，还行&lt;img src=&quot;/images/posts/b14cad11.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. &lt;strong&gt;Shader&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;好吧说实话我看中的就是Shader，直接扒开角色渲染阶段的DXBC源码看一下里面的变量声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UNITY_BINDING(0) uniform UnityPerMaterial {
	mediump float _Cutoff;
	mediump float _BaseSettingEnable;
	mediump vec4 _BaseColor;
	mediump vec4 _MainLightLuminanceRemap;
	mediump float _HeadSettingEnable;
	mediump float _BumpScale;
	mediump float _CombineMatcap;
	mediump float _MatcapMultiplyEnable;
	mediump vec4 _MatcapMultiTint;
	mediump float _MatcapMultiIntensity;
	mediump vec4 _MatcapMultiRemap;
	mediump vec4 _MatcapMultiShadowTint;
	mediump float _SelfShadowEnable;
	mediump vec4 _ShadingOffsetRemap;
	mediump float _ShadingOffsetStrength;
	mediump float _MirrorForward;
	mediump float _MirrorU;
	mediump vec4 _FaceCenterUvAndExtend;
	mediump float _FaceShadingOffset;
	mediump float _FaceShadingSoftness;
	mediump float _FaceExpressionAlpha;
	mediump float _FaceGradient;
	mediump vec4 _FaceGradientColor;
	mediump float _FaceGradientPow;
	mediump float _FaceGradientOffset;
	mediump vec4 _FaceLocalHeightBound;
	mediump float _DirShadowEnable;
	mediump vec4 _DirShadowRemap;
	mediump float _DirShadowStrength;
	mediump vec4 _DirShadowTint;
	mediump float _ScreenSpaceRimShadowEnable;
	mediump float _UseCustomLightDir;
	mediump float _LightDirVSOffset;
	mediump float _SreenSamplingDilation;
	mediump vec4 _DepthShadowRemap;
	mediump vec4 _DepthRimRemap;
	mediump vec4 _SSDepthShadowRemap;
	mediump vec4 _SSdepthRimLightRemap;
	mediump vec4 _DepthDiffShadowTint;
	mediump float _StockingEnable;
	mediump vec4 _StockingColor;
	mediump vec4 _StockingShadingRemap;
	mediump vec4 _StockingShadowColor;
	mediump float _StockingPower;
	mediump vec4 _StockingFresnelTint;
	mediump float _StockingFresnelPower;
	mediump float _StockingStretching;
	mediump float _StockingThicknessMulti;
	mediump float _SkinPower;
	mediump float _StockingThickness;
	mediump vec4 _SkinTransmittanceTint;
	mediump float _Shininess;
	mediump vec4 _ShadowAffectRemap;
	mediump vec4 _SpecularAttenRemap;
	mediump vec4 _SpecularRemap;
	mediump float _SpecularSize;
	mediump vec3 _ActualSpecularTint;
	mediump float _BaseColorAffected;
	mediump vec4 _ShiftMap_ST;
	mediump vec4 _SpotnessTilingOffset;
	mediump vec4 _SpotnessRemap;
	mediump float _1stShiftStrength;
	mediump float _2ndShiftStrength;
	mediump vec4 _StrandExp;
	mediump vec4 _1stKajiyaKaySpecularTint;
	mediump vec4 _2ndKajiyaKaySpecularTint;
	mediump vec4 _ShadowAffectedRemap;
	mediump float _SpecialHighlightEnable;
	mediump float _ParallaxScale;
	mediump float _HighlightSize;
	mediump vec4 _ActualSpecialHighlightTint;
	mediump vec4 _MatcapAdditiveMaskRemap;
	mediump vec4 _ActualMatcapAdditiveTint;
	mediump float _MatcapAdditiveAmount;
	mediump float _DisableScreenSpaceRim;
	mediump float _LightDirOffset;
	mediump vec4 _ActualRimLightTint;
	mediump float _RimlightThreshold;
	mediump float _RimlightFeather;
	mediump float _RimLightColorAffected;
	mediump vec4 _ShadowingRemap;
	mediump float _DropletsEnable;
	mediump float _DropletsRotation;
	mediump float _RainMaskTiling;
	mediump float _RainDropStaticSize;
	mediump float _RainDropStaticDensity;
	mediump float _RainDropStaticTimeSpeed;
	mediump float _RainDropTimeSpeed;
	mediump float _RainDropDownSpeed;
	mediump float _RainDropSize;
	mediump float _Roughness;
	mediump float _DropletFresnelIntensity;
	mediump float _DropletsDiffuseIntensity;
	mediump float _DropletsCausiticMul;
	mediump float _DropletsSpecIntensity;
	mediump float _DropletsFlowSpeed;
	mediump float _WetnessNormalStrength;
	mediump float _Porosity;
	mediump vec4 _ActualEmissionTint;
	mediump float _AdditiveLightIntensity;
	mediump float _GlobalIlluminationEnable;
	mediump float _IndirectDiffuseIntensity;
	mediump float _GlossyReflectionRoughness;
	mediump float _GlossyReflectionIntensity;
	mediump float _TraditionalOutlineEnable;
	mediump float _OutlineWidth;
	mediump float _OutlineZOffset;
	mediump float _OutlineMaxDistance;
	mediump float _OutlineDistanceFade;
	mediump vec4 _OutlineTintPreLight;
	mediump vec4 _OutlineTintAfterLight;
	mediump vec4 _OutlineColorReplace;
	mediump vec4 _HairTransparentRemap;
	mediump float _DebugCaseItem;
	mediump vec4 _HeadCenter;
	mediump vec4 _HeadForward;
	mediump vec4 _HeadRight;
	mediump vec4 _HeadUp;
	mediump vec4 _BodyCenter;
	mediump vec4 _BodyExtent;
	mediump vec4 _BodyForward;
	mediump vec4 _BodyRight;
	mediump vec4 _BodyUp;
	mediump float _CharacterInd;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后直接喂给GPT（该说这是llm语言模型的天性吗，太爽了），回答是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础色/高光/法线/Matcap等通用PBR属性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;角色定制化参数（面部、头发、丝袜、身体）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;丰富的特殊效果（雨滴、湿润、描边、Rim、发光等）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阴影/光源/环境光/全局光照控制&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试和多角色支持&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要分门别类，那就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UnityPerMaterial 角色材质参数
├── 🎨 基础渲染设置 (Base Rendering)
│   ├── 基础控制
│   │   ├── float _Cutoff                    // Alpha裁剪阈值
│   │   ├── float _BaseSettingEnable         // 基础设置开关
│   │   └── vec4 _BaseColor                  // 基础颜色
│   └── 主光源设置
│       └── vec4 _MainLightLuminanceRemap    // 主光源亮度重映射
│
├── 👤 面部渲染系统 (Face Rendering)
│   ├── 面部基础设置
│   │   ├── float _HeadSettingEnable         // 头部设置开关
│   │   ├── float _MirrorForward             // 前向镜像
│   │   ├── float _MirrorU                   // U坐标镜像
│   │   └── vec4 _FaceCenterUvAndExtend      // 面部中心UV和扩展
│   ├── 面部阴影系统
│   │   ├── float _FaceShadingOffset         // 面部阴影偏移
│   │   ├── float _FaceShadingSoftness       // 面部阴影柔和度
│   │   └── float _FaceExpressionAlpha       // 面部表情Alpha
│   ├── 面部渐变效果
│   │   ├── float _FaceGradient              // 面部渐变开关
│   │   ├── vec4 _FaceGradientColor          // 面部渐变颜色
│   │   ├── float _FaceGradientPow           // 面部渐变强度
│   │   └── float _FaceGradientOffset        // 面部渐变偏移
│   └── 面部区域定义
│       └── vec4 _FaceLocalHeightBound       // 面部局部高度边界
│
├── 🌑 阴影渲染系统 (Shadow System)
│   ├── 自阴影
│   │   ├── float _SelfShadowEnable          // 自阴影开关
│   │   ├── vec4 _ShadingOffsetRemap         // 阴影偏移重映射
│   │   └── float _ShadingOffsetStrength     // 阴影偏移强度
│   ├── 方向阴影
│   │   ├── float _DirShadowEnable           // 方向阴影开关
│   │   ├── vec4 _DirShadowRemap             // 方向阴影重映射
│   │   ├── float _DirShadowStrength         // 方向阴影强度
│   │   └── vec4 _DirShadowTint              // 方向阴影色调
│   ├── 屏幕空间阴影
│   │   ├── float _ScreenSpaceRimShadowEnable // 屏幕空间边缘阴影开关
│   │   ├── float _SreenSamplingDilation      // 屏幕采样扩张
│   │   ├── vec4 _SSDepthShadowRemap         // SS深度阴影重映射
│   │   └── vec4 _SSdepthRimLightRemap       // SS深度边缘光重映射
│   ├── 深度阴影
│   │   ├── vec4 _DepthShadowRemap           // 深度阴影重映射
│   │   ├── vec4 _DepthRimRemap              // 深度边缘重映射
│   │   └── vec4 _DepthDiffShadowTint        // 深度差阴影色调
│   ├── 自定义光照
│   │   ├── float _UseCustomLightDir         // 使用自定义光照方向
│   │   ├── float _LightDirVSOffset          // 光照方向视空间偏移
│   │   └── float _LightDirOffset            // 光照方向偏移
│   └── 阴影影响
│       ├── vec4 _ShadowAffectRemap          // 阴影影响重映射
│       └── vec4 _ShadowingRemap             // 阴影重映射
│
├── 🧦 丝袜渲染系统 (Stocking System)
│   ├── 丝袜基础
│   │   ├── float _StockingEnable            // 丝袜开关
│   │   ├── vec4 _StockingColor              // 丝袜颜色
│   │   ├── vec4 _StockingShadingRemap       // 丝袜阴影重映射
│   │   └── vec4 _StockingShadowColor        // 丝袜阴影颜色
│   ├── 丝袜材质属性
│   │   ├── float _StockingPower             // 丝袜强度
│   │   ├── float _StockingStretching        // 丝袜拉伸
│   │   ├── float _StockingThickness         // 丝袜厚度
│   │   ├── float _StockingThicknessMulti    // 丝袜厚度倍数
│   │   └── float _SkinPower                 // 皮肤强度
│   ├── 丝袜菲涅尔效果
│   │   ├── vec4 _StockingFresnelTint        // 丝袜菲涅尔色调
│   │   └── float _StockingFresnelPower      // 丝袜菲涅尔强度
│   └── 皮肤透射
│       └── vec4 _SkinTransmittanceTint      // 皮肤透射色调
│
├── ✨ 高光系统 (Specular System)
│   ├── 基础高光
│   │   ├── float _Shininess                 // 光泽度
│   │   ├── vec4 _SpecularAttenRemap         // 高光衰减重映射
│   │   ├── vec4 _SpecularRemap              // 高光重映射
│   │   ├── float _SpecularSize              // 高光大小
│   │   ├── vec3 _ActualSpecularTint         // 实际高光色调
│   │   └── float _BaseColorAffected         // 基础颜色影响
│   ├── 头发高光 (Kajiya-Kay)
│   │   ├── vec4 _ShiftMap_ST                // 移位贴图UV变换
│   │   ├── vec4 _SpotnessTilingOffset       // 光斑平铺偏移
│   │   ├── vec4 _SpotnessRemap              // 光斑重映射
│   │   ├── float _1stShiftStrength          // 第一层移位强度
│   │   ├── float _2ndShiftStrength          // 第二层移位强度
│   │   ├── vec4 _StrandExp                  // 发丝指数
│   │   ├── vec4 _1stKajiyaKaySpecularTint   // 第一层Kajiya-Kay高光色调
│   │   ├── vec4 _2ndKajiyaKaySpecularTint   // 第二层Kajiya-Kay高光色调
│   │   └── vec4 _ShadowAffectedRemap        // 阴影影响重映射
│   └── 特殊高光
│       ├── float _SpecialHighlightEnable    // 特殊高光开关
│       ├── float _ParallaxScale             // 视差缩放
│       ├── float _HighlightSize             // 高光大小
│       └── vec4 _ActualSpecialHighlightTint // 实际特殊高光色调
│
├── 🔮 Matcap系统 (Matcap Effects)
│   ├── 基础Matcap
│   │   ├── float _BumpScale                 // 法线缩放
│   │   └── float _CombineMatcap             // 组合Matcap
│   ├── 乘法Matcap
│   │   ├── float _MatcapMultiplyEnable      // 乘法Matcap开关
│   │   ├── vec4 _MatcapMultiTint            // 乘法Matcap色调
│   │   ├── float _MatcapMultiIntensity      // 乘法Matcap强度
│   │   ├── vec4 _MatcapMultiRemap           // 乘法Matcap重映射
│   │   └── vec4 _MatcapMultiShadowTint      // 乘法Matcap阴影色调
│   └── 加法Matcap
│       ├── vec4 _MatcapAdditiveMaskRemap    // 加法Matcap遮罩重映射
│       ├── vec4 _ActualMatcapAdditiveTint   // 实际加法Matcap色调
│       └── float _MatcapAdditiveAmount      // 加法Matcap数量
│
├── 💫 边缘光系统 (Rim Light)
│   ├── 边缘光控制
│   │   ├── float _DisableScreenSpaceRim     // 禁用屏幕空间边缘光
│   │   ├── vec4 _ActualRimLightTint         // 实际边缘光色调
│   │   ├── float _RimlightThreshold         // 边缘光阈值
│   │   ├── float _RimlightFeather           // 边缘光羽化
│   │   └── float _RimLightColorAffected     // 边缘光颜色影响
│   └── 透明度控制
│       └── vec4 _HairTransparentRemap       // 头发透明度重映射
│
├── 💧 水滴系统 (Droplets System)
│   ├── 水滴基础设置
│   │   ├── float _DropletsEnable            // 水滴开关
│   │   ├── float _DropletsRotation          // 水滴旋转
│   │   ├── float _RainMaskTiling            // 雨水遮罩平铺
│   │   └── float _DropletsFlowSpeed         // 水滴流动速度
│   ├── 静态水滴
│   │   ├── float _RainDropStaticSize        // 静态雨滴大小
│   │   ├── float _RainDropStaticDensity     // 静态雨滴密度
│   │   └── float _RainDropStaticTimeSpeed   // 静态雨滴时间速度
│   ├── 动态水滴
│   │   ├── float _RainDropTimeSpeed         // 雨滴时间速度
│   │   ├── float _RainDropDownSpeed         // 雨滴下落速度
│   │   └── float _RainDropSize              // 雨滴大小
│   ├── 水滴材质属性
│   │   ├── float _Roughness                 // 粗糙度
│   │   ├── float _DropletFresnelIntensity   // 水滴菲涅尔强度
│   │   ├── float _DropletsDiffuseIntensity  // 水滴漫反射强度
│   │   ├── float _DropletsCausiticMul       // 水滴焦散倍数
│   │   ├── float _DropletsSpecIntensity     // 水滴高光强度
│   │   ├── float _WetnessNormalStrength     // 湿润法线强度
│   │   └── float _Porosity                  // 孔隙度
│   └── 水滴物理属性
│
├── 🌟 发光系统 (Emission &amp;amp; Lighting)
│   ├── 自发光
│   │   ├── vec4 _ActualEmissionTint         // 实际自发光色调
│   │   └── float _AdditiveLightIntensity    // 加法光强度
│   └── 全局光照
│       ├── float _GlobalIlluminationEnable  // 全局光照开关
│       ├── float _IndirectDiffuseIntensity  // 间接漫反射强度
│       ├── float _GlossyReflectionRoughness // 光泽反射粗糙度
│       └── float _GlossyReflectionIntensity // 光泽反射强度
│
├── 📐 描边系统 (Outline System)
│   ├── 描边控制
│   │   ├── float _TraditionalOutlineEnable  // 传统描边开关
│   │   ├── float _OutlineWidth              // 描边宽度
│   │   ├── float _OutlineZOffset            // 描边Z偏移
│   │   ├── float _OutlineMaxDistance        // 描边最大距离
│   │   └── float _OutlineDistanceFade       // 描边距离淡化
│   └── 描边颜色
│       ├── vec4 _OutlineTintPreLight        // 描边光照前色调
│       ├── vec4 _OutlineTintAfterLight      // 描边光照后色调
│       └── vec4 _OutlineColorReplace        // 描边颜色替换
│
├── 📍 角色空间定义 (Character Space)
│   ├── 头部空间
│   │   ├── vec4 _HeadCenter                 // 头部中心
│   │   ├── vec4 _HeadForward                // 头部前向
│   │   ├── vec4 _HeadRight                  // 头部右向
│   │   └── vec4 _HeadUp                     // 头部上向
│   ├── 身体空间
│   │   ├── vec4 _BodyCenter                 // 身体中心
│   │   ├── vec4 _BodyExtent                 // 身体范围
│   │   ├── vec4 _BodyForward                // 身体前向
│   │   ├── vec4 _BodyRight                  // 身体右向
│   │   └── vec4 _BodyUp                     // 身体上向
│   └── 角色标识
│       └── float _CharacterInd             // 角色索引
│
└── 🔧 调试系统 (Debug System)
    └── float _DebugCaseItem                 // 调试案例项目
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;yysy看着实现难度其实还行，因为贴图其实不算很多。&lt;/p&gt;
&lt;p&gt;终末地那个才是真的畜啊，一个人有十几张贴图&lt;/p&gt;
&lt;p&gt;最后尝试实现了还原。&lt;/p&gt;
&lt;p&gt;水面效果也非常有意思，很明显这不是简单摸一个外景该有的技术力（&lt;/p&gt;
&lt;p&gt;Gerstner波浪模拟和水体的焦散效果都非常牛逼，我还以为是简单的顶点位移动画和一个深度测试+RampMap就做完了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#version 320 es

#define HLSLCC_ENABLE_UNIFORM_BUFFERS 1
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
#define UNITY_UNIFORM
#else
#define UNITY_UNIFORM uniform
#endif
#define UNITY_SUPPORTS_UNIFORM_LOCATION 1
#if UNITY_SUPPORTS_UNIFORM_LOCATION
#define UNITY_LOCATION(x) layout(location = x)
#define UNITY_BINDING(x) layout(binding = x, std140)
#else
#define UNITY_LOCATION(x)
#define UNITY_BINDING(x) layout(std140)
#endif
uniform 	vec4 _Time;
uniform 	vec3 _WorldSpaceCameraPos;
uniform 	vec4 unity_OrthoParams;
uniform 	vec4 hlslcc_mtx4x4unity_ObjectToWorld[4];
uniform 	vec4 hlslcc_mtx4x4unity_WorldToObject[4];
uniform 	vec4 hlslcc_mtx4x4unity_MatrixV[4];
uniform 	vec4 hlslcc_mtx4x4unity_MatrixVP[4];
uniform 	mediump float water_Size;
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
UNITY_BINDING(0) uniform UnityPerMaterial {
    #endif
    UNITY_UNIFORM mediump vec4 _BumpMap_ST;
    UNITY_UNIFORM mediump vec4 _NoiseMap_ST;
    UNITY_UNIFORM mediump vec4 _Noise2Map_ST;
    UNITY_UNIFORM mediump float _BumpScale;
    UNITY_UNIFORM mediump float _NormalBlendType;
    UNITY_UNIFORM mediump float _BaseWaterEnable;
    UNITY_UNIFORM mediump vec4 _WaveDirection;
    UNITY_UNIFORM mediump float _WaveSpeed0;
    UNITY_UNIFORM mediump float _WaveSpeed1;
    UNITY_UNIFORM mediump float _SpecularIntensity;
    UNITY_UNIFORM mediump float _VertexWaveSpeed;
    UNITY_UNIFORM mediump vec4 _VertexWave0;
    UNITY_UNIFORM mediump vec4 _VertexWave1;
    UNITY_UNIFORM mediump vec4 _VertexWave2;
    UNITY_UNIFORM mediump float _DepthOffset;
    UNITY_UNIFORM mediump float _DepthFadeOff;
    UNITY_UNIFORM mediump float _SdfOffset;
    UNITY_UNIFORM mediump float _SdfFadeOff;
    UNITY_UNIFORM mediump float _ScreenDepthOffset;
    UNITY_UNIFORM mediump float _ScreenDepthFade;
    UNITY_UNIFORM mediump float _ScreenNearShoreFade;
    UNITY_UNIFORM mediump float _ShoreThreshold;
    UNITY_UNIFORM mediump float _ShoreFadeOff;
    UNITY_UNIFORM mediump float _SdfShoreThreshold;
    UNITY_UNIFORM mediump float _SdfShoreFadeOff;
    UNITY_UNIFORM mediump float _SdfWaveShoreMask;
    UNITY_UNIFORM mediump float _SdfWaveShoreFadeOff;
    UNITY_UNIFORM mediump float _EnableShoreEdgeFade;
    UNITY_UNIFORM mediump float _FarSeaFadeThreshold;
    UNITY_UNIFORM mediump float _FarSeaFadeOff;
    UNITY_UNIFORM mediump float _SdfFarSeaThreshold;
    UNITY_UNIFORM mediump float _SdfFarSeaFadeOff;
    UNITY_UNIFORM mediump float _DepthWaveEnable;
    UNITY_UNIFORM mediump float _DepthGerstnerWaveTimeSpeed;
    UNITY_UNIFORM mediump vec4 _WaveFoamNoiseChannelMask;
    UNITY_UNIFORM mediump vec4 _WaveOri;
    UNITY_UNIFORM mediump float _WaveLength;
    UNITY_UNIFORM mediump float _DepthGerstnerWaveFoamSpeed;
    UNITY_UNIFORM mediump float _FrequencyChangeInt;
    UNITY_UNIFORM mediump float _SDFFrequencyChangeInt;
    UNITY_UNIFORM mediump float _WaveNoiseOffsetIntensity;
    UNITY_UNIFORM mediump vec4 _FoamNoiseChannelMask;
    UNITY_UNIFORM mediump float _ShallowWaterFoamSpeed;
    UNITY_UNIFORM mediump float _ShallowWaterDistortion;
    UNITY_UNIFORM mediump float _DepthFoamNoiseMultiplier;
    UNITY_UNIFORM mediump float _DepthFoamIntensity;
    UNITY_UNIFORM mediump float _DepthGerstnerWaveAtten;
	UNITY_UNIFORM mediump vec4 _WaveFoamColor;
	UNITY_UNIFORM mediump float _FakeBentNormalStrength;
	UNITY_UNIFORM mediump float _FakeReflectivityRoughness;
	UNITY_UNIFORM mediump float _FakeReflectivityIntensity;
	UNITY_UNIFORM mediump vec4 _CausticMap_ST;
	UNITY_UNIFORM mediump vec4 _CausticProjDir;
	UNITY_UNIFORM mediump vec4 _CausticProjPos;
	UNITY_UNIFORM mediump float _WaterLevel;
	UNITY_UNIFORM mediump float _BlendDistance;
	UNITY_UNIFORM mediump float _CausticIntensity;
	UNITY_UNIFORM mediump float _DebugItem;
#if HLSLCC_ENABLE_UNIFORM_BUFFERS
};
#endif
in highp vec4 in_POSITION0;
out highp vec4 vs_TEXCOORD0;
out highp vec3 vs_TEXCOORD1;
out highp vec4 vs_TEXCOORD2;
out highp vec4 vs_TEXCOORD3;
out highp vec4 vs_TEXCOORD4;
out highp vec4 vs_TEXCOORD5;
out highp vec4 vs_TEXCOORD6;
out highp vec4 vs_TEXCOORD7;
out highp vec4 vs_TEXCOORD8;
vec3 u_xlat0;
mediump vec4 u_xlat16_0;
vec4 u_xlat1;
bool u_xlatb1;
vec3 u_xlat2;
vec3 u_xlat3;
vec3 u_xlat4;
float u_xlat5;
vec3 u_xlat6;
vec4 u_xlat7;
mediump float u_xlat16_8;
vec3 u_xlat9;
vec2 u_xlat10;
vec3 u_xlat11;
float u_xlat18;
vec2 u_xlat19;
float u_xlat20;
vec2 u_xlat21;
float u_xlat27;
bool u_xlatb27;
float u_xlat28;
void main()
{
    u_xlat0.y = in_POSITION0.y;
    u_xlat27 = 6.28318548 / _VertexWave0.w;
    u_xlat1.x = 9.80000019 / u_xlat27;
    u_xlat1.x = sqrt(u_xlat1.x);
    u_xlat1.x = u_xlat1.x * _VertexWaveSpeed;
    u_xlat10.x = dot(_VertexWave0.xy, _VertexWave0.xy);
    u_xlat10.x = inversesqrt(u_xlat10.x);
    u_xlat10.xy = u_xlat10.xx * _VertexWave0.xy;
    u_xlat0.xz = in_POSITION0.xz * vec2(water_Size);
    u_xlat28 = dot(u_xlat10.xy, u_xlat0.xz);
    u_xlat1.x = (-u_xlat1.x) * _Time.y + u_xlat28;
    u_xlat1.x = u_xlat27 * u_xlat1.x;
    u_xlat27 = _VertexWave0.z / u_xlat27;
    u_xlat2.x = cos(u_xlat1.x);
    u_xlat1.x = sin(u_xlat1.x);
    u_xlat28 = u_xlat27 * u_xlat2.x;
    u_xlat3.y = u_xlat27 * u_xlat1.x;
    u_xlat3.xz = vec2(u_xlat28) * u_xlat10.xy;
    u_xlat11.xyz = u_xlat0.xyz + u_xlat3.xyz;
    u_xlat9.x = 6.28318548 / _VertexWave1.w;
    u_xlat27 = 9.80000019 / u_xlat9.x;
    u_xlat27 = sqrt(u_xlat27);
    u_xlat27 = u_xlat27 * _VertexWaveSpeed;
    u_xlat28 = dot(_VertexWave1.xy, _VertexWave1.xy);
    u_xlat28 = inversesqrt(u_xlat28);
    u_xlat3.xy = vec2(u_xlat28) * _VertexWave1.xy;
    u_xlat28 = dot(u_xlat3.xy, u_xlat0.xz);
    u_xlat27 = (-u_xlat27) * _Time.y + u_xlat28;
    u_xlat27 = u_xlat27 * u_xlat9.x;
    u_xlat9.x = _VertexWave1.z / u_xlat9.x;
    u_xlat4.x = sin(u_xlat27);
    u_xlat5 = cos(u_xlat27);
    u_xlat27 = u_xlat9.x * u_xlat5;
    u_xlat6.y = u_xlat9.x * u_xlat4.x;
    u_xlat6.xz = vec2(u_xlat27) * u_xlat3.xy;
    u_xlat11.xyz = u_xlat11.xyz + u_xlat6.xyz;
    u_xlat9.x = 6.28318548 / _VertexWave2.w;
    u_xlat27 = 9.80000019 / u_xlat9.x;
    u_xlat27 = sqrt(u_xlat27);
    u_xlat27 = u_xlat27 * _VertexWaveSpeed;
    u_xlat28 = dot(_VertexWave2.xy, _VertexWave2.xy);
    u_xlat28 = inversesqrt(u_xlat28);
    u_xlat21.xy = vec2(u_xlat28) * _VertexWave2.xy;
    u_xlat28 = dot(u_xlat21.xy, u_xlat0.xz);
    vs_TEXCOORD4.xy = u_xlat0.xz * vec2(1.0, -1.0) + vec2(0.0, 1.0);
    u_xlat0.x = (-u_xlat27) * _Time.y + u_xlat28;
    u_xlat0.x = u_xlat0.x * u_xlat9.x;
    u_xlat9.x = _VertexWave2.z / u_xlat9.x;
    u_xlat6.x = cos(u_xlat0.x);
    u_xlat0.x = sin(u_xlat0.x);
    u_xlat18 = u_xlat9.x * u_xlat6.x;
    u_xlat7.y = u_xlat0.x * u_xlat9.x;
    u_xlat7.xz = vec2(u_xlat18) * u_xlat21.xy;
    u_xlat9.xyz = u_xlat11.xyz + u_xlat7.xyz;
    u_xlat11.xyz = u_xlat9.yyy * hlslcc_mtx4x4unity_ObjectToWorld[1].xyz;
    u_xlat11.xyz = hlslcc_mtx4x4unity_ObjectToWorld[0].xyz * u_xlat9.xxx + u_xlat11.xyz;
    u_xlat9.xyz = hlslcc_mtx4x4unity_ObjectToWorld[2].xyz * u_xlat9.zzz + u_xlat11.xyz;
    u_xlat9.xyz = u_xlat9.xyz + hlslcc_mtx4x4unity_ObjectToWorld[3].xyz;
    u_xlat7 = u_xlat9.yyyy * hlslcc_mtx4x4unity_MatrixVP[1];
    u_xlat7 = hlslcc_mtx4x4unity_MatrixVP[0] * u_xlat9.xxxx + u_xlat7;
    u_xlat7 = hlslcc_mtx4x4unity_MatrixVP[2] * u_xlat9.zzzz + u_xlat7;
    gl_Position = u_xlat7 + hlslcc_mtx4x4unity_MatrixVP[3];
    u_xlat7.xy = u_xlat9.xz * _BumpMap_ST.xy + _BumpMap_ST.zw;
    u_xlat7.zw = u_xlat9.xz * _NoiseMap_ST.xy + _NoiseMap_ST.zw;
    vs_TEXCOORD0 = u_xlat7 * vec4(0.00100000005, 0.00100000005, 0.00100000005, 0.00100000005);
    u_xlat7.x = hlslcc_mtx4x4unity_WorldToObject[0].y;
    u_xlat7.y = hlslcc_mtx4x4unity_WorldToObject[1].y;
    u_xlat7.z = hlslcc_mtx4x4unity_WorldToObject[2].y;
    u_xlat28 = dot(u_xlat7.xyz, u_xlat7.xyz);
    u_xlat28 = inversesqrt(u_xlat28);
    vs_TEXCOORD1.xyz = vec3(u_xlat28) * u_xlat7.xyz;
    u_xlat19.xy = u_xlat10.xy * (-u_xlat10.xx);
    u_xlat1.x = u_xlat1.x * _VertexWave0.z;
    u_xlat2.x = u_xlat2.x * _VertexWave0.z;
    u_xlat2.y = u_xlat10.x * u_xlat2.x;
    u_xlat2.xz = u_xlat1.xx * u_xlat19.xy;
    u_xlat1.xyz = u_xlat2.xyz + vec3(1.0, 0.0, 0.0);
    u_xlat2.xy = u_xlat3.xy * (-u_xlat3.xx);
    u_xlat28 = u_xlat4.x * _VertexWave1.z;
    u_xlat20 = u_xlat5 * _VertexWave1.z;
    u_xlat4.y = u_xlat20 * u_xlat3.x;
    u_xlat4.xz = vec2(u_xlat28) * u_xlat2.xy;
    u_xlat1.xyz = u_xlat1.xyz + u_xlat4.xyz;
    u_xlat2.xy = u_xlat21.xy * (-u_xlat21.xx);
    u_xlat0.x = u_xlat0.x * _VertexWave2.z;
    u_xlat28 = u_xlat6.x * _VertexWave2.z;
    u_xlat3.y = u_xlat28 * u_xlat21.x;
    u_xlat3.xz = u_xlat0.xx * u_xlat2.xy;
    u_xlat1.xyz = u_xlat1.xyz + u_xlat3.xyz;
    u_xlat0.x = dot(u_xlat1.xyz, u_xlat1.xyz);
    u_xlat0.x = inversesqrt(u_xlat0.x);
    u_xlat1.xyz = u_xlat0.xxx * u_xlat1.xyz;
    u_xlat2.xyz = u_xlat1.yyy * hlslcc_mtx4x4unity_ObjectToWorld[1].xyz;
    u_xlat1.xyw = hlslcc_mtx4x4unity_ObjectToWorld[0].xyz * u_xlat1.xxx + u_xlat2.xyz;
    u_xlat1.xyz = hlslcc_mtx4x4unity_ObjectToWorld[2].xyz * u_xlat1.zzz + u_xlat1.xyw;
    u_xlat0.x = dot(u_xlat1.xyz, u_xlat1.xyz);
    u_xlat0.x = inversesqrt(u_xlat0.x);
    vs_TEXCOORD2.xyz = u_xlat0.xxx * u_xlat1.xyz;
    vs_TEXCOORD2.w = 1.0;
    vs_TEXCOORD3.xyz = u_xlat9.xyz;
    vs_TEXCOORD3.w = 1.0;
    vs_TEXCOORD4.zw = u_xlat9.xz;
    u_xlat0.xyz = (-u_xlat9.xyz) + _WorldSpaceCameraPos.xyz;
#ifdef UNITY_ADRENO_ES3
    u_xlatb27 = !!(unity_OrthoParams.w==1.0);
#else
    u_xlatb27 = unity_OrthoParams.w==1.0;
#endif
    vs_TEXCOORD5.x = (u_xlatb27) ? hlslcc_mtx4x4unity_MatrixV[0].z : u_xlat0.x;
    vs_TEXCOORD5.y = (u_xlatb27) ? hlslcc_mtx4x4unity_MatrixV[1].z : u_xlat0.y;
    vs_TEXCOORD5.z = (u_xlatb27) ? hlslcc_mtx4x4unity_MatrixV[2].z : u_xlat0.z;
    vs_TEXCOORD5.w = 0.0;
    u_xlat16_8 = dot(_CausticProjDir, _CausticProjDir);
    u_xlat16_8 = inversesqrt(u_xlat16_8);
    u_xlat16_0.xyz = vec3(u_xlat16_8) * _CausticProjDir.xyz;
#ifdef UNITY_ADRENO_ES3
    u_xlatb1 = !!(0.999000013&amp;lt;abs(u_xlat16_0.y));
#else
    u_xlatb1 = 0.999000013&amp;lt;abs(u_xlat16_0.y);
#endif
    u_xlat1.xyz = (bool(u_xlatb1)) ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0);
    u_xlat2.xyz = u_xlat16_0.yzx * u_xlat1.xyz;
    u_xlat1.xyz = u_xlat1.zxy * u_xlat16_0.zxy + (-u_xlat2.xyz);
    u_xlat28 = dot(u_xlat1.xyz, u_xlat1.xyz);
    u_xlat28 = inversesqrt(u_xlat28);
    u_xlat1.xyz = vec3(u_xlat28) * u_xlat1.xyz;
    vs_TEXCOORD6.xyz = u_xlat1.xyz;
    vs_TEXCOORD6.w = _CausticProjPos.x;
    u_xlat2.xyz = u_xlat16_0.zxy * u_xlat1.yzx;
    u_xlat1.xyz = u_xlat16_0.yzx * u_xlat1.zxy + (-u_xlat2.xyz);
    u_xlat28 = dot(u_xlat1.xyz, u_xlat1.xyz);
    u_xlat28 = inversesqrt(u_xlat28);
    vs_TEXCOORD7.xyz = vec3(u_xlat28) * u_xlat1.xyz;
    vs_TEXCOORD7.w = _CausticProjPos.y;
    u_xlat16_0.w = _CausticProjPos.z;
    vs_TEXCOORD8 = u_xlat16_0;
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明按照实现功能分类的话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UnityPerMaterial 水面材质参数
├── 📐 纹理映射系统 (Texture Mapping)
│   ├── 法线贴图
│   │   ├── vec4 _BumpMap_ST          // 法线贴图UV变换
│   │   └── float _BumpScale          // 法线强度
│   ├── 噪声贴图
│   │   ├── vec4 _NoiseMap_ST         // 主噪声贴图UV变换
│   │   └── vec4 _Noise2Map_ST        // 次噪声贴图UV变换
│   └── 法线混合
│       └── float _NormalBlendType    // 法线混合模式
│
├── 🌊 波浪动画系统 (Wave Animation)
│   ├── 基础波浪控制
│   │   ├── float _BaseWaterEnable    // 基础水面开关
│   │   ├── vec4 _WaveDirection       // 波浪方向
│   │   ├── float _WaveSpeed0         // 第一层波速
│   │   └── float _WaveSpeed1         // 第二层波速
│   ├── 顶点波浪 (Gerstner Waves)
│   │   ├── float _VertexWaveSpeed    // 顶点波浪速度
│   │   ├── vec4 _VertexWave0         // 第一层顶点波浪参数
│   │   ├── vec4 _VertexWave1         // 第二层顶点波浪参数
│   │   └── vec4 _VertexWave2         // 第三层顶点波浪参数
│   └── 深度波浪系统
│       ├── float _DepthWaveEnable         // 深度波浪开关
│       ├── float _DepthGerstnerWaveTimeSpeed  // 深度波浪时间速度
│       ├── vec4 _WaveOri              // 波浪方向向量
│       ├── float _WaveLength          // 波长
│       ├── float _DepthGerstnerWaveFoamSpeed  // 深度波浪泡沫速度
│       ├── float _FrequencyChangeInt  // 频率变化强度
│       ├── float _SDFFrequencyChangeInt   // SDF频率变化强度
│       ├── float _WaveNoiseOffsetIntensity // 波浪噪声偏移强度
│       └── float _DepthGerstnerWaveAtten  // 深度波浪衰减
│
├── 🏖️ 海岸线系统 (Shore &amp;amp; Depth)
│   ├── 基础深度淡入淡出
│   │   ├── float _DepthOffset        // 深度偏移
│   │   ├── float _DepthFadeOff       // 深度淡出
│   │   ├── float _ScreenDepthOffset  // 屏幕空间深度偏移
│   │   ├── float _ScreenDepthFade    // 屏幕空间深度淡出
│   │   └── float _ScreenNearShoreFade // 屏幕近岸淡出
│   ├── 传统海岸线
│   │   ├── float _ShoreThreshold     // 海岸线阈值
│   │   └── float _ShoreFadeOff       // 海岸线淡出
│   ├── SDF海岸线系统
│   │   ├── float _SdfOffset          // SDF偏移
│   │   ├── float _SdfFadeOff         // SDF淡出
│   │   ├── float _SdfShoreThreshold  // SDF海岸线阈值
│   │   ├── float _SdfShoreFadeOff    // SDF海岸线淡出
│   │   ├── float _SdfWaveShoreMask   // SDF波浪海岸遮罩
│   │   ├── float _SdfWaveShoreFadeOff // SDF波浪海岸淡出
│   │   ├── float _SdfFarSeaThreshold // SDF远海阈值
│   │   └── float _SdfFarSeaFadeOff   // SDF远海淡出
│   ├── 边缘淡化系统
│   │   ├── float _EnableShoreEdgeFade // 启用海岸边缘淡化
│   │   ├── float _FarSeaFadeThreshold // 远海淡化阈值
│   │   └── float _FarSeaFadeOff      // 远海淡化系数
│   └── 水位控制
│       ├── float _WaterLevel         // 水位高度
│       └── float _BlendDistance      // 混合距离
│
├── 🫧 泡沫系统 (Foam Effects)
│   ├── 波浪泡沫
│   │   ├── vec4 _WaveFoamNoiseChannelMask // 波浪泡沫噪声通道遮罩
│   │   ├── vec4 _WaveFoamColor        // 波浪泡沫颜色
│   │   └── float _ShallowWaterFoamSpeed   // 浅水泡沫速度
│   ├── 深度泡沫
│   │   ├── vec4 _FoamNoiseChannelMask // 泡沫噪声通道遮罩
│   │   ├── float _DepthFoamNoiseMultiplier // 深度泡沫噪声倍数
│   │   └── float _DepthFoamIntensity  // 深度泡沫强度
│   └── 泡沫扭曲
│       └── float _ShallowWaterDistortion  // 浅水扭曲
│
├── ✨ 光照反射系统 (Lighting &amp;amp; Reflection)
│   ├── 镜面反射
│   │   └── float _SpecularIntensity  // 镜面反射强度
│   └── 假反射系统
│       ├── float _FakeBentNormalStrength    // 假弯曲法线强度
│       ├── float _FakeReflectivityRoughness // 假反射粗糙度
│       └── float _FakeReflectivityIntensity // 假反射强度
│
├── 🌟 焦散效果 (Caustics)
│   ├── 焦散贴图
│   │   └── vec4 _CausticMap_ST       // 焦散贴图UV变换
│   ├── 焦散投影
│   │   ├── vec4 _CausticProjDir      // 焦散投影方向
│   │   └── vec4 _CausticProjPos      // 焦散投影位置
│   └── 焦散强度
│       └── float _CausticIntensity   // 焦散强度
│
└── 🔧 调试系统 (Debug)
    └── float _DebugItem              // 调试项目选择
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有时间写一个出来吧，看着还是挺牛逼的&lt;/p&gt;
</content:encoded></item><item><title>用Global Hook抓取Steam游戏</title><link>https://fuwari.vercel.app/posts/capture-steam-games-with-global-hook/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/capture-steam-games-with-global-hook/</guid><description>用Global Hook抓取Steam游戏的技术原理和应用</description><pubDate>Sun, 13 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/534821939&quot;&gt;https://zhuanlan.zhihu.com/p/534821939&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Steam在开启RenderDoc之后无法启动，此时我们需要开启globalhook。详情请参见上面的知乎专栏。&lt;/p&gt;
&lt;p&gt;RenderDoc 截取的帧文件（&lt;code&gt;.rdc&lt;/code&gt; 文件）的存放位置有以下几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认保存位置：&lt;/strong&gt;&lt;br /&gt;
当你成功捕获一帧后，RenderDoc 会自动将 &lt;code&gt;.rdc&lt;/code&gt; 文件保存到一个默认的临时目录。这个目录通常位于你的用户临时文件夹下，例如：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;code&gt;C:\Users\[你的用户名]\AppData\Local\Temp\RenderDoc\&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;code&gt;/tmp/RenderDoc/&lt;/code&gt; 或 &lt;code&gt;$XDG_RUNTIME_DIR/RenderDoc/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些文件通常以 &lt;code&gt;[应用程序名称]_[日期]_[时间].rdc&lt;/code&gt; 的格式命名，例如 &lt;code&gt;MyGame_2024_07_29_10_30_05.rdc&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在 RenderDoc 界面中查看和保存：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;捕获日志 (Capture Log) 区域：&lt;/strong&gt; 当你成功捕获一帧后，RenderDoc 主界面左侧的“&lt;strong&gt;Capture Log&lt;/strong&gt;”区域会显示一个条目，其中包含捕获到的帧的缩略图和一些基本信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右键点击保存：&lt;/strong&gt; 你可以右键点击这个捕获条目，然后选择“&lt;strong&gt;Save Capture As...&lt;/strong&gt;”（另存为...）。这将允许你选择一个自定义的目录和文件名来保存这个 &lt;code&gt;.rdc&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双击打开：&lt;/strong&gt; 双击这个捕获条目会直接在 RenderDoc 中打开并加载该帧进行分析。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置默认保存路径 (不常见，但可能存在)：&lt;/strong&gt;&lt;br /&gt;
虽然 RenderDoc 主要设计为捕获后手动保存，但有些高级设置或自定义脚本可能会影响默认的临时目录。不过，对于大多数用户而言，上述的临时目录是默认行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结来说：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;临时文件：&lt;/strong&gt; RenderDoc 会自动将捕获的帧保存到你的系统临时目录下的 &lt;code&gt;RenderDoc&lt;/code&gt; 子文件夹中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动保存：&lt;/strong&gt; 你应该在 RenderDoc 的“Capture Log”区域中，右键点击你想要永久保存的捕获条目，然后选择“Save Capture As...”将其保存到你希望的任何位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议你养成习惯，在捕获到你需要的帧后，立即将其另存为到你指定的项目文件夹中，这样可以避免临时文件被清理或混淆。&lt;/p&gt;
</content:encoded></item><item><title>如何使用TRAE调用Blender-MCP</title><link>https://fuwari.vercel.app/posts/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8trae%E8%B0%83%E7%94%A8blender-mcp/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8trae%E8%B0%83%E7%94%A8blender-mcp/</guid><description>详细介绍如何配置TRAE与Blender MCP的集成使用方法</description><pubDate>Fri, 11 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;为什么我要记下这个东西？实际上是因为TRAE太sb了，自己修改开源社区的方案扔到自己的火山引擎上。你说和腾讯云那样开个社区讨论就也还行，你社区又不开，README都不写，和Cursor又不一样（美名其曰差异化），居然还要Blender MCP官方来写个文档，居然还是错的。真tm 360全方位立体防卫是吗&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;前置环境条件&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;TRAE&lt;/li&gt;
&lt;li&gt;Blender&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uv包，&lt;/strong&gt;&lt;a href=&quot;https://docs.astral.sh/uv/getting-started/installation/&quot;&gt;&lt;strong&gt;https://docs.astral.sh/uv/getting-started/installation/&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;请一定要安装uv。这是激活Blender-MCP的方式。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于Windows，可以用pip：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install uv
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Blender端配置&lt;/h1&gt;
&lt;p&gt;下载Github中的&lt;a href=&quot;https://github.com/ahujasid/blender-mcp&quot;&gt;https://github.com/ahujasid/blender-mcp&lt;/a&gt;，直接clone/打包zip，我们只需要&lt;strong&gt;addon.py&lt;/strong&gt;即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/a6299c7e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开Blender，在左上角的菜单栏中打开：编辑-偏好设置-插件。右上角-从右上角安装，选择&lt;strong&gt;addon.py&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1439c1d2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此后，在主页面打开侧边栏。如果没有侧边栏显示也可以按键盘上的N呼出：&lt;img src=&quot;/images/posts/3e7eb900.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Poly Haven是一个免费的资源库，有HDRI、模型、材质等；Hyper3D和Rodin 3D则是AI训练的模型库；SketchFab是免费的库，有大量模型和动画素材。&lt;/p&gt;
&lt;p&gt;点击Connect to MCP Server即可。默认为9876端口。&lt;/p&gt;
&lt;h1&gt;电脑端配置&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/4f6d1021.png&quot; alt=&quot;&quot; /&gt;打开MCP端&lt;/p&gt;
&lt;p&gt;添加Blender MCP。&lt;strong&gt;需要代理以下载（真搞笑，自己魔改了下载结果还要挂代理）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/2b119805.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;直接下载后在内置智能体的Builder选择Builder with MCP。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/2b189e07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开TRAE的终端，输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx blender-mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的意思是用之前的Uv包来激活TRAE的MCP server，如果输出的结果如我的截图所示显示Successfully，那么就代表成功了！然后就可以鞭策AI去干活了。&lt;/p&gt;
&lt;p&gt;还有一个小前提，如果你遇到的是 &lt;s&gt;&amp;lt;u&amp;gt;蠢猪&amp;lt;/u&amp;gt;&lt;/s&gt;~~ ~~比较不成熟的模型（比如x包），那么在提示词中可以要求“在Blender中....”，否则这个唐妞可能会开一个python脚本给你。还有就是可能目前只有Claude可以比较好的进行模型的创建。&lt;/p&gt;
&lt;h1&gt;实测&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/09bad6c9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/63f77229.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/19ee42ad.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不赖？&lt;/p&gt;
&lt;h1&gt;Ref&lt;/h1&gt;
&lt;p&gt;uv安装：&lt;a href=&quot;https://docs.astral.sh/uv/getting-started/installation/#standalone-installer&quot;&gt;https://docs.astral.sh/uv/getting-started/installation/#standalone-installer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/luweiseu/p/18846417&quot;&gt;https://www.cnblogs.com/luweiseu/p/18846417&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blender-mcp.com/trae-integration.html&quot;&gt;https://blender-mcp.com/trae-integration.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;挂街示众，两个都是错的&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>C#：委托和异步回调</title><link>https://fuwari.vercel.app/posts/csharp-delegate-async-callback/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/csharp-delegate-async-callback/</guid><description>C#：委托和异步回调的详细介绍和使用方法</description><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;委托和回调&lt;/h1&gt;
&lt;h2&gt;🎯 什么是委托？—— 用“外卖订单”来比喻&lt;/h2&gt;
&lt;p&gt;想象一下，你开了一家外卖店，顾客下单后，你需要通知不同的“执行人”去完成任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有人负责&lt;strong&gt;做菜&lt;/strong&gt;（厨师）&lt;/li&gt;
&lt;li&gt;有人负责&lt;strong&gt;打包&lt;/strong&gt;（打包员）&lt;/li&gt;
&lt;li&gt;有人负责&lt;strong&gt;送外卖&lt;/strong&gt;（骑手）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但你不想每次都硬编码（写死）让“张三”做菜、“李四”打包。你希望更灵活：&lt;strong&gt;谁有空，就让谁干！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就需要一个“通知机制”——你不知道具体是谁干活，但你知道“该叫人来做这件事了”。&lt;/p&gt;
&lt;p&gt;在 C# 中，&lt;strong&gt;委托就是这个“通知机制”&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧱 第一步：定义“任务类型”—— 委托的声明&lt;/h2&gt;
&lt;p&gt;你先要定义一个“岗位职责说明书”，比如“做饭的人必须会做菜”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 委托声明：定义一种“能做饭”的岗位
public delegate void CookDelegate(string dishName);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delegate&lt;/code&gt;：关键词，表示这是一个委托。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void&lt;/code&gt;：表示这个岗位完成任务后不返回结果（比如不需要返回“菜好不好吃”）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CookDelegate&lt;/code&gt;：这是这个岗位的名字。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;string dishName&lt;/code&gt;：表示这个岗位需要一个参数，比如“宫保鸡丁”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ 这就像你说：“所有应聘‘厨师’岗位的人，必须会接收一个菜名，并能做出来。”&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧍 第二步：创建“应聘者”—— 具体的方法&lt;/h2&gt;
&lt;p&gt;现在，有两位厨师来应聘：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Chef
{
    public static void ZhangSanCook(string dish)
    {
        Console.WriteLine($&quot;张三正在做：{dish}&quot;);
    }

    public static void LiSiCook(string dish)
    {
        Console.WriteLine($&quot;李四正在做：{dish}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 这两个方法都符合 &lt;code&gt;CookDelegate&lt;/code&gt; 的要求：接受一个字符串，返回 void。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📞 第三步：发布“招聘通知”—— 创建委托实例&lt;/h2&gt;
&lt;p&gt;现在你开始招人：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建一个委托变量，表示“当前负责做饭的人”
CookDelegate cookJob = null;

// 张三来应聘，你把他安排上
cookJob = Chef.ZhangSanCook;

// 或者你也可以换李四
// cookJob = Chef.LiSiCook;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 这就像你说：“现在让张三来做饭。”&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🔔 第四步：下达任务 —— 调用委托&lt;/h2&gt;
&lt;p&gt;当顾客下单“宫保鸡丁”时，你不需要关心是谁在做，你只管“通知做饭的人”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (cookJob != null)
{
    cookJob(&quot;宫保鸡丁&quot;); // 调用委托
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;张三正在做：宫保鸡丁
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ 你没有写死“张三来做”，而是通过委托“间接调用”了方法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🚀 更强大的功能：多个人一起干！—— 多播委托（Multicast Delegate）&lt;/h2&gt;
&lt;p&gt;委托还能“通知多人”！比如，做菜的同时，还要通知打包员和骑手。&lt;/p&gt;
&lt;p&gt;我们再定义两个委托：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public delegate void PackDelegate(string dish);
public delegate void DeliverDelegate(string address);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Staff
{
    public static void PackFood(string dish)
    {
        Console.WriteLine($&quot;打包员正在打包：{dish}&quot;);
    }

    public static void DeliverFood(string address)
    {
        Console.WriteLine($&quot;骑手正在送往：{address}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，你可以把多个任务“串”起来，像流水线一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用内置的 Action 委托（更简单）
Action&amp;lt;string&amp;gt; workflow = null;

workflow += Chef.ZhangSanCook;     // 第一步：做饭
workflow += Staff.PackFood;        // 第二步：打包
workflow += (address) =&amp;gt; Staff.DeliverFood(&quot;北京市朝阳区&quot;); // 第三步：送外卖

// 一键触发整个流程！
workflow?.Invoke(&quot;宫保鸡丁&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;张三正在做：宫保鸡丁
打包员正在打包：宫保鸡丁
骑手正在送往：北京市朝阳区
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 &lt;code&gt;+=&lt;/code&gt; 表示“再加一个人来干活”，这就是&lt;strong&gt;多播委托&lt;/strong&gt;，像一条流水线，一个任务接一个任务执行。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🌟 委托的真正用途：解耦 + 回调&lt;/h2&gt;
&lt;h3&gt;1. 解耦（Decoupling）&lt;/h3&gt;
&lt;p&gt;你写一个“下单系统”，但你不需要知道谁来做饭、谁来送外卖。你只关心：“有订单时，调用这个委托就行。”&lt;br /&gt;
这样，厨师换了、骑手换了，你的主逻辑完全不用改！&lt;/p&gt;
&lt;h3&gt;2. 回调（Callback）&lt;/h3&gt;
&lt;p&gt;比如你调用一个“异步做饭”方法，做完后要通知你：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void StartCookingAsync(string dish, Action onComplete)
{
    // 模拟做饭耗时
    Task.Delay(2000).Wait();
    Console.WriteLine($&quot;{dish} 做好了！&quot;);
    
    // 做完后，回调通知你
    onComplete?.Invoke();
}

// 使用时：
StartCookingAsync(&quot;红烧肉&quot;, () =&amp;gt; {
    Console.WriteLine(&quot;可以开饭啦！&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;👉 这就是“你先去做饭，做完了告诉我一声”。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;✅ 总结：委托到底是什么？&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;比喻&lt;/th&gt;
&lt;th&gt;编程概念&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;岗位招聘说明书&lt;/td&gt;
&lt;td&gt;&lt;code&gt;delegate void CookDelegate(string dish);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;应聘的厨师&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ZhangSanCook&lt;/code&gt;, &lt;code&gt;LiSiCook&lt;/code&gt; 方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安排谁上岗&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cookJob = ZhangSanCook;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;下达任务&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cookJob(&quot;宫保鸡丁&quot;);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流水线作业&lt;/td&gt;
&lt;td&gt;多播委托 &lt;code&gt;+=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;做完通知我&lt;/td&gt;
&lt;td&gt;回调（Callback）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;🎯 &lt;strong&gt;一句话总结&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;委托就是“把方法当作变量来传递”，让你的代码更灵活、可扩展、可维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;就像你不需要知道谁在干活，你只负责“按按钮”，系统就会自动让正确的人完成任务。&lt;/p&gt;
&lt;h1&gt;invoke和C#中的lambda表达式&lt;/h1&gt;
&lt;h2&gt;🧩 代码回顾：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;onComplete?.Invoke();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StartCookingAsync(&quot;红烧肉&quot;, () =&amp;gt; {
    Console.WriteLine(&quot;可以开饭啦！&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们要理解的是：&lt;strong&gt;这两个部分是如何配合工作的？为什么这样写？&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 场景设定：做饭需要时间&lt;/h2&gt;
&lt;p&gt;想象一下，你让厨师去做一道“红烧肉”，但做饭要花 2 秒钟。你不想傻等，而是说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“你去做好了红烧肉之后，&lt;strong&gt;记得告诉我一声&lt;/strong&gt;，我好去吃饭。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个“告诉我一声”，就是所谓的 &lt;strong&gt;回调（Callback）&lt;/strong&gt;，而 &lt;code&gt;Action onComplete&lt;/code&gt; 就是“告诉我”的那个方式。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;✅ 第一部分：&lt;code&gt;onComplete?.Invoke();&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们先看定义这个方法的地方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void StartCookingAsync(string dish, Action onComplete)
{
    // 模拟做饭耗时
    Task.Delay(2000).Wait();  // 等2秒，假装在做饭
    Console.WriteLine($&quot;{dish} 做好了！&quot;);

    // 做完后，回调通知你
    onComplete?.Invoke();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;🔍 逐行解释：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Action onComplete&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;这是一个&lt;strong&gt;委托参数&lt;/strong&gt;，意思是：“请告诉我，做完饭后要执行哪个方法？”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Action&lt;/code&gt; 是 C# 内置的一个委托类型，表示“一个没有参数、没有返回值的方法”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onComplete?.Invoke();&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;这是关键！我们来拆开看：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;部分&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onComplete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;就是你传进来的“完成后要执行的方法”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;空值条件运算符&lt;/strong&gt;：意思是“如果 &lt;code&gt;onComplete&lt;/code&gt; 不是 null，才继续调用”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Invoke()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调用这个委托指向的方法（也就是执行它）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;✅ 所以这行代码的意思是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“如果有人告诉我‘做完要做什么’，那我现在就执行那个操作；如果没人告诉我，那就算了。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;👉 防止空指针错误！如果不加 &lt;code&gt;?&lt;/code&gt;，而你又没传回调，程序就会报错：“你试图调用一个 null 的方法”。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;✅ 第二部分：&lt;code&gt;() =&amp;gt; { ... }&lt;/code&gt; 是什么？&lt;/h3&gt;
&lt;p&gt;再看调用的地方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StartCookingAsync(&quot;红烧肉&quot;, () =&amp;gt; {
    Console.WriteLine(&quot;可以开饭啦！&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;🔍 拆解这个调用：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;红烧肉&quot;&lt;/code&gt;：这是第一个参数，表示要做哪道菜。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;() =&amp;gt; { ... }&lt;/code&gt;：这是第二个参数，就是那个 &lt;code&gt;Action onComplete&lt;/code&gt;！&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;那 &lt;code&gt;() =&amp;gt; { ... }&lt;/code&gt; 到底是什么？&lt;/h4&gt;
&lt;p&gt;这是 C# 的 &lt;strong&gt;Lambda 表达式&lt;/strong&gt;，你可以把它理解为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“一个匿名的方法（没有名字的方法），它没有参数，执行的时候就打印一句话。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void OnCookingFinished()
{
    Console.WriteLine(&quot;可以开饭啦！&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但我们不想专门去定义一个方法，所以用 &lt;code&gt;() =&amp;gt; { }&lt;/code&gt; 来“现场写一个临时方法”。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;写法&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;这个方法不需要任何参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;“指向”要执行的代码，读作“goes to”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ ... }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;要执行的具体操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;() =&amp;gt; {
    Console.WriteLine(&quot;可以开饭啦！&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;意思就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“当做饭做完时，请执行：打印‘可以开饭啦！’”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;🔄 两个部分如何配合？—— 完整流程&lt;/h2&gt;
&lt;p&gt;我们把整个流程串起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 你调用这个方法
StartCookingAsync(&quot;红烧肉&quot;, () =&amp;gt; {
    Console.WriteLine(&quot;可以开饭啦！&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;程序开始做饭，等 2 秒。&lt;/li&gt;
&lt;li&gt;打印：&lt;code&gt;红烧肉 做好了！&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后执行：&lt;code&gt;onComplete?.Invoke();&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;此时 &lt;code&gt;onComplete&lt;/code&gt; 指向的是你传进来的那个 &lt;code&gt;() =&amp;gt; { ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所以就会执行里面的代码：&lt;code&gt;Console.WriteLine(&quot;可以开饭啦！&quot;);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;🎯 最终输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;红烧肉 做好了！
可以开饭啦！
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;🧠 类比理解：打电话预约&lt;/h2&gt;
&lt;p&gt;想象你打电话给餐厅：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你：“帮我做份红烧肉，做好了&lt;strong&gt;打电话通知我&lt;/strong&gt;。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;“打电话通知我” → 就是 &lt;code&gt;Action onComplete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;你留的电话号码 → 就是 &lt;code&gt;() =&amp;gt; { Console.WriteLine(&quot;可以开饭啦！&quot;); }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;餐厅做完菜后打电话 → 就是 &lt;code&gt;onComplete?.Invoke();&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没留电话（&lt;code&gt;onComplete&lt;/code&gt; 是 null），那餐厅当然没法通知你，所以加个 &lt;code&gt;?.&lt;/code&gt; 防止出错。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;✅ 总结：一句话讲清楚&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onComplete?.Invoke();&lt;/code&gt;：&lt;br /&gt;
“如果有人留了‘事后要做的事’，现在就去做那件事。”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;() =&amp;gt; { ... }&lt;/code&gt;：&lt;br /&gt;
“这是我留的‘事后要做的事’——打印一句话。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;👉 它们配合起来，就实现了“&lt;strong&gt;异步完成后的通知机制&lt;/strong&gt;”，这就是委托最常用的场景之一。&lt;/p&gt;
</content:encoded></item><item><title>Houdini中的基本点线面</title><link>https://fuwari.vercel.app/posts/houdini%E4%B8%AD%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%82%B9%E7%BA%BF%E9%9D%A2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/houdini%E4%B8%AD%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%82%B9%E7%BA%BF%E9%9D%A2/</guid><description>Houdini中Points、Vertices、Primitives、Polygons的基本概念和区别</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当fbx模型导入到Houdini的时候，会产生最基本的四个Class（或者说来自于fbx本身就是这四个Class）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Points&lt;/li&gt;
&lt;li&gt;Primitives&lt;/li&gt;
&lt;li&gt;Vertices&lt;/li&gt;
&lt;li&gt;Polygons&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们在这里就拿RubberToy举例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/7aa82cfb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;基本几何组件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Points（点）&lt;/strong&gt; 是最基础的几何元素，每个点在3D空间中有一个位置坐标(x,y,z)。点可以独立存在，也可以作为其他几何元素的组成部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vertices（顶点）&lt;/strong&gt; 是连接点和图元的桥梁。一个顶点引用一个特定的点，但同一个点可以被多个顶点引用。顶点主要用于定义图元中点的顺序和连接关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitives（图元）&lt;/strong&gt; 是由顶点组成的几何形状，比如多边形、曲线、体积等。每个图元包含一系列顶点，这些顶点定义了图元的形状和拓扑结构。&lt;/p&gt;
&lt;p&gt;对于&lt;strong&gt;Polygons（多边形）&lt;/strong&gt;，一般情况下我们可以就认为是Primitives（即面）。在最后我再讨论一下Polygons和Primitives的不同之处。&lt;/p&gt;
&lt;h2&gt;属性系统&lt;/h2&gt;
&lt;p&gt;Houdini使用强大的属性系统来存储数据。属性可以附加到不同的几何组件上：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Point属性&lt;/strong&gt; 存储在点上，比如位置(P)、颜色(Cd)、法线(N)等。这些属性会在点之间进行插值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vertex属性&lt;/strong&gt; 存储在顶点上，常用于UV坐标、顶点颜色等需要在同一个点的不同面上有不同值的情况。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitive属性&lt;/strong&gt; 存储在图元上，比如材质ID、面的类型等。整个图元共享同一个属性值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Detail属性&lt;/strong&gt; 是全局属性，存储在整个几何体上，比如总的点数量、时间信息等。&lt;/p&gt;
&lt;p&gt;我用一个简单的例子来解释这三者的区别：&lt;/p&gt;
&lt;h2&gt;想象一个立方体&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Points（点）&lt;/strong&gt; 就像是立方体的8个角。每个角在空间中有一个固定的坐标位置，比如(0,0,0)、(1,0,0)等等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitives（图元）&lt;/strong&gt; 就是立方体的6个面。每个面都是一个四边形，需要4个点来定义。比如底面可能由点1、2、3、4组成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vertices（顶点）&lt;/strong&gt; 是连接点和面的&quot;引用&quot;。因为同一个点（比如立方体的一个角）会被3个不同的面共享，所以需要3个不同的顶点来分别引用这个点。&lt;/p&gt;
&lt;h2&gt;关键区别&lt;/h2&gt;
&lt;p&gt;想象立方体的一个角点，这个角点的坐标是固定的（比如原点），但是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个&lt;strong&gt;点&lt;/strong&gt;只有一个位置坐标&lt;/li&gt;
&lt;li&gt;但有&lt;strong&gt;3个顶点&lt;/strong&gt;分别属于3个不同的面，都指向这同一个点&lt;/li&gt;
&lt;li&gt;这样每个面（图元）就可以给自己的顶点设置不同的属性&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么需要这样设计？&lt;/h2&gt;
&lt;p&gt;比如你想给立方体贴纹理，同一个角点在不同面上的UV坐标是不同的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在正面，这个点的UV可能是(0,0)&lt;/li&gt;
&lt;li&gt;在右面，这个点的UV可能是(1,0)&lt;/li&gt;
&lt;li&gt;在顶面，这个点的UV可能是(0,1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有顶点这个中间层，就无法实现同一个点在不同面上有不同的属性值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;lt;u&amp;gt;简单说：点定义位置，图元(Primitives)定义形状，顶点让同一个点在不同图元中可以有不同属性。&amp;lt;/u&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这就引申到了对于Vertices的表示方式。还是拿我们之前的立方体。简单来说，我们可以这么理解：&lt;/p&gt;
&lt;p&gt;一个Point连接着三个面&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被第一个面的Vertices引用，序号为2&lt;/li&gt;
&lt;li&gt;被第二个面的Vertices引用，序号为3&lt;/li&gt;
&lt;li&gt;被第三个面的Vertices引用，序号为1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vertices存在的最大意义就是可以对每一个面进行单独的控制（比如Temp，比如TTL，比如Gravity）。因为如果单独为Point赋值，那么每一个被链接的面都会被赋予同一个颜色。&lt;/p&gt;
&lt;h2&gt;Vertices的双重身份&lt;/h2&gt;
&lt;p&gt;Vertices有两个编号可选。分别是全局编号和复合标识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;全局编号&lt;/strong&gt;：每个vertex有一个全局唯一的编号（@vtxnum），从0开始递增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复合标识&lt;/strong&gt;：vertex同时可以用&lt;code&gt;&amp;lt;primnum&amp;gt;:&amp;lt;vertex_in_prim&amp;gt;&lt;/code&gt;的格式来标识，也就是”面编号+顶点编号“，表示&quot;某个图元中的第几个顶点&quot;。&lt;/p&gt;
&lt;p&gt;对，这是正确的！在Houdini中，vertex确实有一个复合标识系统：&lt;/p&gt;
&lt;h2&gt;Vertex的双重标识&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;全局编号&lt;/strong&gt;：每个vertex有一个全局唯一的编号（@vtxnum），从0开始递增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复合标识&lt;/strong&gt;：vertex同时可以用&lt;code&gt;&amp;lt;primnum&amp;gt;:&amp;lt;vertex_in_prim&amp;gt;&lt;/code&gt;的格式来标识，表示&quot;某个图元中的第几个顶点&quot;。&lt;/p&gt;
&lt;p&gt;假设有在一个一个四边形（quad）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个四边形是0号图元（@primnum = 0）&lt;/li&gt;
&lt;li&gt;它有4个顶点，在这个图元内的编号是0,1,2,3&lt;/li&gt;
&lt;li&gt;所以这4个顶点可以表示为：0:0, 0:1, 0:2, 0:3&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;VEX&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 获取图元中的顶点数量
primvertexcount(0, @primnum)

// 获取图元中第i个顶点的全局编号
primvertex(0, @primnum, i)

// 获取顶点在其所属图元中的局部编号
primvertexindex(0, @vtxnum)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实际应用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 在primitive wrangle中遍历当前图元的所有顶点
int vtx_count = primvertexcount(0, @primnum);
for(int i = 0; i &amp;lt; vtx_count; i++) {
    int vtx = primvertex(0, @primnum, i);  // 获取第i个顶点的全局编号
    // 现在可以操作这个顶点
    setvertexattrib(0, &quot;Cd&quot;, vtx, {1,0,0});//归零color
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以vertex既有全局的线性编号，也有基于图元的层次化标识，这让在不同上下文中引用顶点变得很灵活。&lt;/p&gt;
&lt;p&gt;在VEX中有多种方式来操作和引用vertex，这里是主要的写法：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;从图元获取顶点&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取当前图元的第0个顶点
int vtx = primvertex(0, @primnum, 0);

// 获取图元的所有顶点
int vtx_count = primvertexcount(0, @primnum);
for(int i = 0; i &amp;lt; vtx_count; i++) {
    int vtx = primvertex(0, @primnum, i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从点获取顶点&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取点的第一个顶点
int vtx = pointvertex(0, @ptnum);

// 获取点的所有顶点
int vtx_array[] = pointvertices(0, @ptnum);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;获取和设置Vertex属性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;读取vertex属性&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 直接在vertex wrangle中
vector uv = @uv;
vector color = @Cd;

// 从其他地方读取特定vertex的属性
vector uv = vertex(0, &quot;uv&quot;, vtx_num);
vector color = vertex(0, &quot;Cd&quot;, vtx_num);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设置vertex属性&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在vertex wrangle中直接设置
@uv = {0.5, 0.5};
@Cd = {1, 0, 0};

// 从其他地方设置特定vertex的属性
setvertexattrib(0, &quot;uv&quot;, vtx_num, {0.5, 0.5});
setvertexattrib(0, &quot;Cd&quot;, vtx_num, {1, 0, 0});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vertex与Point/Primitive的转换&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// vertex到point
int pt = vertexpoint(0, @vtxnum);

// vertex到primitive
int prim = vertexprim(0, @vtxnum);

// vertex在primitive中的局部索引
int local_idx = primvertexindex(0, @vtxnum);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实际应用例子&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;为每个面的顶点设置不同UV&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在primitive wrangle中
int vtx_count = primvertexcount(0, @primnum);
for(int i = 0; i &amp;lt; vtx_count; i++) {
    int vtx = primvertex(0, @primnum, i);
    vector uv = {float(i)/vtx_count, 0.5};
    setvertexattrib(0, &quot;uv&quot;, vtx, uv);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建硬边效果&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在vertex wrangle中，根据所属面设置颜色
int prim = vertexprim(0, @vtxnum);
@Cd = rand(prim);  // 同一面的所有顶点颜色相同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些方法让你能够在VEX中灵活地操作vertex属性，实现各种复杂的几何处理效果。&lt;/p&gt;
&lt;p&gt;但是说实话，在一般情况下，Point和Vertex（Vertices）其实是差不多的东西。但Houdini会在颜色处理中处理这些比较微小的差异：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Point Cd&lt;/strong&gt;：颜色在面之间平滑插值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适合：渐变效果、基于位置的着色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Vertex Cd&lt;/strong&gt;：可以产生硬边界&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适合：每个面不同材质、硬边光照、UV接缝处。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如说我们如果创建一个立方体，给其中一个角点设置红色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用Point Cd：这个角的红色会在3个相邻面间平滑混合。即颜色会在每个面之间做平滑的渐变。&lt;/li&gt;
&lt;li&gt;用Vertex Cd：可以让这个角在3个面上显示完全不同的颜色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以说，&lt;strong&gt;在简单模型上看起来差别不大&lt;/strong&gt;，但vertex Cd的灵活性在于能打破point的颜色约束，实现更复杂的着色需求。大部分时候point Cd就够用了。&lt;/p&gt;
&lt;h2&gt;Houdini的Polygons和Primitives之间有什么不同？&lt;/h2&gt;
&lt;p&gt;虽然说在日常的使用中，可以看到在大部分的情况下，Houdini就是将Primitives等价于Polygons；选择的时候，选中了面则会提醒你这是Primitives；比如说PolyExtrude，PolyBevel这种，直接将Primitive等价于Polygons。所以在一般的口头交流中，&lt;strong&gt;这几乎是等价的，并且通常不会产生歧义&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;为什么不一定完全精确？&lt;/h3&gt;
&lt;p&gt;从Houdini的底层几何结构来看，“Primitive”是一个更广泛、更基础的&lt;strong&gt;类别（Class）&lt;/strong&gt;，而“Polygon”只是这个类别下的一个&lt;strong&gt;具体类型（Type）&lt;/strong&gt;。将它们等同，就像说“轿车等同于车辆”一样，虽然轿车是最常见的车辆，但车辆还包括卡车、公交车、摩托车等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primitive（基元）是Houdini几何体的基本组成单位之一，它定义了点是如何被连接和解释的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Houdini支持多种Primitive类型，远不止多边形一种。主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Polygon（多边形）&lt;/strong&gt;：由一系列顶点定义的闭合或开放的面。这是最常见的类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polyline（多边形线）&lt;/strong&gt;：其实是开放的多边形，本质上也是Polygon类型，但通常用来表示曲线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Packed Primitives（打包基元）&lt;/strong&gt;：这是一个非常特殊的“指针”或“引用”型基元。它本身只包含极少的信息（如变换信息），但指向了另一份完整的几何体数据（可能在内存中或硬盘上）。这对于处理海量实例或复杂场景至关重要，因为它们内存占用极低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polygon Soup（多边形汤）&lt;/strong&gt;：一种内存优化存储方式，将大量多边形数据紧凑地存放在一起，读取和显示速度快，但编辑性较差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Volume（体积）/ VDB&lt;/strong&gt;：用于表示烟、火、云、雾等体积数据的基元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NURBS/Bezier Curves and Surfaces&lt;/strong&gt;：通过数学公式定义的平滑曲线和曲面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metaballs（融球）&lt;/strong&gt;：可以相互融合的球状场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quadratic Primitives&lt;/strong&gt;：由数学定义的简单形状，如“原始球体”（Primitive Sphere），它可能只由一个点和半径值定义，非常轻量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;为什么区分这两者至关重要？&lt;/h4&gt;
&lt;p&gt;在进行程序化操作和高级开发时，精确区分Primitive的类型是必需的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序化判断&lt;/strong&gt;：在使用VEX或Python时，你经常需要判断一个基元的具体类型，然后执行不同的逻辑。例如，你不能对一个Volume基元执行&lt;code&gt;PolyExtrude&lt;/code&gt;操作。通过&lt;code&gt;primintrinsic&lt;/code&gt;函数在VEX中可以获取基元的&lt;code&gt;typename&lt;/code&gt;，从而判断它是&quot;Poly&quot;、&quot;Volume&quot;还是&quot;Packed&quot;等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;vex複製&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 在Wrangle节点中判断基元类型
string prim_type = primintrinsic(0, &quot;typename&quot;, @primnum);

if (prim_type == &quot;Poly&quot;) {
    // 执行针对多边形的操作
    v@Cd = {1, 0, 0}; // 设为红色
} else if (prim_type == &quot;Volume&quot;) {
    // 执行针对体积的操作
    v@Cd = {0, 0, 1}; // 设为蓝色
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：理解Packed Primitives和Polygon Soups的特性，是优化复杂场景性能的关键。 &lt;a href=&quot;https://www.sidefx.com/docs/houdini/model/primitives.html&quot;&gt;[2]&lt;/a&gt; 如果你错误地将一个Packed Primitive当作普通Polygon来处理（例如，尝试编辑它的点），将无法得到预期的结果，甚至会破坏其性能优势。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：不同的Primitive类型对应完全不同的工作流程。处理曲线使用&lt;code&gt;Resample&lt;/code&gt;、&lt;code&gt;Carve&lt;/code&gt;等节点；处理体积则使用&lt;code&gt;Volume VOP&lt;/code&gt;或&lt;code&gt;Volume Wrangle&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>TBDR vs. IMR</title><link>https://fuwari.vercel.app/posts/tbdr-vs-imr/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/tbdr-vs-imr/</guid><description>深入分析TBDR和IMR两种GPU渲染架构的特点与选择</description><pubDate>Sat, 05 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;写在前面&lt;/h3&gt;
&lt;p&gt;渲染方案的选择——无论是前向渲染（Forward Rendering）还是延迟渲染（Deferred Rendering）——都与底层硬件架构（如 IMR 和 TBDR）的特性密切相关。这种关联并非绝对的&quot;绑定&quot;，而是基于性能和功耗的权衡。通常，移动端设备多采用 &lt;strong&gt;TBDR&lt;/strong&gt; 架构以追求能效，而桌面端 GPU 则长期使用 &lt;strong&gt;IMR&lt;/strong&gt; 架构以发挥其强大的原始性能和高带宽优势。# 写在前面
渲染方案的选择——无论是前向渲染（Forward Rendering）还是延迟渲染（Deferred Rendering）——都与底层硬件架构（如 IMR 和 TBDR）的特性密切相关。这种关联并非绝对的“绑定”，而是基于性能和功耗的权衡。通常，移动端设备多采用 &lt;strong&gt;TBDR&lt;/strong&gt; 架构以追求能效，而桌面端 GPU 则长期使用 &lt;strong&gt;IMR&lt;/strong&gt; 架构以发挥其强大的原始性能和高带宽优势。&lt;/p&gt;
&lt;h3&gt;为什么不同类型设备会采用不同方案？&lt;/h3&gt;
&lt;p&gt;根本原因在于 &lt;strong&gt;功耗和带宽&lt;/strong&gt; 的限制，尤其是在移动端设备上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bea8edce.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正如“底大一级压死人”的比喻，桌面端 GPU 拥有更大的芯片面积、更高的功耗预算（例如，GPU功耗可达数百瓦）和巨大的内存带宽。相比之下，移动端 GPU 的芯片面积、功耗和散热都受到严格限制。因此，移动端 GPU 必须采用更“聪明”的架构来节省宝贵的内存带宽和电力，这就是 &lt;strong&gt;TBDR（Tile-Based Deferred Rendering，分块延迟渲染）&lt;/strong&gt; 诞生的核心原因。桌面端则长期采用 &lt;strong&gt;IMR（Immediate Mode Rendering，立即模式渲染）&lt;/strong&gt; 架构。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;硬件渲染架构：TBDR vs. IMR&lt;/h3&gt;
&lt;p&gt;这两种是硬件层面的渲染架构，应用程序无法在两者间切换。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/355238b8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;TBDR：Tile-Based Deferred Rendering (分块延迟渲染)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;TBDR 的核心思想是 &lt;strong&gt;“延迟”&lt;/strong&gt; 对内存的写入。它将屏幕划分为一个个小方块（Tile），对每个 Tile 进行独立渲染，并将中间结果暂存在 &lt;strong&gt;极快但容量很小&lt;/strong&gt; 的 &lt;strong&gt;片上内存（On-Chip Memory）&lt;/strong&gt; 中。只有当一个 Tile 完全渲染完毕后，其最终颜色结果才会被一次性写回到系统内存的帧缓冲区（Framebuffer）中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TBDR 的渲染流程大致分为两个主要阶段：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;几何处理与分块 (Geometry Processing and Tiling / Binning Pass)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顶点处理 (Vertex Processing)&lt;/strong&gt;：GPU 首先会完整执行一次顶点着色器，处理场景中的所有几何体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分块 (Tiling)&lt;/strong&gt;：一个专门的硬件单元（Tiler）会遍历所有图元（如三角形），确定每个图元覆盖了屏幕上的哪些 Tile。然后，它会为每个 Tile 生成一个该 Tile 内可见的图元列表（Primitive List）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据写入主存&lt;/strong&gt;：这个包含所有 Tile 的图元列表会被写入 &lt;strong&gt;系统内存&lt;/strong&gt;。&lt;strong&gt;这是在渲染像素之前，对系统内存的唯一一次大规模写入&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化与像素处理 (Rasterization and Pixel Processing)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逐 Tile 渲染&lt;/strong&gt;：GPU 会依次处理每一个 Tile。对于当前处理的 Tile，它会从系统内存中取回对应的图元列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载到片上内存&lt;/strong&gt;：该 Tile 的颜色和深度缓冲区（Tile Buffer）会被创建在高速的 &lt;strong&gt;片上内存&lt;/strong&gt; 中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化 (Rasterization)&lt;/strong&gt;：将 Tile 内的图元转换为片元（Fragment）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏面消除 (Hidden Surface Removal, HSR)&lt;/strong&gt;：在执行昂贵的像素着色器之前，TBDR 硬件会利用片上深度缓冲区进行深度测试，&lt;strong&gt;完美地剔除所有被遮挡的片元&lt;/strong&gt;。这意味着对于每个像素，只有一个片元（最前面的那一个）会被着色，从而极大地减少了 Overdraw（过绘制）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;像素处理 (Pixel/Fragment Shading)&lt;/strong&gt;：为通过 HSR 测试的可见片元执行像素着色器，计算最终颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写回主存 (Resolve Pass)&lt;/strong&gt;：当一个 Tile 内的所有像素都处理完毕后，最终的颜色数据会从片上内存一次性地写入到系统内存的帧缓冲区中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TBDR 的核心优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极低的内存带宽&lt;/strong&gt;：由于颜色和深度缓冲位于片上内存，像素读写操作（如深度测试、颜色混合）都在片上高速进行，避免了频繁读写慢速的系统内存，极大地节省了带宽和功耗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高效的隐藏面消除&lt;/strong&gt;：HSR 几乎能完全消除 Overdraw，避免了为不可见的像素执行昂贵的着色计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高效的 MSAA&lt;/strong&gt;：多重采样抗锯齿（MSAA）的多个样本点也存储在片上内存中，使得解析（Resolve）过程非常高效，带宽成本远低于 IMR。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;IMR: Immediate Mode Rendering (立即模式渲染)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1304c644.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;IMR 是一种更直接的渲染方式。它按照图元提交的顺序，&lt;strong&gt;立即&lt;/strong&gt; 对其进行处理，并将结果直接写入位于 &lt;strong&gt;系统内存（或桌面端的 VRAM）&lt;/strong&gt; 中的最终帧缓冲区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IMR 的渲染流程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;顶点处理 (Vertex Processing)&lt;/strong&gt;：与 TBDR 类似，处理顶点数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化 (Rasterization)&lt;/strong&gt;：将图元转换为片元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Early-Z 测试&lt;/strong&gt;：现代 IMR GPU 会尝试在像素着色前进行深度测试（Early-Z）来剔除部分被遮挡的像素。但由于物体绘制顺序的不确定性，Early-Z &lt;strong&gt;无法保证&lt;/strong&gt; 完美消除所有 Overdraw。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;像素处理 (Pixel Shading)&lt;/strong&gt;：为每个通过测试的片元执行像素着色器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合并 (Merge/ROP)&lt;/strong&gt;：执行深度更新（Late-Z）、颜色混合（Blending）等光栅操作，并将最终颜色写入系统内存的帧缓冲区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;IMR 的特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高带宽需求&lt;/strong&gt;：渲染过程中，深度测试、颜色混合等操作都需要反复读写位于系统内存中的缓冲区，对内存带宽消耗巨大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overdraw 问题&lt;/strong&gt;：如果物体没有从前到后严格排序，后绘制的物体会覆盖先绘制的物体，导致对被覆盖像素的着色计算被浪费，这就是 Overdraw。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强大的原始算力&lt;/strong&gt;：桌面端 GPU 通过极高的时钟频率和巨大的内存带宽来弥补这些效率上的不足，实现“力大砖飞”。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;软件渲染管线与硬件架构的适配&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;前向渲染 (Forward Rendering)&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流程&lt;/strong&gt;：为每个物体，计算所有光源对它的影响，然后渲染。如果一个物体受多个光源影响，它可能需要被渲染多次（Base Pass + Add Pass）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 TBDR 的适配&lt;/strong&gt;：前向渲染是 TBDR 的 &lt;strong&gt;天作之合&lt;/strong&gt;。TBDR 的 HSR 机制可以有效解决前向渲染中因多 Pass 渲染或场景遮挡产生的严重 Overdraw 问题，极大地提升了效率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 IMR 的适配&lt;/strong&gt;：在前向渲染模式下，IMR 架构会因 Overdraw 而浪费大量计算和带宽，尤其是在多光源场景下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;延迟渲染 (Deferred Rendering)&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流程&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;G-Buffer Pass&lt;/strong&gt;：首先渲染一遍所有不透明物体，但不计算光照，而是将它们的几何信息（如位置、法线、材质颜色等）输出到多个屏幕大小的缓冲区中，这个集合被称为 &lt;strong&gt;G-Buffer&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lighting Pass&lt;/strong&gt;：然后，通过一次全屏处理，读取 G-Buffer 中的信息，为屏幕上的每一个像素计算所有光源的光照总和。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 IMR 的适配&lt;/strong&gt;：延迟渲染是桌面端（IMR）处理海量动态光源的常用方案。因为 IMR 拥有极高的内存带宽，能够承受读写 G-Buffer 所带来的巨大带宽压力。光照计算与场景复杂度解耦，性能只与光源数量和屏幕分辨率相关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与 TBDR 的适配&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“经典”延迟渲染的挑战&lt;/strong&gt;：如果将 G-Buffer 存储在 &lt;strong&gt;系统内存&lt;/strong&gt; 中，那么在 Lighting Pass 阶段需要为每个像素从系统内存中读取大量数据。这会完全绕过 TBDR 的片上内存优势，导致灾难性的带宽消耗，使其在移动端上不可行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现代 TBDR 的解决方案&lt;/strong&gt;：现代 Arm Mali 等 TBDR GPU 提供了 &lt;strong&gt;像素本地存储（Pixel Local Storage）&lt;/strong&gt; 或类似的扩展功能。这允许开发者将 &lt;strong&gt;G-Buffer 也创建在高速的片上内存中&lt;/strong&gt;。对于一个 Tile，G-Buffer 的生成和光照计算的读取都可以在片上内存中完成，完全不需要与系统内存发生交互。这种技术被称为 &lt;strong&gt;Tile-Based Deferred Shading&lt;/strong&gt;，它使得延迟渲染在移动端同样高效可行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Forward 渲染&lt;/th&gt;
&lt;th&gt;Deferred 渲染&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;光照计算时机&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与几何处理同步，逐物体计算光照。&lt;/td&gt;
&lt;td&gt;先生成G-Buffer存储几何信息，再统一计算光照。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;光源数量性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;光源越多，物体被渲染的Pass越多，性能开销与 &lt;strong&gt;物体数 x 光源数&lt;/strong&gt; 相关，复杂度高。&lt;/td&gt;
&lt;td&gt;光照计算与几何解耦，性能主要受 &lt;strong&gt;光源数量和屏幕分辨率&lt;/strong&gt; 影响，适合大量动态光源。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overdraw优化&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在IMR上严重，依赖Early-Z。在TBDR上通过HSR可基本消除。&lt;/td&gt;
&lt;td&gt;G-Buffer阶段本身就有Overdraw，但光照阶段每个像素只计算一次，无Overdraw。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;透明物体支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持，但需要从后往前排序以保证混合正确。&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不直接支持&lt;/strong&gt;。因为G-Buffer通常只存储一个表面的信息。透明物体需要额外使用前向渲染管线来处理。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后处理兼容性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;依赖几何数据的后处理（如SSAO）难以实现或效率低。&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非常适合&lt;/strong&gt;。因为G-Buffer中已经包含了进行屏幕空间计算所需的所有几何信息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;硬件MSAA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;支持。在TBDR上效率极高。&lt;/td&gt;
&lt;td&gt;支持。但在IMR上因G-Buffer带宽翻倍而 &lt;strong&gt;非常昂贵&lt;/strong&gt;。在TBDR上通过片上G-Buffer可以高效实现。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;带宽与内存占用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较低（不产生G-Buffer）。&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非常高&lt;/strong&gt;。G-Buffer本身占用大量内存和带宽。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;适用架构与场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TBDR (移动端)&lt;/strong&gt;：天然契合，能效比高。&lt;strong&gt;IMR (桌面端)&lt;/strong&gt;：可用，但在多光源下效率低。&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMR (桌面端)&lt;/strong&gt;：常用方案，利用高带宽处理海量光源。&lt;strong&gt;TBDR (移动端)&lt;/strong&gt;：需使用 &lt;strong&gt;Tile-Based Deferred Shading&lt;/strong&gt; 技术将G-Buffer存于片上内存，同样高效。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Reference&lt;/h3&gt;
&lt;p&gt;三星推荐GPU实践：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.samsung.com/galaxy-gamedev/resources/articles/gpu-framebuffer.html#Tile-based-rasterization&quot;&gt;https://developer.samsung.com/galaxy-gamedev/resources/articles/gpu-framebuffer.html#Tile-based-rasterization&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;高通的面向高性能开发应用程序框架建议：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.qualcomm.com/bundle/publicresource/topics/80-78185-2/best_practices.html?product=1601111740035277&quot;&gt;https://docs.qualcomm.com/bundle/publicresource/topics/80-78185-2/best_practices.html?product=1601111740035277&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mobile GPU Architecture&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bytetech.info/articles/7329802236501393445?from=lark_all_search#doxcnSI8i5HF4LSLNxlGO4bXTAh&quot;&gt;https://bytetech.info/articles/7329802236501393445&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ARM Developer Document:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.arm.com/documentation/102662/0100/Tile-based-GPUs?lang=en&quot;&gt;https://developer.arm.com/documentation/102662/0100/Tile-based-GPUs?lang=en&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;TBDR、TBR、IMR三架构之间的区别：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/413922077&quot;&gt;https://zhuanlan.zhihu.com/p/413922077&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/390625258&quot;&gt;https://zhuanlan.zhihu.com/p/390625258&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>EXR vs. PNG</title><link>https://fuwari.vercel.app/posts/exr-vs-png/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/exr-vs-png/</guid><description>EXR vs. PNG的原理解析和实现方法</description><pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;&lt;strong&gt;1. &quot;贴图格式一般用 TGA&quot;&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;原因：&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TGA 的优势&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无损压缩&lt;/strong&gt;：TGA 支持无损压缩算法，保留图像原始质量，适合需要高精度的贴图（如模型纹理、法线贴图等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的像素深度&lt;/strong&gt;：TGA 支持 8 位、16 位、24 位、32 位像素深度（含 Alpha 通道），开发者可以根据需求选择精度（如 32 位 TGA 支持完整的透明通道）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性&lt;/strong&gt;：TGA 格式在游戏行业中广泛使用，尤其在早期项目中（如 3D 模型贴图、动画制作），许多工具链（如 Maya、3ds Max）和引擎（如 Unity）对 TGA 的支持非常成熟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alpha 通道支持&lt;/strong&gt;：TGA 的 Alpha 通道（透明度）可精确到 256 级（0-255），适合需要精细透明效果的场景（如粒子特效、复杂透明物体）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3D 模型贴图&lt;/strong&gt;：如漫反射贴图、法线贴图、高光贴图等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要高精度 Alpha 的场合&lt;/strong&gt;：如复杂透明物体（玻璃、树叶）或需要多级透明度的特效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;美术资源协作&lt;/strong&gt;：TGA 格式常用于美术团队内部协作（如 Photoshop 导出后直接供引擎使用）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;例外情况&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动平台优化&lt;/strong&gt;：在移动端，TGA 可能会被压缩为 ETC2、PVRTC 等硬件压缩格式，以减少内存占用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具链限制&lt;/strong&gt;：某些项目可能因工具链或流程限制选择其他格式（如 PSD 直接转 UI）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. &quot;PNG 常用在 UI 上&quot;&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PNG 的优势&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无损压缩与透明通道&lt;/strong&gt;：PNG 支持无损压缩，且有完整的 Alpha 透明通道（0-255），适合需要清晰边缘和透明背景的 UI 元素（如按钮、图标、文字）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件体积小&lt;/strong&gt;：PNG 的压缩率通常高于 TGA（尤其是优化后的 PNG-8），适合 UI 图片数量多、尺寸小的场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广泛兼容性&lt;/strong&gt;：PNG 是网页和 UI 设计的标准格式，工具链（如 Photoshop、Figma）导出 PNG 非常便捷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抗锯齿与清晰度&lt;/strong&gt;：PNG 的无损特性确保 UI 元素在缩放或旋转时不会失真。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2D 游戏 UI&lt;/strong&gt;：如菜单、按钮、血条、HUD（抬头显示器）等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;需要透明背景的图片&lt;/strong&gt;：如图标、徽章、动态 UI 元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebGL 或移动端优化&lt;/strong&gt;：PNG 的压缩特性更适合需要快速加载的轻量级项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;例外情况&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复杂动画或 Alpha 渐变&lt;/strong&gt;：如果 UI 需要复杂的透明度渐变或动画，TGA 可能更合适。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能敏感场景&lt;/strong&gt;：某些情况下，PNG 解压时间可能高于 TGA（但差异通常可忽略）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. &quot;TGA 方便 Alpha 通道操作，精度选择更自由&quot;&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alpha 通道精度&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TGA&lt;/strong&gt; 支持 32 位格式（RGB + Alpha），Alpha 通道为 8 位（256 级透明度），且支持手动编辑（如通过 Photoshop 或 GIMP 调整灰度图生成 Alpha）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PNG&lt;/strong&gt; 同样支持 32 位（RGBA），但某些工具链或引擎（如 Unity）在导入时可能对 PNG 的 Alpha 通道进行优化（如压缩或丢弃），导致精度损失（需手动配置）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活的像素深度&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;TGA 支持多种像素深度（如 8 位索引色 + Alpha），适合需要自定义精度的场景（如低配机型优化）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;美术工具支持&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;TGA 的 Alpha 通道在 Photoshop 等工具中可直接编辑（如通过灰度图生成透明度），而 PNG 的 Alpha 通道通常需要额外步骤。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;对比 PNG&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PNG 的限制&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;压缩优化问题&lt;/strong&gt;：某些工具（如 Unity）导入 PNG 时可能自动压缩 Alpha 通道（如将 256 级透明度简化为 1 位透明度），导致精度下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态生成 Alpha 的复杂性&lt;/strong&gt;：PNG 的 Alpha 通道通常直接依赖图像数据，而 TGA 支持从灰度图生成 Alpha（如法线贴图生成）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;总结与建议&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;常规用法&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TGA&lt;/strong&gt;：优先用于 &lt;strong&gt;3D 模型贴图、需要高精度 Alpha 的特效&lt;/strong&gt;（如粒子、透明物体）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PNG&lt;/strong&gt;：优先用于 &lt;strong&gt;UI、图标、需要透明背景的 2D 图片&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择依据&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;精度需求&lt;/strong&gt;：TGA 在 Alpha 通道和像素深度上更灵活。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件体积&lt;/strong&gt;：PNG 通常更小，适合 UI。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具链支持&lt;/strong&gt;：根据美术工具和引擎导入设置选择（如 Unity 对 PNG 的优化更成熟）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例外与优化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;移动端可优先使用压缩格式（如 ETC2、PVRTC），将 TGA/PNG 作为源文件。&lt;/li&gt;
&lt;li&gt;对于 UI，PNG 通常是更优选择；但对于需要复杂透明度的 UI 动画，TGA 可能更合适。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>HDRP的曝光流程和实现</title><link>https://fuwari.vercel.app/posts/hdrp-exposure/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/hdrp-exposure/</guid><description>详解Unity HDRP中的物理相机曝光流程和实现原理</description><pubDate>Thu, 03 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;现代很多的PBR游戏，大家首先关注的是PBR的材质，但是实际上PBR( Physically-Based Rendering )流程除了基于物理的材质模型，还由另外两部分组成，一个是物理光照，另外一个是物理相机。&lt;/p&gt;
&lt;p&gt;在很多引擎中虽然早已是走的PBR的渲染流程，但是开放给开发者的光照参数依然是一个无单位的值。这可能是为了降低一些开发的门槛。这同时也带来一些局限，无单位的值只能通过艺术家们的感觉和经验进行判断。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1ba7c2c9.png&quot; alt=&quot;URP的光照强度设置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;unity在HDRP上引入了物理相机/灯光/的概念，本质上是游戏引擎从经验驱动转向物理驱动的结果，游戏和电影开发者能够通过现实中的光测数据以及相机数据，精准的控制画面的表现，同时也引入了很多物理单位，下面让我们来了解一下曝光的整个流程。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;HDRP中，官方很贴心的给了我们一张表，告诉我们哪个环境下使用什么样的曝光值。&lt;img src=&quot;/images/posts/345e29a6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/946beb40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么这些曝光值是从哪来的呢？现代相机中存在三个参数影响了曝光：ISO，ShutterSpeed，Aperture。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;光圈(Aperture)：通常称为f值，表示镜头的焦距与镜头开口直径的比率。开口越大，进入相机并被传感器捕获的光线就越多。常用值为 f/1.4、f/2.0、f/2.8、f/4.0等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c68e9572.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快门速度(shutter speed) : 快门速度表示相机的传感器或胶片主动收集光线的时间，通常以秒的分数（1/60s、1/200s）表示。&lt;/li&gt;
&lt;li&gt;ISO：传感器对入射光的 ISO 或灵敏度。通常，ISO 值从 100 开始，成倍增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/096baca6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;EV（Exposure Value）则是通过这三个参数计算得出的。我们常见的EV100 时，就是以 ISO 100 作为标准感光度来计算曝光值的。厂商通过测光表实测不同天气下的光照强度，结合标准EV表（基于ISO 100的光圈/快门组合）生成推荐值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/a51e012c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HDRP的相机开启物理相机后，就能设置对应的ISO，ShutterSpeed，Aperture数据了，这些数据会根据公式计算出EV值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/b88d201d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/cd376141.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;结合设置物理灯光亮度，我们能得到一个和现实中拍摄一样的画面表现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/3d9c97b2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然对于我们&lt;a href=&quot;https://www.yuque.com/shuangfeidu/txwa8w/ixp5ze7hg1infbbm&quot;&gt;光栅化流程&lt;/a&gt;（详见我的另一个Blog提到的渲染管线）来说，实际上是没有相机建模的，最终还是用计算出一个EV值给到渲染管线中去进行渲染。但是通过对相机参数的设置，我们能够确定一个基于现实中的曝光基调，再通过这个基调进行实际的调整。&lt;/p&gt;
&lt;p&gt;相比于照片，游戏的曝光的复杂度在于不同的光照环境下, 我们需要不同的曝光度，即使是有了现实中EV值的参考，但是依旧是需要为特定的游戏环境来微调不同的EV值，这也是不小的工作量，还有就是，场景中可能同时存在室内+室外的场景，以及玩家视角的不断移动，我们需要正确的曝光过渡才能正确的渲染出画面。&lt;/p&gt;
&lt;p&gt;自动曝光就是来解决这个问题的，同样的模仿了现实中的相机的测光策略：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/cd78ff01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;游戏中最常见的是重点测光模式：更多的考虑画面中心的亮度，因为游戏的焦点以及主角的位置通常是画面的中心。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bd802d46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;自动曝光做的事情就是，采样整个画面的亮度，根据不同权重计算出平均值，再通过曝光调整将画面拉到一个合适的亮度。&lt;/p&gt;
&lt;p&gt;而这个亮度的标准叫中度灰：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/d1e09520.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有意思的是，但是由于我们人眼对于灰阶的感知并不是线性的，所以中间的灰调并不是50%，根据前人的规律总结，有几种常见的中度灰色:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/b0c8a946.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/a7a590f1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;黑手党1中的自动曝光：&lt;a href=&quot;https://www.yuque.com/attachments/yuque/0/2025/mp4/48487597/1751970439740-4fb6351a-ea51-4e9f-92ac-3dd3ebb77d73.mp4&quot;&gt;Mafia_ Definitive Edition 2025-03-08 20-05-57.mp4&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;自动曝光实现&lt;/h2&gt;
&lt;p&gt;在后效的第一个Pass中就开始自动曝光的处理工作，在&lt;strong&gt;Bloom/ColorGrading&lt;/strong&gt;这些之前，因为正确的曝光是其他后处理效果的前置条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CPU：
TextureHandle RenderPostProcess(） {
    var source = inputColor;
    ....
    source = DynamicExposurePass(renderGraph, hdCamera, source);
    
    TextureHandle bloomTexture = BloomPass(renderGraph, hdCamera, source, m_LensFlareScreenSpace.bloomMip.value, out screenSpaceLensFlareBloomMipTexture);
    TextureHandle logLutOutput = ColorGradingPass(renderGraph, hdCamera);
    ....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到用三个Pass完成了渲染。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/0eb787a1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一个pass：主要作用是降采样整个屏幕像素的亮度并转换为EV100值，以及计算权重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float SampleLuminance(float2 uv)
{
    // 这里获取了上一帧的平均亮度信息，单位是cd/m^2
    // prevExposure 为亮度值的倒数
    float prevExposure = ConvertEV100ToExposure(GetPreviousExposureEV100(), LensImperfectionExposureScale);
    float3 color = SAMPLE_TEXTURE2D_X_LOD(_SourceTexture, s_linear_clamp_sampler, uv, 0.0).xyz;

    // 根据上一帧的曝光还原原始辐射值，再转换为亮度输出
    return Luminance(color / prevExposure);
}

float WeightSample(uint2 pixel, float2 sourceSize, float luminance) {
    // 经典的Center-weighted，中心画面权重高
    float screenDiagonal = 0.5f * (sourceSize.x + sourceSize.y);
    const float2 kCenter = sourceSize * 0.5f;
    return 1.0 - saturate(pow(length(kCenter - pixel) / screenDiagonal, 1.0));
}

// CPU端传入 这里unity是直接将中度灰定义为12.5
float MeterCalibrationConstant = 12.5f;

// 根据EV = log2 (LS / K) 这里 L 是亮度， S是100 ISO 以及中间灰作为矫正常数
// 计算出矫正后的EV100值。
float ComputeEV100FromAvgLuminance(float avgLuminance, float calibrationConstant)
{
    const float K = calibrationConstant;
    return log2(avgLuminance * 100.0 / K);
}

[numthreads(8,8,1)]
    void KPrePass(uint2 dispatchThreadId : SV_DispatchThreadID)
{
    。。。
    float2 uv = GetScreenUV();
    float luma = SampleLuminance(uv);

    float weight = WeightSample(dispatchThreadId, PREPASS_TEX_SIZE.xx, luma);


    float logLuma = ComputeEV100FromAvgLuminance(max(luma, 1e-4), MeterCalibrationConstant);

    //最终输出两数值，一个是像素的亮度转换而成的EV100值，一个是权重
    _OutputTexture[posInputs.positionSS] = float2(logLuma, weight);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个Pass：将1024x1024的(EV100值, 权重)图进行均值计算，分为32x32个线程组，每个组32个线程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[numthreads(REDUCTION_GROUP_SIZE,REDUCTION_GROUP_SIZE,1)]
void KReduction(uint2 groupId : SV_GroupID, uint2 groupThreadId : SV_GroupThreadID) {
    // 每个线程处理2x2像素块
    uint2 sampleIdx = (groupId.xy * REDUCTION_GROUP_SIZE + groupThreadId.xy) * 2u;
    float2 p1 = _InputTexture[sampleIdx + uint2(0u, 0u)].xy;  
    float2 p2 = _InputTexture[sampleIdx + uint2(1u, 0u)].xy;  
    float2 p3 = _InputTexture[sampleIdx + uint2(0u, 1u)].xy;  
    float2 p4 = _InputTexture[sampleIdx + uint2(1u, 1u)].xy;  
    
    // 分解亮度和权重
    float4 smp = float4(p1.x, p2.x, p3.x, p4.x);     
    float4 weights = float4(p1.y, p2.y, p3.y, p4.y); 
    
    // 乘积并存入groupSharedMemory
    gs_luminances[threadIdx] = smp * weights;  
    gs_weights[threadIdx] = dot(weights, 1.0); 
    
    // 组内完成同步，确保同group内的线程都计算完成
    GroupMemoryBarrierWithGroupSync();
    
    //并行归约 （4像素-&amp;gt;2像素-&amp;gt;像素）
    //合并组内全部像素的亮度和权重
    for(uint s = REDUCTION_TOTAL_THREADS / 2u; s &amp;gt; 0u; s &amp;gt;&amp;gt;= 1u)
    {
        if(threadIdx &amp;lt; s)
        {
            gs_luminances[threadIdx] += gs_luminances[threadIdx + s];
            gs_weights[threadIdx] += gs_weights[threadIdx + s];
        }
    
        GroupMemoryBarrierWithGroupSync();
    }
    
    //组内第一个线程去操作，将所有结果输出
    if(threadIdx == 0u)
    {
        _OutputTexture[groupId.xy] = float2(avgLuminance, gs_weights[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三个Pass实际上就是又执行了一遍，将32x32平均到1x1，最终转换为(曝光值, EV100值)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    。。。
    
    if(threadIdx == 0u) {
        float avgLuminance = dot(gs_luminances[0], 0.25);
    
        if (IsNaN(avgLuminance) || IsInf(avgLuminance))
            avgLuminance = 1.0;
    
        if (gs_weights[0] &amp;gt; 0.0)
            avgLuminance /= (gs_weights[0] * 0.25);
        
        //取上一帧的rt进行lerp，防止曝光突变。
        float exposure = AdaptExposure(avgLuminance - ParamExposureCompensation);
        exposure = clamp(exposure, ParamExposureLimitMin, ParamExposureLimitMax);
        _OutputTexture[groupId.xy] = float2(ConvertEV100ToExposure(exposure, LensImperfectionExposureScale), exposure);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终在渲染下一帧的时候，光照计算读取最终曝光值做乘积。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//在下一帧中
diffuseLighting *= GetCurrentExposureMultiplier();
specularLighting *= GetCurrentExposureMultiplier();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他问题&lt;/h2&gt;
&lt;p&gt;当整个画面的动态范围(画面中“最亮”到“最暗”的比值)非常大的时候，会出现的问题就是如果亮部曝光正确，则暗部细节丢失，如果拉高曝光，则亮部信息丢失。例如从山洞中走出。由于中心加权特性，导致外面曝光正常，但是主角的暗部细节完全丢失了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/d903d9b0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;拉高曝光，则天空整个过曝了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/4e80e348.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过直方图也很简单的能看出信息丢失的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/8f5f10c9.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;/images/posts/722d2f18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tonemapping在一定程度上能缓和过曝的区域&lt;/strong&gt;，但是如果原始数据已经过曝严重，tonemapping也无法恢复丢失的细节。如果不想改Tonemapping的话可以和美术沟通加一个LUTs（虽然这会违背物理正确）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/6f7a1b5c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/8a436ed7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解决方式：一般来说只能从场景设计方面去规避，一方面是避免制作巨大亮度差异的场景，另外一个方法就是和现实中的摄影一样，对场景进行补光：&lt;/p&gt;
&lt;p&gt;例如下图，正常情况下，屋顶下应该是死黑，但是通过补光来弥补暗处的信息，虽然这不物理，但是work了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/a1e9a079.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有一些游戏的做法是在角色身上挂点光源 例如黑猴子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/22c66eff.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;p&gt;https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/Physical-Light-Units.html&lt;/p&gt;
&lt;p&gt;https://placeholderart.wordpress.com/2014/11/16/implementing-a-physically-based-camera-understanding-exposure/&lt;/p&gt;
&lt;p&gt;https://photo.stackexchange.com/questions/62307/why-is-18-grey-considered-to-be-in-the-middle-for-photography&lt;/p&gt;
&lt;p&gt;https://en.wikipedia.org/wiki/Middle_gray&lt;/p&gt;
&lt;p&gt;https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/&lt;/p&gt;
&lt;p&gt;https://bartwronski.com/2016/09/01/dynamic-range-and-evs/&lt;/p&gt;
&lt;p&gt;https://en.wikipedia.org/wiki/Exposure_(photography)&lt;/p&gt;
</content:encoded></item><item><title>Rider中Git的使用</title><link>https://fuwari.vercel.app/posts/git-usage-in-rider/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/git-usage-in-rider/</guid><description>Git项目中.gitignore文件的配置方法</description><pubDate>Wed, 02 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/9ca236815e7f&quot;&gt;https://www.jianshu.com/p/9ca236815e7f&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Huggingface镜像站</title><link>https://fuwari.vercel.app/posts/huggingface-mirror-site/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/huggingface-mirror-site/</guid><description>Huggingface镜像站的详细介绍和使用方法</description><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hugging Face的镜像网站主要有以下几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;‌&lt;strong&gt;官方推荐的镜像站&lt;/strong&gt;‌：Hugging Face官方提供了一个国内镜像网站，地址为：&lt;a href=&quot;https://hf-mirror.com/&quot;&gt;https://hf-mirror.com/&lt;/a&gt;‌。这个镜像站可以用于下载各种大模型的权重以及复现代码，是官方推荐的一个可靠来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ModelScope:&lt;/strong&gt;** **‌https://www.modelscope.cn/home&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;上海交通大学AIShell镜像&lt;/strong&gt;‌：上海交通大学为社区贡献了一个可靠的镜像站点，支持多种AI框架及相关资源的加速下载，其中包括Hugging Face的模型。不过，具体的URL需要访问上海交通大学的相关网站或搜索“上海交通大学AIShell镜像”来获取‌。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此外，还有一些其他可能的镜像站点或加速方法，如使用华为云ModelArts平台等。但需要注意的是，这些站点的可用性和稳定性可能会因时间和网络环境的变化而有所不同。因此，在使用时建议根据自己的实际需求和网络环境选择合适的镜像站点。&lt;/p&gt;
</content:encoded></item><item><title>Hugo博客提交Sitemap至Google Search Console</title><link>https://fuwari.vercel.app/posts/hugo-submit-sitemap-to-google-search-console/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/hugo-submit-sitemap-to-google-search-console/</guid><description>Hugo博客提交Sitemap至Google Search Console的详细介绍和使用方法</description><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;第一部分：什么是网站地图 (Sitemap)？&lt;/h3&gt;
&lt;p&gt;简单来说，网站地图（Sitemap）就是一个文件，它像一张你网站的“结构图”或“目录”。 &lt;a href=&quot;https://www.semrush.com/blog/website-sitemap/&quot;&gt;[1]&lt;/a&gt; 这个文件不是给访客看的，而是专门提供给 Google、Bing 这类搜索引擎的。&lt;/p&gt;
&lt;p&gt;它的主要作用有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;帮助搜索引擎发现页面&lt;/strong&gt;：它会列出你网站上所有的重要页面、文章、分类、标签等链接。 &lt;a href=&quot;https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview&quot;&gt;[2]&lt;/a&gt;&lt;a href=&quot;https://yoast.com/what-is-an-xml-sitemap-and-why-should-you-have-one/&quot;&gt;[3]&lt;/a&gt; 这确保了即使某些页面没有被其他地方链接到（称为“孤岛页面”），搜索引擎也能发现它们。 &lt;a href=&quot;https://www.semrush.com/blog/website-sitemap/&quot;&gt;[1]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高收录效率&lt;/strong&gt;：特别是对于新网站或者内容量很大的网站，提交网站地图可以告诉搜索引擎：“嘿，我这里有新内容了，快来看看吧！” 从而加快页面被收录的速度。 &lt;a href=&quot;https://www.semrush.com/blog/website-sitemap/&quot;&gt;[1]&lt;/a&gt;&lt;a href=&quot;https://ecomstrive.com/what-is-sitemap/&quot;&gt;[4]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供附加信息&lt;/strong&gt;：网站地图还可以包含每个页面的最后更新时间、更新频率等元数据，帮助搜索引擎更智能地抓取你的网站。 &lt;a href=&quot;https://yoast.com/what-is-an-xml-sitemap-and-why-should-you-have-one/&quot;&gt;[3]&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个文件通常是一个名为 &lt;code&gt;sitemap.xml&lt;/code&gt; 的 XML 文件。 &lt;a href=&quot;https://yoast.com/what-is-an-xml-sitemap-and-why-should-you-have-one/&quot;&gt;[3]&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第二部分：如何在你的 Hugo 博客中找到网站地图？&lt;/h3&gt;
&lt;p&gt;这是最棒的部分：&lt;strong&gt;Hugo 已经自动为你生成了网站地图！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除非你特意在配置文件中禁用了它，否则 Hugo 在每次生成你的网站时，都会在网站的根目录创建一个 &lt;code&gt;sitemap.xml&lt;/code&gt; 文件。 &lt;a href=&quot;https://gohugo.io/templates/sitemap/&quot;&gt;[5]&lt;/a&gt;&lt;a href=&quot;https://hugo.zcopy.site/templates/sitemap/&quot;&gt;[6]&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你的网站地图地址是：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://你的域名.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，如果你的博客域名是 &lt;code&gt;my-awesome-blog.com&lt;/code&gt;，那么你的网站地图就在 &lt;code&gt;https://my-awesome-blog.com/sitemap.xml&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何验证？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;直接访问&lt;/strong&gt;：在浏览器地址栏输入你的网站地图地址，如果能看到一个 XML 格式的文件内容，那就证明它存在且工作正常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地检查&lt;/strong&gt;：在你的电脑上，运行 &lt;code&gt;hugo&lt;/code&gt; 命令来生成网站。然后查看生成的 &lt;code&gt;public&lt;/code&gt; 文件夹，你应该能在里面直接找到 &lt;code&gt;sitemap.xml&lt;/code&gt; 这个文件。这可以确认文件在部署到 Cloudflare Pages 之前就已经被正确生成了。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;第三部分：如何解决 Google 的警告（提交网站地图）&lt;/h3&gt;
&lt;p&gt;既然找到了地图，我们只需要把它交给 Google 就行了。这个过程是在 Google Search Console (谷歌搜索控制台) 中完成的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;登录 Google Search Console&lt;/strong&gt;&lt;br /&gt;
访问 &lt;a href=&quot;https://search.google.com/search-console/&quot;&gt;Google Search Console&lt;/a&gt; 并登录你的 Google 账户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择你的网站&lt;/strong&gt;&lt;br /&gt;
在左上角的下拉菜单中，确保你选择了正确的网站属性（即你的博客域名）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导航到“网站地图”&lt;/strong&gt;&lt;br /&gt;
在左侧的菜单栏中，找到“索引”分类，然后点击下方的“&lt;strong&gt;网站地图&lt;/strong&gt;”(Sitemaps)。 &lt;a href=&quot;https://raddinteractive.com/how-to-add-a-sitemap-to-google-search-console-step-by-step/&quot;&gt;[7]&lt;/a&gt;&lt;a href=&quot;https://yoast.com/help/submit-sitemap-search-engines/&quot;&gt;[8]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加新的网站地图&lt;/strong&gt;&lt;br /&gt;
在页面顶部的“添加新的网站地图”输入框中，你不需要输入完整的 URL。因为 Google 已经知道了你的域名，你只需要输入文件名即可：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sitemap.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后点击“&lt;strong&gt;提交&lt;/strong&gt;”(Submit) 按钮。 &lt;a href=&quot;https://raddinteractive.com/how-to-add-a-sitemap-to-google-search-console-step-by-step/&quot;&gt;[7]&lt;/a&gt;&lt;a href=&quot;https://zh-cn.site123.com/support/1327976-%E5%90%91-google-search-console-%E6%8F%90%E4%BA%A4%E7%AB%99%E7%82%B9%E5%9C%B0%E5%9B%BE&quot;&gt;[9]&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;等待处理&lt;/strong&gt;&lt;br /&gt;
提交后，Google 会去抓取并处理你的网站地图。初始状态可能会显示为“已提交”或“正在处理”。过一段时间（几小时到几天不等），如果一切顺利，状态会变为“&lt;strong&gt;成功&lt;/strong&gt;”，并且会显示 Google 从中发现的网址数量。 &lt;a href=&quot;https://raddinteractive.com/how-to-add-a-sitemap-to-google-search-console-step-by-step/&quot;&gt;[7]&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成以上步骤后，Google Search Console 的警告就会消失，并且 Google 能更有效地收录你博客上的所有文章和页面。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第四部分：额外提示与检查&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;确认未被禁用&lt;/strong&gt;：极少数情况下，你可能在 Hugo 的配置文件（如 &lt;code&gt;hugo.toml&lt;/code&gt; 或 &lt;code&gt;config.toml&lt;/code&gt;）中禁用了网站地图。请检查文件中是否有类似 &lt;code&gt;disableKinds = [&quot;sitemap&quot;]&lt;/code&gt; 的配置。 &lt;a href=&quot;https://gohugo.io/templates/sitemap/&quot;&gt;[5]&lt;/a&gt; 如果有，请将 &lt;code&gt;&quot;sitemap&quot;&lt;/code&gt; 从列表中移除，然后重新生成和部署你的网站。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Pages 的角色&lt;/strong&gt;：在这个流程中，Cloudflare Pages 只是一个托管平台。它忠实地把你用 Hugo 生成的所有静态文件（包括 &lt;code&gt;sitemap.xml&lt;/code&gt;）部署到网络上。所以，你不需要在 Cloudflare Pages 上做任何额外配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐做法：&lt;/strong&gt;&lt;code&gt;**robots.txt**&lt;/code&gt;：为了让搜索引擎更容易找到你的网站地图，一个最佳实践是在 &lt;code&gt;robots.txt&lt;/code&gt; 文件中声明它的位置。
&lt;ul&gt;
&lt;li&gt;在 Hugo 项目的 &lt;code&gt;static&lt;/code&gt; 文件夹下创建一个名为 &lt;code&gt;robots.txt&lt;/code&gt; 的文件。&lt;/li&gt;
&lt;li&gt;在文件中加入以下内容（记得替换成你自己的域名）：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;User-agent: *
Allow: /

Sitemap: https://你的域名.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;- 这样，搜索引擎在访问你网站时，会首先查看 `robots.txt`，并立刻得知网站地图的位置了。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Learn more:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.semrush.com/blog/website-sitemap/&quot;&gt;What Is a Sitemap? Website Sitemaps Explained - Semrush&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview&quot;&gt;What Is a Sitemap | Google Search Central | Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yoast.com/what-is-an-xml-sitemap-and-why-should-you-have-one/&quot;&gt;What is an XML sitemap and why should you have one? - Yoast&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ecomstrive.com/what-is-sitemap/&quot;&gt;What Is A Sitemap? XML, HTML and Visual Explained - eComStrive.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gohugo.io/templates/sitemap/&quot;&gt;Sitemap templates - Hugo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hugo.zcopy.site/templates/sitemap/&quot;&gt;Sitemap templates | Hugo中文网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://raddinteractive.com/how-to-add-a-sitemap-to-google-search-console-step-by-step/&quot;&gt;How to Add a Sitemap to Google Search Console (Step-by-Step) - Radd Interactive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yoast.com/help/submit-sitemap-search-engines/&quot;&gt;Submit your sitemap to search engines - Yoast&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zh-cn.site123.com/support/1327976-%E5%90%91-google-search-console-%E6%8F%90%E4%BA%A4%E7%AB%99%E7%82%B9%E5%9C%B0%E5%9B%BE&quot;&gt;向Google Search Console 提交站点地图| 支持中心 - 创建免费网站- SITE123&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>重生之我在OBS写Shader——OBS ShaderFilter插件</title><link>https://fuwari.vercel.app/posts/%E9%87%8D%E7%94%9F%E4%B9%8B%E6%88%91%E5%9C%A8obs%E5%86%99shaderobs-shaderfilter%E6%8F%92%E4%BB%B6/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E9%87%8D%E7%94%9F%E4%B9%8B%E6%88%91%E5%9C%A8obs%E5%86%99shaderobs-shaderfilter%E6%8F%92%E4%BB%B6/</guid><description>在OBS中使用ShaderFilter插件创建自定义视觉效果</description><pubDate>Mon, 30 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;OBS ShaderFilter是一个可以使用HLSL/GLSL对图层进行处理的插件，而且内部会有很多OBS原生并不会带有的滤镜，比如box模糊和一堆搞怪滤镜。&lt;a href=&quot;https://obsproject.com/forum/resources/obs-shaderfilter.1736/&quot;&gt;https://obsproject.com/forum/resources/obs-shaderfilter.1736/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;直接下载exe文件，可以直接锁定到OBS目录并安装，还是很方便的。&lt;/p&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;p&gt;安装后，对某个图层右键——滤镜——＋User Defined Filter。在这里你可以选择使用effect文件、还是shader文件、亦或是直接自行输入hlsl。&lt;img src=&quot;/images/posts/5f772527.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然我自己的话比较熟悉HLSL，对于HLSL的一些函数比较眼熟，所以我偏向于直接用HLSL。当然用OpenGL也不是不行，要在快捷方式和高级设置中稍微改一下即可。&lt;/p&gt;
&lt;h3&gt;如何让OBS启用OpenGL渲染？&lt;/h3&gt;
&lt;p&gt;因为程序会优先启动微软的DirectX渲染，也就是hlsl，但是如果偏要使用OpenGL也是可以的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step1. 增加 --allow-opengl 启动参数，选中&lt;strong&gt;&lt;strong&gt;OBS快捷方式&lt;/strong&gt;&lt;/strong&gt;，在目标后面增加&lt;/strong&gt;** --allow-opengl**&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/23558ff0.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step2. 设置中进行切换&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进入obs设置界面，在 高级-视频-渲染器 中切换到OpenGL，&lt;strong&gt;切换应用设置之后需要重启&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/25cc2689.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;关于OBS和ShaderToy一些系统变量的变换&lt;/h2&gt;
&lt;p&gt;ShaderToy会有这些基础变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iTime：时间&lt;/li&gt;
&lt;li&gt;iResolution：屏幕分辨率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OBS（hlsl）则会有这些变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;elapsed_time&lt;/code&gt;：OBS的时间变量&lt;/li&gt;
&lt;li&gt;uv_size  ：uv大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时由于语言不同，一些运算函数也会有所不同。比如HLSL需要使用&lt;code&gt;fract()&lt;/code&gt;和&lt;code&gt;fract2()&lt;/code&gt;函数实现GLSL的&lt;code&gt;mod()&lt;/code&gt;计算。&lt;/p&gt;
&lt;p&gt;最后奉上一个我自己转换的shader；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 立体线框立方体着色器 - 从Shadertoy (https://shadertoy.com/view/McS3DW) 转换为OBS格式

uniform float speed&amp;lt;
    string label = &quot;动画速度&quot;;
    string widget_type = &quot;slider&quot;;
    float minimum = 0.0;
    float maximum = 200.0;
    float step = 0.01;
&amp;gt; = 100.0;

uniform float thickness&amp;lt;
    string label = &quot;线条粗细&quot;;
    string widget_type = &quot;slider&quot;;
    float minimum = 0.1;
    float maximum = 50.0;
    float step = 0.1;
&amp;gt; = 15.0;

uniform float scale&amp;lt;
    string label = &quot;图案缩放&quot;;
    string widget_type = &quot;slider&quot;;
    float minimum = 1.0;
    float maximum = 50.0;
    float step = 0.1;
&amp;gt; = 10.0;

uniform float4 line_color&amp;lt;
    string label = &quot;线条颜色&quot;;
    string widget_type = &quot;color&quot;;
&amp;gt; = {1.0, 1.0, 1.0, 1.0};

uniform float opacity&amp;lt;
    string label = &quot;整体不透明度&quot;;
    string widget_type = &quot;slider&quot;;
    float minimum = 0.0;
    float maximum = 1.0;
    float step = 0.01;
&amp;gt; = 1.0;

// 辅助函数
float fract(float v){
    return v - floor(v);
}

float2 fract2(float2 v){
    return float2(v.x - floor(v.x), v.y - floor(v.y));
}

// 计算点到线段的距离
float segment(float2 p, float2 a, float2 b) {
    p -= a;
    b -= a;
    return length(p - b * clamp(dot(p, b) / dot(b, b), 0.0, 1.0));
}

// 旋转函数
float2 rotate(float2 p, float a) {
    float c = cos(a);
    float s = sin(a);
    return float2(p.x * c - p.y * s, p.x * s + p.y * c);
}

// 使用旋转变换3D点（现在接受旋转值作为参数，而不是使用全局变量）
float2 T(float3 p, float rotation) {
    p.xy = rotate(p.xy, -rotation);
    p.xz = rotate(p.xz, 0.785); // 约45度
    p.yz = rotate(p.yz, -0.625); // 约-36度
    
    return p.xy;
}

float4 mainImage(VertData v_in) : TARGET {
    // 采样背景图像
    float4 originalColor = image.Sample(textureSampler, v_in.uv);
    
    float2 R = uv_size;
    float2 u = v_in.uv;
    u.y = 1.0 - u.y; // 翻转Y坐标以匹配Shadertoy
    u = u * R; // 从0-1范围转换为像素坐标
    
    // 计算图案坐标
    float2 X, U = scale * u / R.y;
    float2 M = float2(2.0, 2.3); // 平铺大小
    float2 I = floor(U/M)*M;
    float2 J;
    
    // 计算平铺模数
    U = fract2(U/M)*M;
    
    // 初始无线条
    float lineIntensity = 0.0;
    
    // 绘制2x2网格中的四个瓦片
    for (int k = 0; k &amp;lt; 4; k++) {
        X = float2(k % 2, k / 2) * M;
        J = I + X;
        
        // 偏移每隔一个瓦片以增加视觉效果
        if (int((J.x / M.x) + (J.y / M.y)) % 2 &amp;gt; 0) X.y += 1.15;
        
        // 计算时间依赖的旋转（现在作为局部变量）
        float adjustedTime = elapsed_time * speed / 100.0;
        float t_rotation = tanh(-0.2 * (J.x + J.y) + fract(2.0 * adjustedTime / 10.0) * 10.0 - 1.6) * 0.785;
        
        // 绘制立方体的六个部分（共12条线）
        for (float a = 0.0; a &amp;lt; 6.0; a += 1.57) { // 每约90度
            float3 A = float3(cos(a), sin(a), 0.7);
            float3 B = float3(-A.y, A.x, 0.7);
            
            // 绘制立方体边缘的线条，将旋转值作为参数传递
            lineIntensity += smoothstep(thickness/R.y, 0.0, segment(U-X, T(A, t_rotation), T(B, t_rotation)));
            lineIntensity += smoothstep(thickness/R.y, 0.0, segment(U-X, T(A, t_rotation), T(A * float3(1.0, 1.0, -1.0), t_rotation)));
            
            // 镜像点用于立方体背面
            A.z = -A.z; 
            B.z = -B.z;
            lineIntensity += smoothstep(thickness/R.y, 0.0, segment(U-X, T(A, t_rotation), T(B, t_rotation)));
        }
    }
    
    // 限制值避免线条重叠区域过亮
    lineIntensity = min(lineIntensity, 1.0);
    
    // 应用线条颜色并与原始图像混合
    float4 finalColor = float4(
        lerp(originalColor.rgb, line_color.rgb, lineIntensity * opacity),
        originalColor.a
    );
    
    return finalColor;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Unity的Scene界面鼠标滚轮慢到拖不动的问题</title><link>https://fuwari.vercel.app/posts/unity%E7%9A%84scene%E7%95%8C%E9%9D%A2%E9%BC%A0%E6%A0%87%E6%BB%9A%E8%BD%AE%E6%85%A2%E5%88%B0%E6%8B%96%E4%B8%8D%E5%8A%A8%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity%E7%9A%84scene%E7%95%8C%E9%9D%A2%E9%BC%A0%E6%A0%87%E6%BB%9A%E8%BD%AE%E6%85%A2%E5%88%B0%E6%8B%96%E4%B8%8D%E5%8A%A8%E7%9A%84%E9%97%AE%E9%A2%98/</guid><description>Unity的Scene界面鼠标滚轮慢到拖不动的问题的解决方案和处理方法</description><pubDate>Thu, 19 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;又水一篇（&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/6f1211b0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Unity的旋转、缩放、移动矩阵</title><link>https://fuwari.vercel.app/posts/unity%E7%9A%84%E6%97%8B%E8%BD%AC%E7%BC%A9%E6%94%BE%E7%A7%BB%E5%8A%A8%E7%9F%A9%E9%98%B5/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity%E7%9A%84%E6%97%8B%E8%BD%AC%E7%BC%A9%E6%94%BE%E7%A7%BB%E5%8A%A8%E7%9F%A9%E9%98%B5/</guid><description>Unity的旋转、缩放、移动矩阵的原理解析和实现方法</description><pubDate>Tue, 17 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在Unity中，&lt;strong&gt;移动（平移）、旋转、缩放&lt;/strong&gt;的矩阵变换是通过&lt;strong&gt;4×4的齐次坐标矩阵&lt;/strong&gt;实现的，这些矩阵共同构成了物体在3D空间中的变换。以下是每个部分的详细解释及其作用：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;1. 平移（Translation）矩阵&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动物体&lt;/strong&gt;：将物体在空间中沿X、Y、Z轴方向平移，改变物体的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;矩阵结构&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| 1   0   0   tx |
| 0   1   0   ty |
| 0   0   1   tz |
| 0   0   0   1  |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数&lt;/strong&gt;：&lt;code&gt;tx&lt;/code&gt;, &lt;code&gt;ty&lt;/code&gt;, &lt;code&gt;tz&lt;/code&gt; 是沿X、Y、Z轴的位移量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位置&lt;/strong&gt;：平移向量位于矩阵的&lt;strong&gt;第四列&lt;/strong&gt;（前三行最后一列）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;仅对点有效&lt;/strong&gt;：平移仅对具有位置的点（&lt;code&gt;w=1&lt;/code&gt;）生效，对方向向量（&lt;code&gt;w=0&lt;/code&gt;）无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变换顺序&lt;/strong&gt;：平移通常在旋转和缩放之后应用（如复合变换&lt;code&gt;M = T * R * S&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 旋转（Rotation）矩阵&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;改变物体方向&lt;/strong&gt;：绕X、Y、Z轴旋转，保持物体形状不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;矩阵结构&lt;/strong&gt;：&lt;/h4&gt;
&lt;p&gt;旋转矩阵是&lt;strong&gt;3×3的正交矩阵&lt;/strong&gt;，位于齐次矩阵的左上角（前3行前3列）。具体形式取决于旋转轴：&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;绕X轴旋转（θ角）&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| 1      0        0      0 |
| 0  cosθ   -sinθ   0 |
| 0  sinθ    cosθ   0 |
| 0      0        0      1 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;绕Y轴旋转（θ角）&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| cosθ   0   sinθ   0 |
| 0      1    0     0 |
| -sinθ  0   cosθ   0 |
| 0      0    0     1 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;绕Z轴旋转（θ角）&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| cosθ  -sinθ   0   0 |
| sinθ   cosθ   0   0 |
| 0       0     1   0 |
| 0       0     0   1 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正交性&lt;/strong&gt;：旋转矩阵的行/列向量是单位向量且彼此正交（确保不扭曲形状）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组合旋转&lt;/strong&gt;：多个旋转可通过矩阵相乘组合（如欧拉角&lt;code&gt;Rx * Ry * Rz&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unity中的实现&lt;/strong&gt;：Unity使用四元数（Quaternion）简化旋转操作，但底层仍通过矩阵计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 缩放（Scaling）矩阵&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;改变物体尺寸&lt;/strong&gt;：沿X、Y、Z轴缩放，控制物体的大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;矩阵结构&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| sx   0    0    0 |
| 0   sy    0    0 |
| 0    0   sz    0 |
| 0    0    0    1 |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数&lt;/strong&gt;：&lt;code&gt;sx&lt;/code&gt;, &lt;code&gt;sy&lt;/code&gt;, &lt;code&gt;sz&lt;/code&gt; 是沿X、Y、Z轴的缩放因子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位置&lt;/strong&gt;：缩放因子位于矩阵的&lt;strong&gt;主对角线&lt;/strong&gt;（左上3×3子矩阵的对角线）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;均匀/非均匀缩放&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;均匀缩放&lt;/strong&gt;（&lt;code&gt;sx = sy = sz&lt;/code&gt;）：保持物体形状比例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非均匀缩放&lt;/strong&gt;（如&lt;code&gt;sx ≠ sy&lt;/code&gt;）：可能导致物体变形（如拉伸）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对旋转的影响&lt;/strong&gt;：非均匀缩放会改变旋转轴的方向，需谨慎使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 齐次坐标（Homogeneous Coordinates）&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;作用&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;统一表示变换&lt;/strong&gt;：通过添加&lt;code&gt;w&lt;/code&gt;分量，将平移、旋转、缩放统一为矩阵乘法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;结构&lt;/strong&gt;：&lt;/h4&gt;
&lt;p&gt;齐次坐标将3D点&lt;code&gt;(x, y, z)&lt;/code&gt;扩展为&lt;code&gt;(x, y, z, 1)&lt;/code&gt;，向量扩展为&lt;code&gt;(x, y, z, 0)&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;点（&lt;/strong&gt;&lt;code&gt;w=1&lt;/code&gt;&lt;strong&gt;）&lt;/strong&gt;：参与平移和线性变换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向量（&lt;/strong&gt;&lt;code&gt;w=0&lt;/code&gt;&lt;strong&gt;）&lt;/strong&gt;：仅参与旋转和缩放（无平移）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;关键点&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;矩阵乘法兼容性&lt;/strong&gt;：齐次坐标允许将平移、旋转、缩放组合成单个4×4矩阵。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unity中的应用&lt;/strong&gt;：所有变换矩阵（如&lt;code&gt;Model&lt;/code&gt;、&lt;code&gt;View&lt;/code&gt;、&lt;code&gt;Projection&lt;/code&gt;）均基于齐次坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 复合变换（Combined Transform）&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在Unity中，物体的变换通常通过&lt;strong&gt;缩放→旋转→平移&lt;/strong&gt;的顺序组合：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;M_{\text{复合}} = T \times R \times S
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;矩阵结构&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;| Rx*x  Ry*x  Rz*x  tx |
| Rx*y  Ry*y  Rz*y  ty |
| Rx*z  Ry*z  Rz*z  tz |
| 0     0     0     1  |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;左上3×3&lt;/strong&gt;：旋转后的缩放矩阵（&lt;code&gt;R × S&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第四列&lt;/strong&gt;：平移向量&lt;code&gt;T&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后一行&lt;/strong&gt;：始终为&lt;code&gt;(0, 0, 0, 1)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Unity中的实现&lt;/strong&gt;：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transform组件&lt;/strong&gt;：封装了位置（Position）、旋转（Rotation）、缩放（Scale），底层通过矩阵运算实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matrix4x4类&lt;/strong&gt;：直接操作矩阵（如&lt;code&gt;Matrix4x4.TRS&lt;/code&gt;生成复合矩阵）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;6. 典型应用示例&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;(1) 移动物体&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 生成平移矩阵
Matrix4x4 translation = Matrix4x4.Translate(new Vector3(5, 0, 0));
Vector3 point = new Vector3(1, 2, 3);
Vector3 translatedPoint = translation.MultiplyPoint(point); // 结果(6,2,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;(2) 旋转物体&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 绕Y轴旋转90度
Matrix4x4 rotation = Matrix4x4.Rotate(Quaternion.Euler(0, 90, 0));
Vector3 rotatedPoint = rotation.MultiplyPoint(new Vector3(1, 0, 0)); // 结果(0,0,-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;(3) 缩放物体&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// X轴缩放2倍
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(2, 1, 1));
Vector3 scaledPoint = scale.MultiplyPoint(new Vector3(1, 2, 3)); // 结果(2,2,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;(4) 复合变换&lt;/strong&gt;：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 缩放→旋转→平移
Matrix4x4 transformMatrix = Matrix4x4.TRS(
    position: new Vector3(5, 0, 0),    // 平移
    rotation: Quaternion.Euler(0, 90, 0), // 旋转
    scale: new Vector3(2, 1, 1)        // 缩放
);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;7. 注意事项&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;矩阵相乘顺序&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;右乘优先&lt;/strong&gt;：&lt;code&gt;M = T × R × S&lt;/code&gt; 表示先缩放，再旋转，最后平移（矩阵相乘顺序与操作顺序相反）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非均匀缩放的风险&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;非均匀缩放会破坏旋转矩阵的正交性，可能导致法线方向错误（需重新计算法线）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unity的优化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Unity通过四元数（Quaternion）和欧拉角（EulerAngles）封装旋转，避免直接操作旋转矩阵的复杂性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平移矩阵&lt;/strong&gt;：控制物体位置（第四列）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旋转矩阵&lt;/strong&gt;：控制物体方向（左上3×3的正交子矩阵）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缩放矩阵&lt;/strong&gt;：控制物体尺寸（左上3×3的对角线）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;齐次坐标&lt;/strong&gt;：统一平移、旋转、缩放的计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这些矩阵的组合，Unity实现了物体在3D空间中的灵活变换，开发者可通过&lt;code&gt;Transform&lt;/code&gt;组件或直接操作矩阵实现复杂效果。&lt;/p&gt;
&lt;h1&gt;为什么是这个顺序？&lt;/h1&gt;
&lt;p&gt;在3D图形学和Unity引擎中，&lt;strong&gt;变换操作的顺序必须遵循“先缩放（Scale）→再旋转（Rotation）→最后平移（Translation）”&lt;/strong&gt;，这是因为矩阵乘法的&lt;strong&gt;非交换性&lt;/strong&gt;和变换的&lt;strong&gt;几何意义&lt;/strong&gt;所决定的。以下是详细解释：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;1. 变换顺序的核心原因：矩阵乘法的非交换性&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;矩阵乘法不满足交换律（即 (&lt;img src=&quot;/images/posts/d111982c.svg&quot; alt=&quot;image&quot; /&gt;)），因此变换的顺序直接影响最终结果。复合变换的矩阵乘法顺序需要从右到左依次应用，即：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/4cfe2b6f.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但实际操作顺序是&lt;strong&gt;先缩放、再旋转、最后平移&lt;/strong&gt;（即从右到左读取矩阵相乘的顺序）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 顺序分解：缩放 → 旋转 → 平移&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;(1) 先缩放（Scale）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：改变物体的尺寸（沿坐标轴方向放大或缩小）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;缩放操作&lt;strong&gt;不改变坐标轴的方向和原点&lt;/strong&gt;，因此后续的旋转和平移不会受到缩放的影响。&lt;/li&gt;
&lt;li&gt;如果先缩放，后续的旋转将以缩放后的坐标轴为基准（例如，缩放后旋转会以缩放后的轴方向进行旋转）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(2) 再旋转（Rotation）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：改变物体的方向（绕坐标轴旋转）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;旋转会改变坐标轴的方向，但&lt;strong&gt;不改变原点&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果旋转在缩放之后，旋转后的坐标轴方向是基于缩放后的物体尺寸（例如，旋转后的轴方向不会因后续的平移而改变）。&lt;/li&gt;
&lt;li&gt;如果旋转在平移之前，旋转是围绕原点进行的，而平移后的物体位置不会被旋转影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(3) 最后平移（Translation）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：移动物体到目标位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;平移会改变物体的&lt;strong&gt;原点位置&lt;/strong&gt;，但不会改变坐标轴的方向或缩放后的尺寸。&lt;/li&gt;
&lt;li&gt;如果平移在最后，它将物体从原点移动到目标位置，而缩放和旋转已经确定了物体的尺寸和方向。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 顺序错误的后果&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;(1) 先平移再缩放&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：平移后的物体距离原点更远，缩放会放大或缩小平移的距离。
&lt;ul&gt;
&lt;li&gt;例如：先平移 &lt;code&gt;(2, 0, 0)&lt;/code&gt;，再缩放 &lt;code&gt;2&lt;/code&gt;，最终位置会是 &lt;code&gt;(4, 0, 0)&lt;/code&gt;（缩放影响了平移的距离）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预期&lt;/strong&gt;：可能希望缩放仅改变尺寸，但平移后的缩放会扭曲位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(2) 先旋转再缩放&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：旋转后的坐标轴方向可能与缩放方向不一致，导致物体沿旋转后的轴方向缩放，产生意外变形。
&lt;ul&gt;
&lt;li&gt;例如：先绕Y轴旋转90°，再沿X轴缩放2倍，实际会沿旋转后的X轴（原Z轴方向）缩放，导致方向错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(3) 先平移再旋转&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：旋转会围绕原点进行，而平移后的物体远离原点，导致旋转中心不正确。
&lt;ul&gt;
&lt;li&gt;例如：先平移 &lt;code&gt;(5, 0, 0)&lt;/code&gt;，再绕原点旋转90°，物体将绕原点旋转，而非自身的中心。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 为什么Unity和其他引擎遵循此顺序？&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;(1) 符合数学定义&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复合变换矩阵&lt;/strong&gt;：&lt;br /&gt;
Unity的&lt;code&gt;Transform&lt;/code&gt;组件默认的变换顺序是缩放→旋转→平移，其矩阵表达式为：&lt;br /&gt;
&lt;img src=&quot;/images/posts/1b55c297.svg&quot; alt=&quot;image&quot; /&gt;
&lt;ul&gt;
&lt;li&gt;其中，(S)是缩放矩阵，(R)是旋转矩阵，(T)是平移矩阵。&lt;/li&gt;
&lt;li&gt;这样，物体的变换过程是：先缩放自身，再旋转自身方向，最后平移到目标位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(2) 父子对象的层级关系&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地坐标系的继承&lt;/strong&gt;：&lt;br /&gt;
子对象的变换是基于父对象的变换后的坐标系。例如：
&lt;ul&gt;
&lt;li&gt;如果父对象先缩放，子对象的旋转和后续平移会基于父对象缩放后的坐标轴。&lt;/li&gt;
&lt;li&gt;如果父对象先平移，子对象的缩放和旋转会基于父对象的位置，可能导致位置偏移。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(3) 避免几何错误&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保持旋转中心&lt;/strong&gt;：&lt;br /&gt;
默认顺序确保旋转和缩放均以物体的&lt;strong&gt;本地原点&lt;/strong&gt;（通常是几何中心）为基准，避免因平移导致旋转中心偏移。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 实际应用中的验证&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;(1) 场景中的立方体&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正确顺序（缩放→旋转→平移）&lt;/strong&gt;：&lt;br /&gt;
一个立方体先缩放为 &lt;code&gt;2x2x2&lt;/code&gt;，绕Y轴旋转90°，最后平移到 &lt;code&gt;(5, 0, 0)&lt;/code&gt;，结果符合预期。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误顺序（平移→旋转→缩放）&lt;/strong&gt;：&lt;br /&gt;
先平移到 &lt;code&gt;(5, 0, 0)&lt;/code&gt;，再旋转会导致物体绕原点旋转（远离中心），缩放会改变平移后的距离，导致位置混乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(2) Unity代码示例&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 正确顺序：缩放→旋转→平移
transform.localScale = new Vector3(2, 2, 2);  // 缩放
transform.Rotate(0, 90, 0);                   // 旋转（绕Y轴）
transform.position = new Vector3(5, 0, 0);    // 平移

// 错误顺序：平移→旋转→缩放
transform.position = new Vector3(5, 0, 0);    // 平移
transform.Rotate(0, 90, 0);                   // 旋转（绕原点，导致位置偏移）
transform.localScale = new Vector3(2, 2, 2);  // 缩放会放大平移后的距离
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;6. 总结&lt;/strong&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;操作顺序&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;数学意义&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;几何意义&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;缩放（S）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改变坐标轴的长度（保持原点和方向）&lt;/td&gt;
&lt;td&gt;改变物体尺寸，但不移动或旋转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;旋转（R）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改变坐标轴的方向（保持原点和缩放后的长度）&lt;/td&gt;
&lt;td&gt;改变物体方向，但不改变尺寸或位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;平移（T）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;改变原点位置（保持坐标轴的方向和缩放）&lt;/td&gt;
&lt;td&gt;移动物体到目标位置，但不改变尺寸或方向&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;遵循此顺序的原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缩放&lt;/strong&gt;确保后续旋转和移动的基准是物体的原始尺寸。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旋转&lt;/strong&gt;基于缩放后的坐标轴方向，避免方向错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平移&lt;/strong&gt;最后执行，确保物体移动到正确的位置，不受缩放或旋转的影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这种方式，可以精确控制物体的变换，避免因顺序错误导致的逻辑或视觉错误。&lt;/p&gt;
</content:encoded></item><item><title>Unity Bones Retargeting系统</title><link>https://fuwari.vercel.app/posts/unity-bones-retargeting/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity-bones-retargeting/</guid><description>Unity骨骼动画Retargeting的配置方法和注意事项</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;前置可选条件：package manager中加入了Animation Rigging（骨骼可视化Bone Renderer）和MMD4Macanim（用于把pmx转换为fbx）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在Unity中常常会遇见使用不同角色的动画，也就是Bones Retargeting系统。可以在Project中点击fbx后的Rig分支选项看到目前的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/36604e66.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Animation Type：Generic、Humanoid、None、Legacy。&lt;/p&gt;
&lt;p&gt;Generic：导入fbx的时候默认继承的选项，也就是不更改任何的骨骼名称，直接使用fbx内的所有骨骼命名；&lt;/p&gt;
&lt;p&gt;Legacy：一个比较老的标准，常见于很久以前的Builtin管线，如果不是从老项目挪过来的资源不会使用&lt;/p&gt;
&lt;p&gt;Humanoid：重点讲这个东西，这是Retarget的重点。&lt;/p&gt;
&lt;h2&gt;Humanoid：&lt;/h2&gt;
&lt;p&gt;从generic更改为Humanoid后，Unity会尝试根据内部的描述文件逐一进行描述模糊匹配（Postprocessors的描述文件）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/57cbad61.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Defination：&lt;/p&gt;
&lt;p&gt;1、 Create from this model： 直接使用当前FBX文件里的骨骼结构，自动生成一个新的Avatar（骨骼映射）。&lt;/p&gt;
&lt;p&gt;2、 Copy from other avatar：  用另一个FBX或Prefab中已经创建好的Avatar（骨骼映射）。  优势是可以直接沿用所有骨骼的名字，不需要进行映射， 动画资产更容易批量复用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/7d57bd13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SkinWeights：&lt;/p&gt;
&lt;p&gt;这是蒙皮权重的设置，&lt;strong&gt;用来控制每个顶点最多受多少根骨骼影响&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Standard是表明每个顶点最多有4个骨骼可以参与并控制进行骨骼影响。 顶点权重大于4的，只保留最重要的4个，其余自动舍弃或归零。&lt;/p&gt;
&lt;p&gt;Custom：可以选择并允许你指定“每顶点允许的骨骼数量”，比如2、4、8等（需要在Graphics Settings里自定义）。  但是一般我们只需要选择4个即可。因为大多数GPU对每个顶点可参与变形的骨骼数量是有限制的，4是最常见的上限。&lt;/p&gt;
&lt;h3&gt;这和DCC中的蒙皮有什么区别？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Unity的Skin Weights设置，决定了建模软件里“每个顶点蒙皮权重”最多有几组能被保留和使用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在Blender、Maya等3D建模/动画软件中，你可以给每个顶点分配任意数量的权重，比如1、2、4、甚至10根骨骼影响一个顶点。  一般来说， 权重越多，顶点变形越平滑，但数据量越大，性能损耗也增加。 也可以自由涂抹、调整每个骨骼对每个顶点的影响比例。&lt;/p&gt;
&lt;p&gt;Unity在导入模型时，会根据你在&lt;strong&gt;Skin Weights&lt;/strong&gt;（蒙皮权重）选项里选的最大权重数，对每个顶点&lt;strong&gt;做一次“筛选”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;选“4 Bones”，就只保留每个顶点影响最大的4个骨骼的权重，其余全部舍弃（并重新归一化）。&lt;/p&gt;
&lt;p&gt;如果你在建模软件里有顶点被5、6、8根骨骼影响，导入到Unity后，只会留下影响最大的4个，其余全部丢弃。&lt;/p&gt;
&lt;p&gt;这一步&lt;strong&gt;只发生在导入时&lt;/strong&gt;，跟你建模软件里的原始权重关系密切，但会被Unity“削减”到上限如果超过了Unity的设置则会自动只保留对该顶点权重最高的四个骨骼。&lt;/p&gt;
&lt;p&gt;选“Custom”可以设置更高（比如8），就会保留更多组权重。但是性能开销会十分美丽&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;引用：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.unity3d.com/cn/2021.3/Manual/ConfiguringtheAvatar.html#AvatarSetup&quot;&gt;导入带有人形动画的模型 - Unity 手册&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;img src=&quot;/images/posts/a746109f.png&quot; alt=&quot;&quot; /&gt;&lt;/h3&gt;
&lt;p&gt;Strip bones&lt;/p&gt;
&lt;p&gt;勾选 &lt;strong&gt;Strip Bones&lt;/strong&gt;，Unity会在导入模型时&lt;strong&gt;自动移除所有没有被蒙皮权重（Skin Weights）影响到的骨骼&lt;/strong&gt;，即那些对模型变形没有实际作用的骨骼节点不会导入进来。如果原始骨骼里有一些辅助骨、挂点骨、导出时残留的无用骨骼，但这些骨骼没有任何顶点被它们影响（权重为0），勾选这个后这些骨骼会被Unity忽略掉，减少无用数据。&lt;/p&gt;
&lt;p&gt;但是要注意的是，如果我们有一些辅助骨骼（比如说使用辅助骨骼完成武器动画绑定）的时候，就不要勾选这个，这回去掉辅助骨骼。&lt;/p&gt;
&lt;p&gt;optimize game objects（ 优化游戏对象 ）&lt;/p&gt;
&lt;p&gt;勾选后，Unity会直接&lt;strong&gt;将骨骼链优化为底层的数据结构&lt;/strong&gt;，只保留Mesh和根节点，把大部分骨骼节点从Hierarchy中“隐藏”掉，不再作为GameObject存在。这极大地&lt;strong&gt;减少了场景中的对象数量&lt;/strong&gt;，显著提升运行效率。&lt;/p&gt;
&lt;h3&gt;来自GPT对这个选项的解释：&lt;/h3&gt;
&lt;p&gt;你的疑问很正常，&lt;strong&gt;Optimize Game Objects&lt;/strong&gt; 这个选项确实容易让人迷糊。下面用&lt;strong&gt;最直白的语言&lt;/strong&gt;和&lt;strong&gt;实例对比&lt;/strong&gt;帮你彻底搞懂它的作用和意义！&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;1. &lt;strong&gt;问题本质：为什么要“优化骨骼对象”？&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在Unity里，导入带骨骼动画的模型时，每一根骨骼通常会生成一个GameObject（带Transform），比如这样一棵骨骼树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;角色
├── Hips
│   ├── Spine
│   │   ├── Chest
│   │   │   ├── Neck
│   │   │   └── Head
│   │   └── LeftShoulder
│   │       └── LeftArm
│   │           └── ...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;每一个节点（骨骼）都变成场景里的一个“游戏对象”！（可以在inspector中看到）&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;2. &lt;strong&gt;这样会有什么问题？&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;如果一个角色有50根骨骼，就有50个GameObject。&lt;/li&gt;
&lt;li&gt;一个场景有100个角色，就有5000个骨骼对象！&lt;/li&gt;
&lt;li&gt;Unity在运行时要&lt;strong&gt;每帧同步这些Transform、管理层级、分配内存&lt;/strong&gt;，有很大性能开销，特别是在移动端、VR等对性能敏感的场合。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;3. &lt;strong&gt;什么是“Optimize Game Objects”？&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;勾选了它，Unity就不再为每根骨骼生成独立GameObject&lt;/strong&gt;。&lt;/h5&gt;
&lt;h5&gt;只保留最重要的根节点和Mesh节点。&lt;/h5&gt;
&lt;h5&gt;骨骼动画的数据结构变成&lt;strong&gt;底层C++层内存里的“骨骼矩阵数组”&lt;/strong&gt;，不用Unity的GameObject系统去维护。&lt;/h5&gt;
&lt;h5&gt;&lt;strong&gt;Hierarchy视图里看不到骨骼树&lt;/strong&gt;了，只有主对象和Mesh。&lt;/h5&gt;
&lt;h5&gt;&lt;strong&gt;对比：&lt;/strong&gt;&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#### 不勾选Optimize&lt;/th&gt;
&lt;th&gt;#### 勾选Optimize&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#### 每个骨骼都是GameObject&lt;/td&gt;
&lt;td&gt;#### 只有根节点是GameObject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#### 可以脚本随时访问骨骼Transform&lt;/td&gt;
&lt;td&gt;#### 大部分骨骼无法直接访问/脚本操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#### 性能开销大&lt;/td&gt;
&lt;td&gt;#### 性能极高，内存和CPU压力小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;4. &lt;strong&gt;实际例子&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;不优化：&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;你可以写：&lt;code&gt;transform.Find(&quot;Spine/Chest/Neck/Head&quot;)&lt;/code&gt; 直接找到头部骨骼，挂武器、加特效。&lt;/li&gt;
&lt;li&gt;但每个角色都要消耗很多GameObject内存，场景多了就卡。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;strong&gt;优化后：&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;你&lt;strong&gt;没法直接脚本操作骨骼&lt;/strong&gt;（因为它们不再是GameObject）。&lt;/li&gt;
&lt;li&gt;但同样动画能正常播放，性能大大提升。&lt;/li&gt;
&lt;li&gt;适合&lt;strong&gt;大批量NPC、只看动画不用动态操作骨骼的角色&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. &lt;strong&gt;如果你还想操作骨骼怎么办？&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Unity有“暴露骨骼”(Expose Transforms)功能：&lt;br /&gt;
你可以指定“需要动态操作的那几个骨骼”在Hierarchy里保留，其余优化掉。&lt;/p&gt;
&lt;h4&gt;6. &lt;strong&gt;一句话总结&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Optimize Game Objects&lt;/strong&gt;就是让Unity不为每根骨骼生成GameObject，只在底层“默默执行动画”，这样&lt;strong&gt;能极大提升性能和效率&lt;/strong&gt;。如果你只是让角色自动播放动画，不需要挂武器、特效、换装，强烈建议开启它！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;/h3&gt;
&lt;h2&gt;准备妥当后...&lt;/h2&gt;
&lt;p&gt;便可以开始打开Config对骨骼进行Retarget。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/89be0f92.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Mapping是进行关键配置的地方，如果有些地方没有出现映射的话需要我们进行手动调整，在这个情况下，模型应该摆出Tpose以方便我们进行骨骼映射。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/8b0b7beb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最下方的两个选项：&lt;/p&gt;
&lt;p&gt;Mapping：指定映射方法。分为Clear、Automap、Load/Save。Clear则是直接从头进行分配，清空所有已经映射过的骨骼，从头进行手动分配；Automap则是之前提到过的使用Unity内置的配置文件模糊匹配；Save/Load则是用于模型的批量化处理。&lt;/p&gt;
&lt;p&gt;Pose： 主要用于&lt;strong&gt;调整当前骨骼的姿势&lt;/strong&gt;，便于正确映射和校准人形骨骼。  分为Reset、Sample pose和Enforce T-pose。&lt;/p&gt;
&lt;p&gt;Sample pose：把骨骼恢复到&lt;strong&gt;导出FBX时的原始绑定姿势&lt;/strong&gt;，通常就是建模时的A-Pose或T-Pose，或者动画师在蒙皮时设置的初始姿势。&lt;/p&gt;
&lt;p&gt;Enforce T-pose： 把骨骼恢复到&lt;strong&gt;导出FBX时的原始绑定姿势&lt;/strong&gt;，通常就是建模时的A-Pose或T-Pose，或者动画师在蒙皮时设置的初始姿势。  有时如果不是T-pose那么自动绑定会出现问题。可以选择这个选项尝试重新自动映射。&lt;/p&gt;
&lt;p&gt;Reset：重置姿势。恢复到导入姿势。&lt;/p&gt;
&lt;h2&gt;还想钳制姿势？&lt;/h2&gt;
&lt;p&gt;我们知道，即使骨骼映射一直，蒙皮权重相似，但是如果对于跨风格的动画（二次元角色动画重新映射到欧美角色上），很容易出现骨骼运动过于不协调的时候。这就需要config的第二个页面： Muscles &amp;amp; Settings  了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/636e235c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Muscle Group Preview：这里的“Muscle”指的是Unity Humanoid系统对人形骨架的各个自由度的抽象，比如手臂的上举、下放、前后摆动等。通过拖动这些滑块，你可以&lt;strong&gt;实时预览角色各个大类动作的变形效果&lt;/strong&gt;，比如张合嘴巴、左右转头、臂展、收腿等。用来&lt;strong&gt;检测骨骼分配和蒙皮权重是否合理&lt;/strong&gt;，比如看张嘴会不会带动到脸部错误部位、转头会不会扭曲等。发现异常可以回到Mapping面板修正骨骼分配，或调整权重。&lt;/p&gt;
&lt;p&gt;Per-Muscles Settings： 展开 Body、Head、Left Arm 等子项后，可以&lt;strong&gt;单独调节每一块肌肉的活动范围&lt;/strong&gt;（比如手臂能抬多高、脖子能转多远）。  这些滑块可以指定人体骨骼的极限运动值， 让Avatar更适配不同体型的模型（比如胳膊较短/较长、脖子较粗等）&lt;/p&gt;
&lt;p&gt;Additional Settings：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Upper Arm Twist / Lower Arm Twist&lt;/strong&gt;：&lt;br /&gt;
控制手臂扭转时的影响范围，防止手臂旋转时出现“爆炸”或不自然。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Upper Leg Twist / Lower Leg Twist&lt;/strong&gt;：&lt;br /&gt;
控制大腿/小腿扭转的范围与效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Arm Stretch / Leg Stretch&lt;/strong&gt;：&lt;br /&gt;
控制手臂、腿部在极限动作时的伸展弹性，防止动画重定向时出现“拉长”或变短的畸形。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feet Spacing&lt;/strong&gt;：&lt;br /&gt;
控制两脚之间的默认距离，方便站立动作的适配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Translation DoF （Translation Degree of Freedom，平移自由度）  &lt;strong&gt;：&lt;br /&gt;
决定骨骼是否允许平移自由度（一般默认关闭）。 平移自由度是&lt;/strong&gt;允许某些骨骼节点除了旋转，还可以在空间中移动（平移）&lt;/strong&gt;。  但是一般不会出现，如果连骨骼都出现了平移，那么关节会出现非常严重的脱节情况。&lt;/p&gt;
</content:encoded></item><item><title>关于blender选中物体不显示高亮描边的问题</title><link>https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8Eblender%E9%80%89%E4%B8%AD%E7%89%A9%E4%BD%93%E4%B8%8D%E6%98%BE%E7%A4%BA%E9%AB%98%E4%BA%AE%E6%8F%8F%E8%BE%B9%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8Eblender%E9%80%89%E4%B8%AD%E7%89%A9%E4%BD%93%E4%B8%8D%E6%98%BE%E7%A4%BA%E9%AB%98%E4%BA%AE%E6%8F%8F%E8%BE%B9%E7%9A%84%E9%97%AE%E9%A2%98/</guid><description>关于blender选中物体不显示高亮描边的问题的解决方案和处理方法</description><pubDate>Tue, 10 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;又水了一篇欧耶（&lt;/p&gt;
&lt;h2&gt;blender里面选中物体后不显示黄色线框，这样怎么调回去？&lt;/h2&gt;
&lt;p&gt;就是在物体模式不是编辑模式里面选择模型，右边的场景集合显示选中了，但视图界面的模型没有线框显示选中那个物体。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/afc9610f.jpeg&quot; alt=&quot;显示叠加层，孩子！&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;点选物体就是选不上，右边的场景集合也没有选中的提示&lt;/h2&gt;
&lt;h2&gt;&lt;img src=&quot;/images/posts/a99b92d1.png&quot; alt=&quot;查看物体——可见性是否勾选&quot; /&gt;&lt;/h2&gt;
</content:encoded></item><item><title>利用Stencil Buffer进行描边</title><link>https://fuwari.vercel.app/posts/%E5%88%A9%E7%94%A8stencil-buffer%E8%BF%9B%E8%A1%8C%E6%8F%8F%E8%BE%B9/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%88%A9%E7%94%A8stencil-buffer%E8%BF%9B%E8%A1%8C%E6%8F%8F%E8%BE%B9/</guid><description>利用Stencil Buffer进行描边的详细介绍和使用方法</description><pubDate>Wed, 04 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://indienova.com/indie-game-development/stencil-buffer-4/&quot;&gt;走进 Stencil Buffer 系列 4：Stencil 后处理局部描边 | indienova 独立游戏&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://indienova.com/indie-game-development/stencil-buffer-1-modle-outline-stroke/&quot;&gt;走进 Stencil Buffer 系列 1：模型轮廓描边 | indienova 独立游戏&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;利用graphics bllit函数将纹理复制到对应的texture中&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.unity.cn/cn/2019.4/ScriptReference/Graphics.Blit.html&quot;&gt;https://docs.unity.cn/cn/2019.4/ScriptReference/Graphics.Blit.html&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>操作热键记录</title><link>https://fuwari.vercel.app/posts/%E6%93%8D%E4%BD%9C%E7%83%AD%E9%94%AE%E8%AE%B0%E5%BD%95/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%93%8D%E4%BD%9C%E7%83%AD%E9%94%AE%E8%AE%B0%E5%BD%95/</guid><pubDate>Mon, 02 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Rider操作&lt;/h2&gt;
&lt;p&gt;长按ctrl+左键对准库函数——调用用法示例
ctrl+f——查找单独文件内的指定变量&lt;/p&gt;
&lt;h2&gt;Unity操作&lt;/h2&gt;
&lt;h3&gt;启动程序内的性能调试&lt;/h3&gt;
&lt;p&gt;打开调试窗口内stats——进行窗口内的draw call/batch批次处理和硬件的详细数据。用于优化游戏内光照的技术。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了更好地了解正在发生的情况，您可以启用 Stats 面板位于游戏视图的右上角。查看批次数量，以及通过批次保存的批次数量。这些代表绘制调用。仅在主灯处于活动状态时执行此操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Window-Frame debugger。帧调试器，会将选定的渲染物体进行一帧帧的渲染。可以快速了解渲染批次的物体批次排列。同时，根据zwrite深度写入的特性，渲染裁剪空间的时候最好的取样方式是对物体离摄像头从前到后的特点进行排列渲染。而从后到前的渲染方式往往会渲染过于远的物体并且添加不必要的性能开销，我们称之为Overdraw.应该尽量避免这个情况。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;启用后，帧调试器允许您单步执行每个单独的绘制调用。窗口本身显示每个绘制调用的详细信息。游戏视图将显示渲染的内容，包括选定的绘制调用。Unity 从前到后对对象进行排序，但这并不是决定绘制顺序的唯一因素。更改 GPU 状态的成本也很高，因此也应该尽量减少。这是通过将相似的对象渲染在一起来完成的。例如，Unity 更喜欢成组渲染球体和立方体，因为这样就不必经常在网格之间切换。同样，Unity 更喜欢对使用相同材质的对象进行分组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;快速同步Camera至Scence视角&lt;/h3&gt;
&lt;p&gt;在Hierarchy中选中对应的camera后，同时按下ctrl+shift+f。&lt;/p&gt;
</content:encoded></item><item><title>TBDR+Forward vs. IMR+Deferred_ 两种设备的不同权衡</title><link>https://fuwari.vercel.app/posts/tbdrforward-vs-imrdeferred_-%E4%B8%A4%E7%A7%8D%E8%AE%BE%E5%A4%87%E7%9A%84%E4%B8%8D%E5%90%8C%E6%9D%83%E8%A1%A1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/tbdrforward-vs-imrdeferred_-%E4%B8%A4%E7%A7%8D%E8%AE%BE%E5%A4%87%E7%9A%84%E4%B8%8D%E5%90%8C%E6%9D%83%E8%A1%A1/</guid><description>TBDR+Forward vs. IMR+Deferred_ 两种设备的不同权衡的原理解析和实现方法</description><pubDate>Sun, 25 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;这两套渲染方案，每一个渲染方案对应的是硬件层面（IMR、TBDR）和软件层面（Foward、Deferred），对应的是目前主流的两套方案：移动端渲染方案和电脑端渲染方案&lt;/p&gt;
&lt;h2&gt;为什么不同类型设备会采用不同方案？&lt;/h2&gt;
&lt;p&gt;这要从移动端的性能敏感说起。直接上图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/b011e42e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;相机也有一句话说的是“底大一级压死人”，这句话同样也可以应用在芯片设计中。（~~光是看着面积就知道很吓人了）~~同时，桌面端直接利用电源供电，仅算上GPU就可以获得高达375W的功率，不仅能在算力上“力大飞砖”，同时同样的功能，桌面端的指标明显要优异于移动端。更别说芯片面积受限，On-Chip Memory（L1/L2缓存）容量要比桌面端更小（的多）。好的，现在我们知道了手机的算力天生就要比电脑小得多，接下来就先从硬件结构上说明这两个硬件渲染方法：TBDR/IMR。&lt;/p&gt;
&lt;h2&gt;TBDR/IMR&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;隶属于硬件层面的渲染架构，不可以进行两者间的切换。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;TBDR： Tile-Based Deferred Rendering&lt;/h3&gt;
&lt;p&gt;TBDR大致可以分为三个模块：Binning Pass, Rendering Pass, Resolve Pass.&lt;img src=&quot;/images/posts/b83922c0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最上一层：Render Pipeline (渲染管线)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间一层：On-Chip Buffer（a.k.a. 片上内存，Tiled Frame Buffer &amp;amp; Tiled Depth Buffer）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最下一层：系统内存，CPU和GPU共享&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Binning Pass：&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：将几何数据（顶点、三角形）分配到对应的屏幕小块（Tile）中，为后续渲染做准备。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;流程步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;顶点处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顶点着色器&lt;/strong&gt;（Vertex Shader）会接收来自软件层面的顶点数据，处理顶点数据，计算顶点的屏幕坐标（投影到屏幕空间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化处理&lt;/strong&gt;：部分GPU（如Adreno）可能使用简化版顶点着色器，仅计算顶点位置，忽略纹理坐标、法线等细节（这些在分块阶段不需要）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分块分配&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将屏幕划分为固定大小的 &lt;strong&gt;Tile&lt;/strong&gt;（如16x16像素的小方块）。&lt;/li&gt;
&lt;li&gt;遍历所有三角形，确定每个三角形覆盖了哪些 Tile。&lt;/li&gt;
&lt;li&gt;将覆盖的 Tile 记录到 &lt;strong&gt;Primitive List&lt;/strong&gt;（图元列表）中，记录每个 Tile 包含哪些三角形。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据存储&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将每个 Tile 的 Primitive List 和顶点数据写入 &lt;strong&gt;系统内存(aka. LPDDR)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键点&lt;/strong&gt;：分块阶段仅记录几何信息，不进行光栅化或像素着色。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tiler 单元&lt;/strong&gt;：负责将三角形分配到对应的 Tile。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顶点着色器&lt;/strong&gt;：处理顶点坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统内存&lt;/strong&gt;：存储分块后的几何数据（Primitive List）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Rendering Pass:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：逐 Tile 处理几何数据，完成光栅化和像素着色，结果暂存到片上内存（On-Chip Memory）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;流程步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tile 遍历&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;按顺序处理每个 Tile（如从左到右、从上到下）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;几何处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重新处理顶点&lt;/strong&gt;：读取该 Tile 的 Primitive List，重新执行顶点着色器（这次会完整计算顶点属性，如纹理坐标、法线等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;裁剪与剔除&lt;/strong&gt;：进行背面剔除、视锥体剔除等，减少无效三角形。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化（****Rasterization）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将三角形转换为该 Tile 内的像素（片元）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度测试（Z-Test）&lt;/strong&gt;：利用 &lt;strong&gt;HSR（Hidden Surface Removal，隐藏面消除）&lt;/strong&gt; 或 &lt;strong&gt;LRZ（Low Resolution Z）&lt;/strong&gt; 技术，快速剔除被遮挡的片元，减少不必要的像素着色计算。（需要注意的是，虽然我们认为IMR可以“不切实际的进行高强度运算”，但是&lt;strong&gt;它同样可以采用Early-Z/Pre-Z来进行剔除并减少overdraw&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;像素着色&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;开始执行像素着色器（Fragment Shader），计算每个片元的颜色、光照等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果暂存&lt;/strong&gt;：将处理后的像素数据写入 &lt;strong&gt;片上内存&lt;/strong&gt;（Tile Buffer）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;光栅化引擎&lt;/strong&gt;：将三角形转换为像素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;像素着色器&lt;/strong&gt;：处理像素颜色和光照。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HSR/LRZ 单元&lt;/strong&gt;：隐藏面消除，减少过绘制（Overdraw）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;片上内存&lt;/strong&gt;：临时存储 Tile 的渲染结果（颜色、深度缓冲等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Resolve Pass：&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：将所有 Tile 的渲染结果合并到最终的帧缓冲区（Frame Buffer）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;流程步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;合并 Tile 数据&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将每个 Tile 的 Tile Buffer 内容（颜色、深度等）从片上内存写入系统内存的帧缓冲区Framebuffer。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多采样抗锯齿（MSAA）&lt;/strong&gt;：如果启用了 MSAA，此时会直接将多个样本点合并为最终像素颜色。这里由于Tile的像素数据直接按块存在于片上内存，极大提高了MSAA的缓存命中率（直接采样点即可而不需要从片外内存读取Framebuffer）。这也是为什么MSAA在低性能移动端的性能损耗会小于TAA。但是，MSAA的每个像素存储多个样本点（如4x MSAA占用4倍显存），在移动端设备中，显存容量有限（如低端设备仅1-2GB），可能导致显存不足或CPU和GPU间的左右脑互搏，带宽竞争。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清理与提交&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;清除片上内存中的临时数据（如深度缓冲）。&lt;/li&gt;
&lt;li&gt;将完成的帧提交给显示屏。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROP（光栅操作单元）&lt;/strong&gt;：合并 Tile 数据并执行最终的写入操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统内存DDR&lt;/strong&gt;：存储最终的帧缓冲区。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;IMR: Immediate Mode Rendering&lt;/h3&gt;
&lt;p&gt;IMR的渲染流程分为以下&lt;strong&gt;4个核心阶段：Vertex Process, Rasterizer,  Pixel Shading, Merge.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c66840cd.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Vertex Process&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：将顶点数据转换为屏幕空间坐标，并执行顶点着色器计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顶点缓冲区（Vertex Buffer）&lt;/strong&gt;：存储顶点的原始数据（坐标、法线、纹理坐标等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顶点着色器（Vertex Shader, VS）&lt;/strong&gt;：GPU的可编程单元，负责计算顶点的最终位置、颜色、纹理坐标等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;固定功能单元（Fixed-Function Units）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顶点属性组装（Vertex Assembly）&lt;/strong&gt;：将顶点数据从内存加载到GPU内部缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;投影变换（Projection Transform）&lt;/strong&gt;：将顶点坐标从模型空间转换为屏幕空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存（L1/L2 Cache）&lt;/strong&gt;：减少顶点数据从主存重复读取的延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;顶点数据从系统内存加载到GPU的顶点缓冲区。&lt;/li&gt;
&lt;li&gt;顶点着色器对每个顶点执行计算（如光照、动画变形）。&lt;/li&gt;
&lt;li&gt;处理后的顶点数据通过固定功能单元完成投影变换，得到屏幕坐标。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;Rasterization&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：将三角形转换为像素（片元），并进行深度测试（Early-Z）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;光栅化单元（Rasterizer）&lt;/strong&gt;：负责将三角形分解为像素级别的片元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度缓冲区（Depth Buffer）&lt;/strong&gt;：存储每个像素的深度值，用于隐藏面消除（Z-Test）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;裁剪单元（Clipping Unit）&lt;/strong&gt;：剔除超出视口的三角形。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;光栅化单元将三角形的顶点坐标扩展为覆盖的像素区域。&lt;/li&gt;
&lt;li&gt;每个像素生成一个&lt;strong&gt;片元（Fragment）&lt;/strong&gt;，并计算其深度值。&lt;/li&gt;
&lt;li&gt;片元通过Early-Z测试：若深度值小于Depth Buffer中的值，则保留；否则丢弃。&lt;/li&gt;
&lt;li&gt;未被丢弃的片元传递给像素着色器处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Pixel Shadeing&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：计算每个片元的最终颜色值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;像素着色器（Pixel Shader, PS aka. Fragment Shader, FS）&lt;/strong&gt;：对通过Early-Z的片元执行光照、纹理采样等计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纹理单元（Texture Units）&lt;/strong&gt;：从纹理内存加载纹理数据（如法线贴图、颜色贴图）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;固定功能插值单元&lt;/strong&gt;：对顶点属性进行插值（如颜色、纹理坐标）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;片元通过插值单元获取顶点属性的插值结果（如像素颜色、纹理坐标）。&lt;/li&gt;
&lt;li&gt;像素着色器根据插值后的数据、纹理采样结果及光照模型计算最终颜色。&lt;/li&gt;
&lt;li&gt;若启用了Alpha测试（Alpha Test），部分片元可能被丢弃。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Merger&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：将片元颜色写入Frame Buffer，并执行混合（Blending）和深度更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参与组件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;混合单元（Blending Unit）&lt;/strong&gt;：根据混合方程（如透明度）合并新颜色与已有颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度Stencil单元&lt;/strong&gt;：执行Late-Z测试和Stencil Buffer操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frame Buffer（颜色缓冲区）&lt;/strong&gt;：存储最终像素颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Depth Buffer&lt;/strong&gt;：存储深度值，用于后续帧的深度测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过像素着色的片元进入Late-Z测试（若未通过Early-Z）。&lt;/li&gt;
&lt;li&gt;混合单元将新颜色与Frame Buffer中的颜色混合（如透明物体叠加）。&lt;/li&gt;
&lt;li&gt;更新Frame Buffer和Depth Buffer，完成像素输出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;电脑端参与的组件：&lt;/h4&gt;
&lt;h5&gt;硬件组件：&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GPU核心模块&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算单元（Shader Cores）&lt;/strong&gt;：执行顶点着色器、像素着色器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化引擎&lt;/strong&gt;：处理光栅化和深度测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存控制器&lt;/strong&gt;：管理与系统内存的交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存结构（片上内存）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;L1/L2 Cache&lt;/strong&gt;：缓存顶点数据和中间结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Texture Cache&lt;/strong&gt;：加速纹理采样。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显存（VRAM）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;存储Frame Buffer、Depth Buffer、纹理等数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;软件组件：&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;图形API&lt;/strong&gt;（如DirectX、OpenGL）：
&lt;ul&gt;
&lt;li&gt;管理顶点缓冲区、渲染状态（如混合模式、深度测试）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动程序&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将API命令转换为GPU可执行的指令流。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;着色器代码&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;顶点着色器、像素着色器的可编程逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Forward搭配TBDR，而Deferred则会搭配IMR？&lt;/h3&gt;
&lt;p&gt;这就要说到缺点了。虽然Forward在目标实现接近于IMR（两者的目的都是需要及时渲染），按理来说应该是Forward+IMR。我们来简要过一下两个渲染管线：&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Forward管线流程&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;顶点处理&lt;/strong&gt;：顶点着色器计算顶点坐标、法线等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化&lt;/strong&gt;：生成片元并进行Early-Z测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;片段处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ForwardBase Pass&lt;/strong&gt;：计算主光源（如Directional Light）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ForwardAdd Pass&lt;/strong&gt;：为每个附加光源（点光源、聚光灯）单独渲染，叠加光照。&lt;/li&gt;
&lt;li&gt;需要注意的是，在URP14中，ForwardAdd Pass被取消，为了简洁化，可以直接调用GetAdditionalLightsCount()。但是底层实现逻辑仍然一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;LightAdd in URP14：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#ifdef _ADDITIONAL_LIGHTS
                int additionalLightsCount = GetAdditionalLightsCount();
                for (int i = 0; i &amp;lt; additionalLightsCount; ++i)
                {
                    Light light = GetAdditionalLight(i, input.positionWS);
                    
                    // 简单的Lambert漫反射
                    half3 additionalLambert = CalculateLambert(N, light.direction, light.color, light.distanceAttenuation * light.shadowAttenuation);
                    
                    additionalLightsColor += baseColor.rgb * additionalLambert * _AdditionalLightsIntensity;
                }
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;混合与输出&lt;/strong&gt;：将结果写入Frame Buffer。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;Deferred管线流程&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;顶点处理&lt;/strong&gt;：仅计算顶点坐标，不处理光照。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化&lt;/strong&gt;：开始采集并生成G-Buffer（存储几何数据：位置、法线、颜色、材质属性等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光照Pass&lt;/strong&gt;：遍历屏幕像素，根据G-Buffer数据计算所有光源对像素的影响。在这一阶段， 会在屏幕上逐像素执行光照，**不再访问场景几何 **。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合与输出&lt;/strong&gt;：将结果写入Frame Buffer。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/f9b644cf.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现了没有？Deferred管线这里多了一个Gbuffer。这个Gbuffer是什么呢，这是一个会储存在**片外内存（对应桌面端的VRAM，移动端的LPDDR）**的Buffer。&lt;/p&gt;
&lt;p&gt;延迟渲染需要将几何属性（如位置、法线、材质参数、颜色等）存储到 Gbuffer的多张渲染目标（Render Targets, RT） 中，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Position RT：存储像素的三维位置。&lt;/li&gt;
&lt;li&gt;Normal RT：存储法线向量。&lt;/li&gt;
&lt;li&gt;Albedo RT：存储基础颜色。&lt;/li&gt;
&lt;li&gt;Depth RT：存储深度信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然deferred在多光源计算的性能消耗明显优于forward管线，那么代价是什么呢？代价是deferred虽然可以一次性解决屏幕的所有物体渲染，不需要逐物体渲染；但是这个条件的前提是&lt;strong&gt;你的Gbuffer可以迅速地从系统内存读取到计算单元中并作计算&lt;/strong&gt;。更别说你还没有开启MSAA（4xMSAA=从Gbuffer采样四次，带宽要求直接x4），TAA（从Framebuffer的时域采样，直接*n帧带宽）。&lt;/p&gt;
&lt;p&gt;另外，由于Gbuffer不会存储透明物体数据，因为Deferred渲染的核心是&lt;strong&gt;几何处理阶段&lt;/strong&gt;（Geometry Pass），将所有不透明物体的几何信息（如位置、法线、材质属性、深度）写入&lt;strong&gt;G-Buffer&lt;/strong&gt;（多个渲染目标）。&lt;/p&gt;
&lt;p&gt;也就是说，如果还要加一个透明处理，就意味着：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以用加一个Forward管线单独处理，但是这会脱离Gbuffer，导致Deferred的屏幕空间效果不起作用&lt;/li&gt;
&lt;li&gt;单独开一个Gbuffer接着渲染。再次从片外内存读写一遍。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;对比复杂度&lt;/h3&gt;
&lt;p&gt;最后对比一下算法（由通义生成）：在分析延迟渲染（Gbuffer + IMR）与前向渲染（+ TBDR）的带宽压力差异时，需要从&lt;strong&gt;渲染流程、数据存储方式、架构特性&lt;/strong&gt; 三个维度进行对比。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;1. 延迟渲染（Gbuffer + IMR）的带宽压力来源&lt;/h4&gt;
&lt;h5&gt;(1) Gbuffer的存储与传输&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gbuffer的定义&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;延迟渲染需要将几何属性（如位置、法线、材质参数、颜色等）存储到 &lt;strong&gt;多张渲染目标（Render Targets, RT）&lt;/strong&gt; 中，例如：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Position RT&lt;/strong&gt;：存储像素的三维位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Normal RT&lt;/strong&gt;：存储法线向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Albedo RT&lt;/strong&gt;：存储基础颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Depth RT&lt;/strong&gt;：存储深度信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;带宽消耗点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始化阶段&lt;/strong&gt;：渲染所有几何体时，必须将所有可见像素的几何属性写入Gbuffer，这需要 &lt;strong&gt;多次系统内存写入&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光照阶段&lt;/strong&gt;：每个光源的计算需要读取Gbuffer中的所有RT数据（如法线、位置、材质），导致 &lt;strong&gt;多次系统内存读取&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;屏幕分辨率越高&lt;/strong&gt;，Gbuffer的带宽需求呈线性增长（如1080p屏幕需要约200万像素 × 多个RT通道）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(2) IMR架构的劣势&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接写入系统内存&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在IMR架构下，Gbuffer的写入和读取均直接通过 &lt;strong&gt;系统内存（VRAM）&lt;/strong&gt; 进行，无法利用片上内存暂存中间结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频繁的读写操作&lt;/strong&gt;：例如，每个光源的计算需读取所有RT数据，带宽压力随光源数量线性增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抗锯齿（MSAA）的额外开销&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;若启用MSAA，Gbuffer的每个像素需存储多个样本点的数据（如4x MSAA需4倍存储空间），进一步增加带宽需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于读写速度的刚需：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Gbuffer的显存带宽需求在移动端是致命的（如1080p屏幕下，四张Gbuffer的带宽消耗可达 &lt;strong&gt;2GB/s&lt;/strong&gt;，而移动端总带宽仅30GB/s）。&lt;/li&gt;
&lt;li&gt;延迟渲染需要 &lt;strong&gt;多张RT&lt;/strong&gt;，导致显存带宽成为瓶颈。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 前向渲染（+ TBDR）的带宽优化&lt;/h4&gt;
&lt;h5&gt;(1) TBDR架构的优势&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分块处理与片上内存暂存&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分块（Tiling）&lt;/strong&gt;：将屏幕分割为小Tile，每个Tile的渲染数据（颜色、深度、模板）暂存在 &lt;strong&gt;片上内存（On-Chip Memory）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间结果暂存&lt;/strong&gt;：仅在 &lt;strong&gt;解析阶段（Resolve）&lt;/strong&gt; 将Tile结果合并到系统内存的Framebuffer，减少频繁写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐藏面消除（HSR/LRZ）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;提前剔除被遮挡的像素，仅对 &lt;strong&gt;可见像素&lt;/strong&gt; 进行纹理采样和光照计算，减少无效带宽消耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纹理读取的局部性优化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;同一Tile内的像素共享纹理数据，片上缓存（如L2缓存）可高效复用，减少系统内存访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(2) 前向渲染的流程优化&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逐Tile处理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;每个Tile的渲染在片上内存完成，仅需读取 &lt;strong&gt;当前Tile相关的纹理和几何数据&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无需存储Gbuffer&lt;/strong&gt;：颜色和深度数据直接暂存于Tile Buffer，最终合并到Framebuffer。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过绘制（Overdraw）的减少&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;HSR/LRZ技术剔除被遮挡像素，减少纹理和着色计算的带宽需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(3) 知识库依据&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;知识库[2][5][7]&lt;/strong&gt; 强调：
&lt;ul&gt;
&lt;li&gt;TBDR通过 &lt;strong&gt;片上内存&lt;/strong&gt; 存储中间数据，仅最终合并时写入系统内存，显著降低带宽。&lt;/li&gt;
&lt;li&gt;HSR/LRZ技术可将Overdraw降至接近1，减少无效像素的纹理读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;3. 带宽压力对比：延迟渲染 vs 前向渲染&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;维度&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;延迟渲染（Gbuffer + IMR）&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;前向渲染（+ TBDR）&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gbuffer存储需求&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;需存储多张RT（如Position、Normal、Albedo等），带宽随RT数量线性增长。&lt;/td&gt;
&lt;td&gt;无需Gbuffer，仅存储颜色和深度数据，带宽需求更低。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;系统内存访问频率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每个Pass需读取所有RT数据（如每个光源需读取Gbuffer），导致 &lt;strong&gt;高频系统内存访问&lt;/strong&gt;。&lt;/td&gt;
&lt;td&gt;仅最终合并时写入系统内存，中间数据在片上内存处理， &lt;strong&gt;系统内存访问频率低&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;纹理读取优化&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;纹理数据需多次读取（如每个光源读取Gbuffer），无片上缓存优化。&lt;/td&gt;
&lt;td&gt;纹理数据通过片上缓存复用，且仅处理可见像素， &lt;strong&gt;带宽消耗显著降低&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;抗锯齿开销&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MSAA需存储多倍样本数据，Gbuffer带宽压力进一步增加。&lt;/td&gt;
&lt;td&gt;TBDR的 &lt;strong&gt;片上MSAA&lt;/strong&gt; 可暂存多样本数据，减少系统内存带宽需求。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;移动设备适用性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;移动端显存带宽有限，Gbuffer的高带宽需求导致性能瓶颈（如知识库[4]提到的2GB/s vs 30GB/s总带宽）。&lt;/td&gt;
&lt;td&gt;TBDR的优化使其在移动端成为主流，前向渲染与TBDR协同降低带宽压力。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h4&gt;4. 具体场景示例&lt;/h4&gt;
&lt;h5&gt;场景：1080p屏幕，4光源&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟渲染（Gbuffer + IMR）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gbuffer带宽&lt;/strong&gt;：假设每像素存储4个通道（如Position、Normal、Albedo、Depth），8bit/通道：
&lt;ul&gt;
&lt;li&gt;单帧带宽 = 1920×1080 × 4通道 × 8bit × 60fps ≈ &lt;strong&gt;2.5GB/s&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光照阶段带宽&lt;/strong&gt;：每个光源需读取Gbuffer数据，4光源总带宽 ≈ &lt;strong&gt;10GB/s&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前向渲染（+ TBDR）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纹理读取&lt;/strong&gt;：仅对可见像素进行光照计算，假设Overdraw减少为1，带宽 ≈ 1920×1080 × 4光源 × 0.2（缓存命中） ≈ &lt;strong&gt;0.8GB/s&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统内存写入&lt;/strong&gt;：仅最终合并时写入Framebuffer，带宽 ≈ &lt;strong&gt;0.5GB/s&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;5. 总结：带宽压力差异的核心原因&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟渲染（Gbuffer + IMR）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;带宽杀手&lt;/strong&gt;：多张RT的存储与高频读写、无片上内存优化导致带宽需求激增。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端不友好&lt;/strong&gt;：显存带宽有限，Gbuffer的高开销易触发带宽瓶颈。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前向渲染（+ TBDR）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;带宽优化&lt;/strong&gt;：利用片上内存暂存数据、HSR剔除无效像素、纹理缓存复用，显著降低系统内存访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端首选&lt;/strong&gt;：与TBDR架构协同，平衡性能与功耗，适合移动设备的典型场景（少光源、透明物体）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;类比理解&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟渲染（Gbuffer + IMR）&lt;/strong&gt;：像快递公司每次配送都要先建一个庞大的中央仓库（Gbuffer），再多次往返取货送货，导致交通拥堵（带宽不足）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前向渲染（+ TBDR）&lt;/strong&gt;：快递员分片区配送（分块），每个片区的货物暂存在本地仓库（片上内存），仅最终合并到中央仓库，减少重复运输。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>xyzw中的w：齐次坐标系的作用</title><link>https://fuwari.vercel.app/posts/the-role-of-w-in-xyzw-homogeneous-coordinates/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/the-role-of-w-in-xyzw-homogeneous-coordinates/</guid><description>xyzw中的w：齐次坐标系的作用的技术原理和应用</description><pubDate>Fri, 23 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Unity 渲染管线里，顶点着色器（vertex）要把模型顶点转换到裁剪空间（clip space）里，然后在屏幕映射步骤顶点才会从裁剪空间映射到电脑屏幕上。&lt;/p&gt;
&lt;p&gt;当然，可以注意到，对于摄像机控件，经常会看到“正交模式”和“透视模式”。这两者的视锥体是不同的，正交的视锥体是正方体，透视的视锥体是金字塔形状的锥体，两者在inspector面板中的参数是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c2d09b85.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/3f041256.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这两个模式的裁剪空间都是由6个面组成的封闭立方体组成，此时还没有变换到NDC坐标。超过这个立方体坐标都不再会被渲染。透视模式的两条平行直线可以相交于一点，但是在传统的笛卡尔xyz坐标系中确实永远不可能实现的，这就是xyzw的第四个分量w的功劳！&lt;/p&gt;
&lt;p&gt;在透视摄像机的画面（透视空间）里使用的并不是笛卡尔坐标系，为了描述透视空间，科学家提出了 “ 齐次坐标 ” 的概念：&lt;/p&gt;
&lt;p&gt;即，用 N+1 个数来表示 N 维空间中的点或向量，对于三维空间中的点，通常是使用 (x, y, z, w) 来表示三维空间中的点在齐次坐标空间中的位置。三维空间（笛卡尔坐标系）和齐次坐标系之间可以通过齐次除法进行相互转换，科学家定义的规则是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/8783dc26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;齐次坐标系的一个功能便是能描述透视空间下的renti对于上面的平行线相交于一点，这个点的齐次坐标便是（x，y，z，0）。因为这个w值转换为一般的三维坐标便是无穷大，但是在齐次坐标系中它又是一个确定的值——这是在一般的笛卡尔坐标系中做不到的。此外，太阳光用的也是这个思路：w设置为0，xyz仅用于表达方向，这就等价于平行光矢量。&lt;/p&gt;
&lt;p&gt;第二个功能就是能够描述坐标的位移；对于标准坐标系中，平移、旋转、缩放都需要不同的计算方式，尤其是位移变换，需要向量的加法；但是加法在性能消耗上是大于矩阵乘法的。而齐次坐标系，&lt;strong&gt;所有的仿射变换（包括平移、旋转、缩放等）都可以用一个单一的矩阵乘法来表示&lt;/strong&gt;。这意味着复杂的变换序列可以被组合成单个矩阵，简化了变换流程。在视图空间进行设置时（点未作任何矩阵投影变换），&lt;/p&gt;
&lt;p&gt;w的值可以区分点和向量：1为点，0为向量。因为向量不需要平移。&lt;/p&gt;
&lt;p&gt;裁剪空间（clipspace）中的顶点是用齐次坐标表示的，在屏幕映射阶段里要对裁剪空间的顶点进行一个统一的齐次除法操作，来把顶点从齐次坐标系转换到笛卡尔坐标系的归一化设备坐标（Normalized Device Coordinates, NDC）空间里，经过这一步之后，裁剪空间将会变换到一个立方体内。&lt;/p&gt;
&lt;p&gt;OpenGL 和 DirectX 对 NDC 空间的定义有所不同，前者定义 NDC 空间的 xyz 取值范围是 [-1, 1] ，而后者定义 NDC 空间的 xy 取值范围为 [-1, 1]，z 的取值范围为 [0, 1]。而 Unity 选择了 OpenGL 的规范：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/030f1ecb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要进行齐次除法就必须要获得正确的 w 分量，&lt;strong&gt;投影矩阵之前的 w(0和1) 分量并没有齐次除法的用途&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以，投影矩阵的&lt;strong&gt;一个作用&lt;/strong&gt;就是：&lt;strong&gt;正确计算出从视图空间到透视空间中的顶点的 w 分量&lt;/strong&gt;，以便在之后的屏幕映射步骤中进行齐次除法操作，当一个顶点通过透视投影矩阵变换后，其位置会变为齐次坐标形式(x′,y′,z′,w′)。&lt;strong&gt;注意，在裁剪空间这里的w′值并不是视图空间固定的1/0，而是取决于原始顶点的深度（即它离相机的距离）&lt;/strong&gt;。具体来说，对于一个位于视图空间中的点(x,y,z)，&lt;strong&gt;经过透视投影矩阵变换后得到的新w&apos;通常是基于该裁剪空间点的z值（深度）计算出来的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在裁剪空间中，完成所有必要的裁剪操作之后，为了将这些坐标映射到标准化设备坐标(NDC, Normalized Device Coordinates)空间，需要将齐次坐标除以w′（即进行透视除法：x′′=x′/w′,y′′=y′/w′,z′′=z′/w′）。这一过程使得远处的物体看起来更小，近处的物体看起来更大，从而实现了透视的效果。经过投影矩阵处理之后的顶点，将会给他的 w 分量赋予重要的含义。&lt;/p&gt;
&lt;p&gt;投影矩阵的&lt;strong&gt;另一个作用&lt;/strong&gt;是：对顶点的位置进行一定的缩放，使得顶点转换到 NDC 空间的范围中，&lt;/p&gt;
&lt;p&gt;因为用视锥体的六个面来判断顶点是否在视锥体里，非常麻烦，我们希望用一个矩阵直接转换顶点进我们定义的视锥体里，转换之后，如果 x, y, z 符合条件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/d1103960.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
那么就会参与最终的渲染，全片完。&lt;/p&gt;
&lt;p&gt;顺带一提，对于正交摄像机的话，他的裁剪空间是一个长方体，拍摄出来的画面是可以用三维坐标来描述的，不需要齐次坐标来描述，所以正交摄像机的投影矩阵对 w 分量没有进行操作（或者理解为正交摄像机的裁剪空间是 w 恒等于 1 的齐次坐标空间）。&lt;/p&gt;
</content:encoded></item><item><title>Houdini中的Enforce Unique Name Attribute per instance</title><link>https://fuwari.vercel.app/posts/houdini%E4%B8%AD%E7%9A%84enforce-unique-name-attribute-per-instance/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/houdini%E4%B8%AD%E7%9A%84enforce-unique-name-attribute-per-instance/</guid><description>Houdini中的Enforce Unique Name Attribute per instance的解决方案和处理方法</description><pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;__在Houdini中，&lt;code&gt;Copy to Points&lt;/code&gt; (复制到点) 等节点在进行几何体实例化时，有一个非常重要的选项叫做 &lt;strong&gt;&quot;Enforce Unique Name Attribute per instance&quot;&lt;/strong&gt;（为每个实例强制生成唯一名称属性）。&lt;/p&gt;
&lt;h3&gt;作用是什么？&lt;/h3&gt;
&lt;p&gt;当你在Houdini中将一个几何体（例如一棵树、一块石头）复制到多个点上以创建大量实例时，这些实例在内部通常被视为“打包原语”（Packed Primitives）。默认情况下，这些打包原语可能没有一个唯一的标识符来区分彼此。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Enforce Unique Name Attribute per instance&quot; 的作用是：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它会在每个生成的打包原语（即每个实例）上创建一个&lt;strong&gt;唯一的字符串属性&lt;/strong&gt;，通常这个属性的名称是 &lt;code&gt;name&lt;/code&gt;。这个 &lt;code&gt;name&lt;/code&gt; 属性的值会是类似 &lt;code&gt;geo_0&lt;/code&gt;、&lt;code&gt;geo_1&lt;/code&gt;、&lt;code&gt;geo_2&lt;/code&gt; 这样的唯一标识符。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么这很重要？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个唯一的名称属性对于许多下游工作流至关重要，特别是当你需要对每个实例进行单独的控制、识别或在渲染器、游戏引擎中进行特殊处理时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;材质覆盖 (Material Overrides)：&lt;/strong&gt; 允许你在渲染时为特定的实例应用不同的材质，即使它们共享相同的原始几何体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;属性变化 (Attribute Variations)：&lt;/strong&gt; 即使实例共享相同的源几何体，你也可以通过这个唯一的 &lt;code&gt;name&lt;/code&gt; 属性来引用它们，并为每个实例设置独特的属性（如颜色、缩放、旋转偏移等），而无需打破实例化的效率优势。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择和调试 (Selection &amp;amp; Debugging)：&lt;/strong&gt; 在复杂的场景中，你可以通过这个唯一的名称来选择或识别特定的实例，这对于调试和精确控制非常有用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导出到其他软件/游戏引擎：&lt;/strong&gt; 许多外部渲染器（如Redshift, Arnold, V-Ray, Karma）和游戏引擎（如Unity, Unreal Engine）在处理实例时，会查找这种唯一的名称属性，以便进行更精细的控制或优化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;举例说明&lt;/h3&gt;
&lt;p&gt;假设你正在创建一个森林场景，需要复制大量的树木。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建源几何体：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Box&lt;/code&gt; SOP，作为你的“树”的简化模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建散布点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Grid&lt;/code&gt; SOP。&lt;/li&gt;
&lt;li&gt;连接一个 &lt;code&gt;Scatter&lt;/code&gt; SOP 到 &lt;code&gt;Grid&lt;/code&gt;，生成一些点作为树木的放置位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复制到点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Copy to Points&lt;/code&gt; SOP。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;Box&lt;/code&gt; 连接到 &lt;code&gt;Copy to Points&lt;/code&gt; 的第一个输入（要复制的几何体）。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;Scatter&lt;/code&gt; 的输出连接到 &lt;code&gt;Copy to Points&lt;/code&gt; 的第二个输入（点）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用唯一名称属性：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;选择 &lt;code&gt;Copy to Points&lt;/code&gt; 节点。&lt;/li&gt;
&lt;li&gt;在参数面板中，找到 &lt;strong&gt;&quot;Packed Primitives&quot;&lt;/strong&gt; 标签页。&lt;/li&gt;
&lt;li&gt;勾选 &lt;strong&gt;&quot;Enforce Unique Name Attribute per instance&quot;&lt;/strong&gt; 选项。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;观察结果：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现在，如果你在 &lt;code&gt;Copy to Points&lt;/code&gt; 节点之后连接一个 &lt;code&gt;Null&lt;/code&gt; 节点，并打开几何体电子表格（Geometry Spreadsheet），将显示类型切换为 &lt;code&gt;Primitives&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;你会发现每个打包原语（&lt;code&gt;packedfragment&lt;/code&gt;）上都多了一个名为 &lt;code&gt;name&lt;/code&gt; 的字符串属性，其值是唯一的，例如 &lt;code&gt;box_0&lt;/code&gt;、&lt;code&gt;box_1&lt;/code&gt;、&lt;code&gt;box_2&lt;/code&gt; 等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;应用场景（材质覆盖）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现在，你有了这些带有唯一名称的树木实例，你可以利用它们来做一些事情。例如，在Karma渲染器中，你可以通过这个 &lt;code&gt;name&lt;/code&gt; 属性来覆盖特定树的材质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在你的场景中，为 &lt;code&gt;Box&lt;/code&gt; 创建一个默认材质（例如，绿色）。&lt;/li&gt;
&lt;li&gt;假设你想让 &lt;code&gt;box_5&lt;/code&gt; 这个实例变成红色。&lt;/li&gt;
&lt;li&gt;在Karma的材质覆盖系统（或者其他渲染器的类似机制）中，你可以指定一个规则：当实例的 &lt;code&gt;name&lt;/code&gt; 属性是 &lt;code&gt;box_5&lt;/code&gt; 时，将其材质替换为红色材质。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;如果没有这个选项会怎样？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你不勾选 &quot;Enforce Unique Name Attribute per instance&quot;，那么所有的打包原语可能都没有这个唯一的 &lt;code&gt;name&lt;/code&gt; 属性。这意味着渲染器或下游工具将无法轻易地识别和区分每个单独的实例，你就无法对它们进行精细的、基于实例的材质覆盖或属性修改，除非你打破实例（这将大大增加内存占用和文件大小）。&lt;/p&gt;
&lt;p&gt;总之，&quot;Enforce Unique Name Attribute per instance&quot; 提供了一种高效且灵活的方式，来为大量实例提供唯一的标识符，从而实现更高级的控制和定制化。&lt;/p&gt;
&lt;p&gt;它更常出现在&lt;strong&gt;创建实例的节点&lt;/strong&gt;上，最典型的就是 &lt;code&gt;Copy to Points&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然而，Pyro模拟确实可以与实例化的概念结合起来，尤其是在以下两种常见场景中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;实例化Pyro的发射源 (Instancing Pyro Emitters):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;你可能创建了多个独立的几何体作为Pyro的发射源（比如，多个火把、多个爆炸点）。&lt;/li&gt;
&lt;li&gt;这些几何体本身可能是通过 &lt;code&gt;Copy to Points&lt;/code&gt; 节点实例化出来的，而在这个 &lt;code&gt;Copy to Points&lt;/code&gt; 节点上，你就会勾选“Enforce Unique Name Attribute per instance”来为每个发射源提供唯一的ID。&lt;/li&gt;
&lt;li&gt;然后，这些带有唯一ID的实例化的几何体被送入 &lt;code&gt;Pyro Source&lt;/code&gt; 节点，作为烟雾或火焰的发射区域。&lt;/li&gt;
&lt;li&gt;在这种情况下，这个选项是在Pyro模拟的&lt;strong&gt;上游&lt;/strong&gt;使用的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实例化烘焙好的Pyro模拟结果 (Instancing Baked Pyro Results):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这是更常见，也更容易让你联想到“Pyro中出现实例选项”的场景。&lt;/li&gt;
&lt;li&gt;你可能模拟了一个小型的、通用的火焰或烟雾效果（例如，一个小型爆炸、一团烟雾）。&lt;/li&gt;
&lt;li&gt;为了在场景中重复使用这个效果，你会将这个模拟结果烘焙成一个静态的体积（Volume），然后将这个体积转换为一个“打包原语”（Packed Primitive）。&lt;/li&gt;
&lt;li&gt;接着，你会使用 &lt;code&gt;Copy to Points&lt;/code&gt; 节点将这个打包好的Pyro效果复制到场景中的多个位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在这个 &lt;strong&gt;&lt;code&gt;Copy to Points&lt;/code&gt;&lt;/strong&gt; 节点上，你就会找到并使用“Enforce Unique Name Attribute per instance”选项。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Pyro中“Enforce Unique Name Attribute per instance”的例子&lt;/h3&gt;
&lt;p&gt;我们以第二种场景为例：&lt;strong&gt;复制多个烘焙好的小型爆炸效果。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 在一个场景中放置多个相似但独立的爆炸效果，并能够对每个爆炸进行单独的材质或属性调整。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作流示例：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建单个Pyro爆炸模拟：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Sphere&lt;/code&gt; SOP (作为爆炸源)。&lt;/li&gt;
&lt;li&gt;连接一个 &lt;code&gt;Pyro Source&lt;/code&gt; SOP 到 &lt;code&gt;Sphere&lt;/code&gt;，将其转换为烟雾和火焰的体积属性。&lt;/li&gt;
&lt;li&gt;连接一个 &lt;code&gt;Pyro Solver&lt;/code&gt; SOP 到 &lt;code&gt;Pyro Source&lt;/code&gt;，进行爆炸模拟。调整参数以获得一个满意的单次爆炸效果。&lt;/li&gt;
&lt;li&gt;为了性能，通常会连接一个 &lt;code&gt;Pyro Bake Volume&lt;/code&gt; SOP 到 &lt;code&gt;Pyro Solver&lt;/code&gt;，将模拟结果烘焙成静态体积。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;将Pyro结果打包成实例：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;Pyro Bake Volume&lt;/code&gt; 之后，连接一个 &lt;code&gt;Convert VDB&lt;/code&gt; SOP。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;Convert VDB&lt;/code&gt; 节点上，将 &quot;Convert To&quot; 设置为 &lt;code&gt;Polygons&lt;/code&gt; (或者其他你希望打包的几何体类型，但对于体积，通常是转换为VDB，然后直接打包VDB)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键步骤：&lt;/strong&gt; 连接一个 &lt;code&gt;Assemble&lt;/code&gt; SOP 到 &lt;code&gt;Convert VDB&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;Assemble&lt;/code&gt; 节点上，勾选 &lt;strong&gt;&quot;Create Name Attribute&quot;&lt;/strong&gt; (通常会默认勾选)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最重要的是，勾选 &quot;Create Packed Geometry&quot;&lt;/strong&gt;。这将把你的烘焙好的Pyro体积转换为一个可以被实例化的打包原语。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建散布点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Grid&lt;/code&gt; SOP。&lt;/li&gt;
&lt;li&gt;连接一个 &lt;code&gt;Scatter&lt;/code&gt; SOP 到 &lt;code&gt;Grid&lt;/code&gt;，生成你希望放置爆炸效果的点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复制Pyro实例到点：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放置一个 &lt;code&gt;Copy to Points&lt;/code&gt; SOP。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;Assemble&lt;/code&gt; 节点的输出（你的打包Pyro爆炸）连接到 &lt;code&gt;Copy to Points&lt;/code&gt; 的第一个输入。&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;Scatter&lt;/code&gt; 节点的输出（散布点）连接到 &lt;code&gt;Copy to Points&lt;/code&gt; 的第二个输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启用“Enforce Unique Name Attribute per instance”：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;选择 &lt;code&gt;Copy to Points&lt;/code&gt; 节点。&lt;/li&gt;
&lt;li&gt;在参数面板中，导航到 &lt;strong&gt;&quot;Packed Primitives&quot;&lt;/strong&gt; 标签页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;勾选 &quot;Enforce Unique Name Attribute per instance&quot;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结果和用途：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现在，你的场景中会有多个独立的爆炸效果实例。每个实例都带有一个唯一的 &lt;code&gt;name&lt;/code&gt; 属性（例如 &lt;code&gt;explosion_0&lt;/code&gt;, &lt;code&gt;explosion_1&lt;/code&gt; 等）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;渲染器中的材质变化：&lt;/strong&gt; 你可以利用这个唯一的 &lt;code&gt;name&lt;/code&gt; 属性，在渲染器（如Karma, Redshift, Arnold）中为特定的爆炸实例应用不同的颜色、亮度或任何其他材质属性覆盖。例如，让 &lt;code&gt;explosion_3&lt;/code&gt; 看起来更暗，或者 &lt;code&gt;explosion_7&lt;/code&gt; 带有蓝色的火焰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;游戏引擎中的控制：&lt;/strong&gt; 如果你将这些带有唯一名称的实例导出到游戏引擎，引擎可以识别这些唯一的ID，从而允许游戏逻辑单独控制每个爆炸实例的生命周期、播放速度或特效参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Houdini内部操作：&lt;/strong&gt; 你也可以在Houdini内部，使用 &lt;code&gt;Group Expression&lt;/code&gt; 或 &lt;code&gt;Blast&lt;/code&gt; 节点，通过 &lt;code&gt;name&lt;/code&gt; 属性来选择或删除特定的爆炸实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，虽然“Enforce Unique Name Attribute per instance”选项不在 &lt;code&gt;Pyro Solver&lt;/code&gt; 或 &lt;code&gt;Pyro Source&lt;/code&gt; 节点本身，但它在处理和实例化Pyro模拟结果时，是实现高级控制的关键步骤。你可能是在这种“Pyro结果实例化”的工作流中看到过它。&lt;/p&gt;
</content:encoded></item><item><title>MikuMiku Rig &amp;&amp; Uuunya Tools</title><link>https://fuwari.vercel.app/posts/mikumiku-rig--uuunya-tools/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/mikumiku-rig--uuunya-tools/</guid><description>Blender模型导入Unity的完整工作流程</description><pubDate>Fri, 02 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;虽然Rigify并没有像Unity那样的骨骼重定向功能，但是还有两个工具可以快速帮助我们将MMD 的骨骼模式调整为Rigify的模式。（因为带了控制器果然还是爽啊 直接薄纱传统k帧）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;另：其实Blender中是有重定向骨骼并转换为新骨骼组的功能，名叫 autorig pro（付费） 但是骨骼重定向的时候只会允许出现英文字符 也就是说你还要过一遍mmd tools的翻译&lt;/p&gt;
&lt;p&gt;5.14更新：我好像看到了一个新的叫做Bone Animation Copy Tool？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Miku Miku Rig：&lt;/h2&gt;
&lt;p&gt;国人开发的转换插件，因此对于中文的适配也会更好。&lt;a href=&quot;https://github.com/LaoBro/Miku_Miku_Rig&quot;&gt;https://github.com/LaoBro/Miku_Miku_Rig&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;经过转换后的骨骼将会复制一份作为存在（也就是说两个都可以控制人物），并且不再接受来自VRM的动作文件转换。适合想要快速手k生草动画的时候（&lt;/p&gt;
&lt;h2&gt;Uuunya Tools：&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/MMD-Blender/blender_mmd_uuunyaa_tools&quot;&gt;https://github.com/MMD-Blender/blender_mmd_uuunyaa_tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是MMD Tools的更加复杂的版本，作者的原句是：mmd_uuunyaa_tools is a blender addon for adjust scenes, models and materials in concert with &lt;a href=&quot;https://github.com/UuuNyaa/blender_mmd_tools&quot;&gt;UuuNyaa/blender_mmd_tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;需要安装MMD Tools作为前置安装环境。&lt;/p&gt;
&lt;p&gt;教程【Rigify: 导入VMD动作并修改，导出VMD，使用FBX动作】 &lt;a href=&quot;https://www.bilibili.com/video/BV16V4y1q7dh/?share_source=copy_web&amp;amp;vd_source=b52e07b2a7f0f2a330cbc0df9dbb2a8f&quot;&gt;https://www.bilibili.com/video/BV16V4y1q7dh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，虽然和MMR相比，Uuunya允许使用VRM的动作文件，但是由于某些不可抗力（其实这个插件的VRM动作文件最后也是嵌套到Rigify中去了，而Rigify的权重，IK，蒙皮等配置一般都会是有默认的配置的），会导致从VRM导入到控制器后会出现一些比例失调的问题。&lt;/p&gt;
&lt;p&gt;看了好久还是感叹maya确实不可撼动（暂时的） 毕竟Blender可以操作的地方太多了 对于对齐工作流绝对是一大弊端&lt;/p&gt;
</content:encoded></item><item><title>将Unity项目迁移至Github</title><link>https://fuwari.vercel.app/posts/%E5%B0%86unity%E9%A1%B9%E7%9B%AE%E8%BF%81%E7%A7%BB%E8%87%B3github/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%B0%86unity%E9%A1%B9%E7%9B%AE%E8%BF%81%E7%A7%BB%E8%87%B3github/</guid><pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我这里用的是SourceTree，是git的可视化版本。
Unity准备工作：
GitHub 准备工作：
SourceTree操作步骤：
添加本地仓库：
在右上角的仓库设置中，对仓库使用的gitignore进行编辑（这里用的是Unity的推荐的gitignore）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Unity generated[Ll]ibrary/[Tt]emp/[Oo]bj/[Bb]uild/[Bb]uilds/[Ll]ogs/[Uu]ser[Ss]ettings/# Unity3D generated meta files*.pidb.meta*.pdb.meta*.mdb.meta# Unity3D generated file on crash reportssysinfo.txt# Visual Studio files.vs/*.csproj*.sln*.suo*.tmp*.user*.userprefs*.pidb*.booproj*.svd# OS generated.DS_Store.DS_Store?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
</content:encoded></item><item><title>Github拒绝推送大文件分支</title><link>https://fuwari.vercel.app/posts/github%E6%8B%92%E7%BB%9D%E6%8E%A8%E9%80%81%E5%A4%A7%E6%96%87%E4%BB%B6%E5%88%86%E6%94%AF/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/github%E6%8B%92%E7%BB%9D%E6%8E%A8%E9%80%81%E5%A4%A7%E6%96%87%E4%BB%B6%E5%88%86%E6%94%AF/</guid><pubDate>Thu, 27 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在本地git完后准备推送的时候，有时候会报错Github不接受单个100MB以上的大文件。这时候需要逐步分析大文件并进行逐一排查。
查看该仓库中存在的大文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rev-list --objects --all | git cat-file --batch-check=&apos;%(objectname) %(objecttype) %(size)&apos; | sort -k 3 -n -r | head -n 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方式1：使用git-lfs&lt;/h2&gt;
&lt;p&gt;git lfs支持将指定类型的大文件做处理（如指定zip、rar等），但是在对于排查后的项目文件可能难以指定(比如说一般的插件也会过大，但是文件类型并不一致不能简单地写文件类型）&lt;/p&gt;
&lt;h2&gt;方式2：使用git filter-repo&lt;/h2&gt;
&lt;p&gt;git filter-repo是一个屏蔽/删除工具,可以定向清除指定的文件夹和文件并可以对分支中的所有版本生效。
在git中安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install git-filter-repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后指定文件夹/文件并删除（这里以插件文件夹为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git filter-repo --path plugins/large_file.zip --invert-paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的历史版本已被修改，需要重写。因此在git push的时候需要指定--force。
对于SourceTree只能回退版本了；当然也是有弊端的，如果这个文件已经在多个提交甚至在一开始就提交过，那么不管回退多少都是没用的，只能用git filter-repo了。&lt;/p&gt;
</content:encoded></item><item><title>C#脚本和Shader中共享同步变量</title><link>https://fuwari.vercel.app/posts/csharp-script-shader-sync-variables/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/csharp-script-shader-sync-variables/</guid><description>C#脚本和Shader中共享同步变量的解决方案和处理方法</description><pubDate>Mon, 10 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个博客的起源是我在参考其他人的思路的时候 ，日出日落的天空盒变化需要通过参考灯光向量的y轴进行lerp操作。但是Unity6中似乎无法复现，在日出日落（0.25时间，0.75时间）会出现明显的跳变现象。经过反复&lt;s&gt;折磨&lt;/s&gt;调试发现似乎是Unity采用了投机取巧的方式。&lt;/p&gt;
&lt;p&gt;由于我的灯光向量（Rotation）从-90开始计数，根据时间的变化逐渐加满，结果灯光向量在加到360的时候居然会直接归0？方向确实也没有什么变化，灯光的方向也是对的，但是会导致天空盒出现十分明显的跳变（跳个几帧又恢复为原来的情况）。故想到了使用时间控制系统中的Time来控制天空盒的Lerp。我现在使用的方式是 Shader.SetGlobal的方法进行变量共享 。大致原理是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C# 脚本侧&lt;/strong&gt;&lt;br /&gt;
使用 Unity 提供的 API，例如 &lt;code&gt;Shader.SetGlobalFloat&lt;/code&gt;、&lt;code&gt;Shader.SetGlobalVector&lt;/code&gt; 等，将值传递给 Shader&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shader 侧&lt;/strong&gt;&lt;br /&gt;
在 Shader 中定义与 C# 脚本中对应的全局变量（如 &lt;code&gt;_TimeOfDay&lt;/code&gt;），然后通过变量的值来动态控制材质的外观或渲染效果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享机制&lt;/strong&gt;&lt;br /&gt;
通过全局变量的方式，Shader 的多个实例（材质）可以共享同一个变量值，避免逐一设置变量值的繁琐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;C#脚本侧&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class DayNightCycle : MonoBehaviour
{
    [Header(&quot;Time Settings&quot;)]
    public float dayDuration = 120f;
    public float maxSunIntensity = 1f; 
    public float minSunIntensity = 0f; 

    [Header(&quot;Light Settings&quot;)]
    public Light sunLight; 
    public Gradient lightColor; 

    [Range(0, 1)]
    public float timeOfDay = 0f; // 首先在class中声明一个timeOfDay

    private float timeSpeed; 
    
    [Header(&quot;Bloom Settings&quot;)]
    public Volume postProcessingVolume; 
    private Bloom bloom; 
    public float maxBloomIntensity = 10f; 
    public float minBloomIntensity = 0f; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先进行声明，然后进入Start中将这个变量共享：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Start()
    {
        // 在这里设置全局 Shader 属性
        Shader.SetGlobalFloat(&quot;_TimeOfDay&quot;, timeOfDay);
        timeSpeed = 1f / dayDuration;
        //Debug
        if (postProcessingVolume != null)
        {
           
            if (!postProcessingVolume.profile.TryGet(out bloom))
            {
                Debug.LogError(&quot;无法获取Bloom组件！请确保Volume中添加了Bloom效果。&quot;);
            }
        }
        else
        {
            Debug.LogError(&quot;未指定Post Processing Volume！&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于时间是变化的，因此要在Update中持续进行同步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Update()
    {
        // 广播时间属性，让天空盒可以根据时间变化。前面是Shader中的变量名，后面是C#的变量名
        Shader.SetGlobalFloat(&quot;_TimeOfDay&quot;, timeOfDay);
        timeOfDay += timeSpeed * Time.deltaTime;
        if (timeOfDay &amp;gt; 1f)
        {
            timeOfDay = 0f; 
        }
        UpdateSunRotation();
        UpdateSunColor();
        UpdateSunIntensity();
        UpdateBloomIntensity();
        //Debug
         if (bloom != null)
         {
             Debug.Log($&quot;Bloom intensity: {bloom.intensity.value}, Time: {timeOfDay}&quot;);
         }
         else
         {
             Debug.LogWarning(&quot;Bloom is null!&quot;);
         }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Shader侧&lt;/h2&gt;
&lt;p&gt;在struct完appdata和v2f之后的变量声明中进行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float _Test, addSunandMoon, _addHorizon, _addGradient, _addCloud, _addStar, _MirrorMode;

float _SunRadius, _MoonRadius, _MoonOffset, _MoonTexScale, _MoonTexBrightness, _MoonRotation;
float _TimeOfDay;//在这里进行声明，注意要符合类型
float _MaxCloudHeight;
float4 _DayTopColor, _DayBottomColor, _NightBottomColor, _NightTopColor, _StarsSkyColor;
float4 _HorizonLightNight, _HorizonLightDay, _HorizonColorDay, _HorizonColorNight, _SunSet, _SunColor, _MoonColor;
float4 _CloudColorDayMain, _CloudColorDaySec, _CloudColorNightMain, _CloudColorNightSec;
float _HorizonBrightness, _MidLightIntensity, _CloudBrightnessDay, _CloudBrightnessNight, _Fuzziness, _FuzzinessSec, _DistortionSpeed, _CloudNoiseSpeed, _CloudNoiseScale, _DistortScale, _StarsCutoff, _StarsSpeed, _CloudCutoff, _CloudSpeed, _HorizonHeight, _HorizonIntensity, _CloudScale, _StarScale;
sampler2D _Stars, _CloudNoise, _Cloud, _DistortTex, _MoonTex;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再fragment部分就可以计算了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...other code
float dayNightTransition;
if (_TimeOfDay &amp;lt; 0.25f || _TimeOfDay &amp;gt; 0.75f)
{
    // 夜晚时段
    dayNightTransition = 0;
}
else
{
    // 白天时段
    dayNightTransition = 1;
}

// 在日出日落时添加平滑过渡
if (_TimeOfDay &amp;gt;= 0.2f &amp;amp;&amp;amp; _TimeOfDay &amp;lt;= 0.3f)
{
    // 日出过渡
    dayNightTransition = smoothstep(0.2f, 0.3f, _TimeOfDay);
}
else if (_TimeOfDay &amp;gt;= 0.7f &amp;amp;&amp;amp; _TimeOfDay &amp;lt;= 0.8f)
{
    // 日落过渡
    dayNightTransition = smoothstep(0.8f, 0.7f, _TimeOfDay);
}

float3 skyGradients = lerp(gradientNight, gradientDay, dayNightTransition);
//...other code

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此变量便可以被共享并同步，根据时间的 变化进行lerp（夜晚、日出日落、白天）。&lt;/p&gt;
&lt;p&gt;还有要注意，这个Shader的属性，必须要这个脚本挂在在场景中。&lt;/p&gt;
&lt;h2&gt;还有没有其他的方式进行共享？&lt;/h2&gt;
&lt;p&gt;有的兄弟有的！&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Material.Set*&lt;/code&gt;适合单个材质，简单易用，适合独立材质的变量传递，&lt;strong&gt;但注意会导致材质实例化&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MaterialPropertyBlock&lt;/code&gt;可以作为单个渲染器动态传递变量，&lt;strong&gt;避免材质实例化，适合动态属性设置&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ComputeBuffer&lt;/code&gt;传递数组或结构体；适合&lt;strong&gt;传递大规模数据&lt;/strong&gt;，如粒子系统、动态网格等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cbuffer&lt;/code&gt; / &lt;code&gt;uniform&lt;/code&gt;适用于单个材质，&lt;strong&gt;高效传递简单变量，适合基本场景&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;全局关键字/全局范围：用于控制 Shader 的功能开关，&lt;strong&gt;适合启用/禁用特效或功能模块&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;全局纹理或 RenderTexture：作用于单个材质或全局，传递动态纹理或图像数据，&lt;strong&gt;适合后处理或实时渲染场景&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Shader.SetGlobal*&lt;/code&gt;：全局范围，作为API可以高效传递全局变量，&lt;strong&gt;适合全局控制（如时间、环境光）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GraphicsBuffer（GBUFFER）&lt;/code&gt;是更加高级 GPU 数据传输，&lt;strong&gt;具有高性能，适合高级计算场景（如粒子模拟、大规模数据处理）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Unity的Post-Processing标准（V1、V2、V3）</title><link>https://fuwari.vercel.app/posts/unitypostprocessing/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unitypostprocessing/</guid><description>Unity的Post-Processing标准（V1、V2、V3）</description><pubDate>Thu, 06 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Post Processing Stack (PPS) 是Unity引擎中的一个模块，用于管理和应用各种后处理效果到渲染的图像上。它允许开发者和艺术家在最终图像输出之前，对场景的渲染结果进行一系列的图像处理操作，从而增强视觉效果或实现特定的艺术风格。对这个专门的后处理模块，我们成为Unity Post Processing Stack（后处理堆栈），因为在这个组件中多个后处理可以按照顺序依次实现，像一个栈的存入弹出，故得名。目前根据实现方法分为V1、V2和V3.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Unity-Technologies/PostProcessing?tab=readme-ov-file&quot;&gt;https://github.com/Unity-Technologies/PostProcessing?tab=readme-ov-file&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(来自Unity Graphic项目的branch。主项目在这里&lt;a href=&quot;https://github.com/Unity-Technologies/Graphics&quot;&gt;https://github.com/Unity-Technologies/Graphics&lt;/a&gt;）&lt;/p&gt;
&lt;h2&gt;PPS V1&lt;/h2&gt;
&lt;p&gt;类似于启动时的bat/Tags文件，没有明确的「配置文件」概念，所有效果都集中在脚本中控制；基本上通过直接访问 &lt;code&gt;Camera&lt;/code&gt; 的组件来应用效果。优点是可以快速实现简单的效果，不用考虑override/sealed等覆写和HLSL交互实现。缺点是难以实现高级的后处理效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var postProcessing = Camera.main.GetComponent&amp;lt;PostProcessingBehaviour&amp;gt;();
postProcessing.profile.bloom.enabled = true; // 启用 Bloom 效果

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PPS V2&lt;/h2&gt;
&lt;p&gt;是目前使用较多的后处理标准。因为Unity在PPS V2采用&lt;strong&gt;模块化设计&lt;/strong&gt;，效果分离为独立模块，易于扩展和控制；对应的代码/模块被储存在存储在 &lt;code&gt;PostProcessingProfile&lt;/code&gt; 中，便于共享和编辑 。 （如 PostProcessEffectSettings和PostProcessEffectRenderer 和Shader之间互相隔离）&lt;/p&gt;
&lt;p&gt;核心功能是利用&lt;code&gt;PostProcessEffectSettings&lt;/code&gt; 和 &lt;code&gt;PostProcessEffectRenderer&lt;/code&gt;实现自定义的扩展效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在 V2 中，自定义效果需要继承以下两个核心类：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;**PostProcessEffectSettings**&lt;/code&gt;&lt;br /&gt;
用于定义自定义效果的参数和设置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**PostProcessEffectRenderer**&lt;/code&gt;&lt;br /&gt;
用于实现效果的渲染逻辑。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

// 定义自定义效果的参数
[System.Serializable]
[PostProcess(typeof(CustomEffectRenderer), PostProcessEvent.AfterStack, &quot;Custom/CustomEffect&quot;)]
public class CustomEffect : PostProcessEffectSettings
{
    public FloatParameter intensity = new FloatParameter { value = 1.0f }; // 参数1
    public ColorParameter tint = new ColorParameter { value = Color.white }; // 参数2
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

public class CustomEffectRenderer : PostProcessEffectRenderer&amp;lt;CustomEffect&amp;gt;
{
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find(&quot;Hidden/CustomEffectShader&quot;));
        sheet.properties.SetFloat(&quot;_Intensity&quot;, settings.intensity);
        sheet.properties.SetColor(&quot;_Tint&quot;, settings.tint);
        
        // 应用 Shader 效果
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;工作原理&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PostProcessEffectSettings&lt;/code&gt; 类描述了效果的配置，包括所有参数（如强度、颜色等）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PostProcessEffectRenderer&lt;/code&gt; 类负责渲染这些效果，通常需要使用 Shader 实现具体的视觉效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ehyMwVnnnTg&quot;&gt;https://www.youtube.com/watch?v=ehyMwVnnnTg&lt;/a&gt; 讲述了如何使用V2标准实现角色描边。&lt;/p&gt;
&lt;h2&gt;PPS V3&lt;/h2&gt;
&lt;p&gt;PPS V3将特性完全集成到 Unity 的 &lt;strong&gt;URP（Universal Render Pipeline）&lt;/strong&gt; 和 &lt;strong&gt;HDRP（High Definition Render Pipeline）&lt;/strong&gt;，不再作为独立插件，而是作为渲染管线的一部分。&amp;lt;u&amp;gt;后处理效果将完全通过 Volume Profile 管理&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;p&gt;效果参数不再通过 &lt;code&gt;PostProcessVolume&lt;/code&gt; 调整，而是直接访问 Volume 组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class URPPostProcessingExample : MonoBehaviour
{
    public Volume volume;

    void Start()
    {
        // 获取 Volume Profile 中的 Bloom 设置
        if (volume.profile.TryGet&amp;lt;Bloom&amp;gt;(out var bloom))
        {
            bloom.intensity.value = 3f; // 修改强度
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Yuque Exporter使用说明</title><link>https://fuwari.vercel.app/posts/yuque-exporter%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/yuque-exporter%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E/</guid><description>Git项目中.gitignore文件的配置方法</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这个脚本的核心任务是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;遍历&lt;/strong&gt;你指定的 Markdown 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找&lt;/strong&gt;文件中所有符合语雀图片格式的 URL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载&lt;/strong&gt;这些 URL 指向的图片。&lt;/li&gt;
&lt;li&gt;将图片&lt;strong&gt;保存&lt;/strong&gt;到本地（最好是与 Markdown 文件放在一起，利用 Hugo 的页面捆绑功能）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;替换&lt;/strong&gt; Markdown 文件中原来的网络 URL 为新的本地路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面，我将为你提供一个开箱即用的 Python 脚本，并详细解释如何使用它来完美解决你的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;方案：使用 Python 脚本自动化处理&lt;/h3&gt;
&lt;p&gt;这个方案是目前最灵活、最一劳永逸的。我们将编写一个 Python 脚本，它会自动将你的 Markdown 文件转换为 Hugo 推荐的 &lt;strong&gt;页面捆绑（Page Bundle）&lt;/strong&gt; 结构，并将所有图片下载到文章对应的文件夹内。&lt;/p&gt;
&lt;h4&gt;准备工作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装 Python&lt;/strong&gt;: 如果你的电脑还没有安装 Python，请先从 &lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;python.org&lt;/a&gt; 下载并安装。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安装 requests 库&lt;/strong&gt;: 这是一个用来发送网络请求（如下载图片）的库。在你的终端或命令行中运行：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Python 脚本 (&lt;code&gt;yuque_to_hugo.py&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;请将以下代码保存为一个名为 &lt;code&gt;yuque_to_hugo.py&lt;/code&gt; 的文件，并将它放在你 &lt;strong&gt;Hugo 项目的根目录&lt;/strong&gt;（与 &lt;code&gt;config.toml&lt;/code&gt; 和 &lt;code&gt;content&lt;/code&gt; 文件夹同级）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import re
import requests
import hashlib
from urllib.parse import urlparse
import shutil

# --- 配置区 ---
# 你从语雀导出的 Markdown 文件存放的目录
# 假设你把它们都放在了 content/posts/yuque-import 目录下
MARKDOWN_DIR = &quot;content/posts/yuque-import&quot; 
# 语雀图片链接的特征，用于正则匹配
# 修正点: 使用了非捕获分组 (?:...) 来避免产生额外的捕获结果
YUQUE_CDN_PATTERN = r&quot;https?://(?:cdn\.nlark\.com|aliyuncs\.com)/yuque&quot;
# --- 配置区结束 ---

# 正则表达式，用于匹配 Markdown 中的图片链接
# 现在它会正确地只捕获两个部分：1.alt文本, 2.图片URL
IMG_REGEX = re.compile(r&quot;!\[(.*?)\]\((%s.*?)\)&quot; % YUQUE_CDN_PATTERN)

def process_markdown_file(md_file_path):
    &quot;&quot;&quot;处理单个 Markdown 文件&quot;&quot;&quot;
    print(f&quot;--- 开始处理文件: {md_file_path} ---&quot;)

    # 1. 将普通 .md 文件转换为 Page Bundle 结构
    dir_path, file_name = os.path.split(md_file_path)
    base_name, ext = os.path.splitext(file_name)
    
    # 如果已经是 index.md 或 _index.md，则直接使用其所在目录
    if file_name.lower() in [&quot;index.md&quot;, &quot;_index.md&quot;]:
        bundle_dir = dir_path
        # 如果是这种情况，md_file_path 已经是正确的路径，不需要移动
    else:
        bundle_dir = os.path.join(dir_path, base_name)
        new_md_path = os.path.join(bundle_dir, &quot;index.md&quot;)

        if not os.path.exists(bundle_dir):
            os.makedirs(bundle_dir)
            print(f&quot;创建页面捆绑目录: {bundle_dir}&quot;)
        
        # 移动并重命名 md 文件
        shutil.move(md_file_path, new_md_path)
        md_file_path = new_md_path
        print(f&quot;已将 {file_name} 移动到 {new_md_path}&quot;)

    # 2. 读取新的 md 文件内容
    with open(md_file_path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        content = f.read()

    # 3. 查找所有匹配的图片链接
    # 修正后的 findall 将只返回 (alt_text, url) 这样的二元组
    images = IMG_REGEX.findall(content)
    if not images:
        print(&quot;未找到需要处理的语雀图片。\n&quot;)
        return

    print(f&quot;找到 {len(images)} 张语雀图片，开始下载和替换...&quot;)
    
    # 4. 遍历所有找到的图片链接
    for alt_text, url in images:
        try:
            # 清理 URL，移除 # 后面的参数
            clean_url = url.split(&apos;#&apos;)[0]
            
            # 生成一个简短且唯一的文件名，避免中文或特殊字符问题
            # 使用 URL 的 MD5 哈希值前8位作为文件名
            file_ext = os.path.splitext(urlparse(clean_url).path)[1] or &apos;.png&apos; # 如果没有后缀，默认为.png
            file_hash = hashlib.md5(clean_url.encode()).hexdigest()[:8]
            new_filename = f&quot;{file_hash}{file_ext}&quot;
            
            # 图片要保存的本地路径
            local_image_path = os.path.join(bundle_dir, new_filename)
            
            # 下载图片
            if not os.path.exists(local_image_path):
                print(f&quot;  -&amp;gt; 正在下载: {clean_url}&quot;)
                headers = {&apos;User-Agent&apos;: &apos;Mozilla/5.0&apos;} # 模拟浏览器，防止被禁
                response = requests.get(clean_url, headers=headers, stream=True)
                response.raise_for_status() # 如果下载失败则抛出异常
                
                with open(local_image_path, &quot;wb&quot;) as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
                print(f&quot;     保存成功: {local_image_path}&quot;)
            else:
                print(f&quot;  -&amp;gt; 图片已存在，跳过下载: {new_filename}&quot;)

            # 替换 Markdown 内容中的旧 URL 为新本地路径
            # 注意：这里我们只替换文件名，因为图片和md文件在同一目录
            original_markdown_link = f&quot;![{alt_text}](/images/posts/{url})&quot;
            new_markdown_link = f&quot;![{alt_text}](/images/posts/{new_filename})&quot;
            content = content.replace(original_markdown_link, new_markdown_link)

        except requests.exceptions.RequestException as e:
            print(f&quot;     下载失败: {url}, 错误: {e}&quot;)
        except Exception as e:
            print(f&quot;     处理失败: {url}, 错误: {e}&quot;)

    # 5. 将修改后的内容写回文件
    with open(md_file_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        f.write(content)
        
    print(f&quot;文件 {md_file_path} 处理完成。\n&quot;)


if __name__ == &quot;__main__&quot;:
    if not os.path.isdir(MARKDOWN_DIR):
        print(f&quot;错误: 目录 &apos;{MARKDOWN_DIR}&apos; 不存在。请检查配置。&quot;)
    else:
        # 遍历目录下的所有 .md 文件
        for root, _, files in os.walk(MARKDOWN_DIR):
            # 创建一个文件列表的副本进行迭代，因为我们可能会在循环中重命名文件
            for file in list(files):
                if file.endswith(&quot;.md&quot;):
                    process_markdown_file(os.path.join(root, file))
        print(&quot;所有 Markdown 文件处理完毕！&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;如何使用这个脚本？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;导出并放置文件&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;从语雀批量导出你的文章为 Markdown 格式。&lt;/li&gt;
&lt;li&gt;在你的 Hugo 项目的 &lt;code&gt;content/posts/&lt;/code&gt; 目录下，创建一个新文件夹，比如 &lt;code&gt;yuque-import&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将所有导出的 &lt;code&gt;.md&lt;/code&gt; 文件&lt;strong&gt;复制&lt;/strong&gt;到 &lt;code&gt;content/posts/yuque-import/&lt;/code&gt; 目录中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置脚本&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;打开 &lt;code&gt;yuque_to_hugo.py&lt;/code&gt; 文件。&lt;/li&gt;
&lt;li&gt;确认 &lt;code&gt;MARKDOWN_DIR&lt;/code&gt; 变量的值与你上一步创建的文件夹路径一致。默认是 &lt;code&gt;content/posts/yuque-import&lt;/code&gt;，如果你的路径不同，请修改它。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行脚本&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;打开你的终端（命令行工具）。&lt;/li&gt;
&lt;li&gt;确保你当前位于 Hugo 项目的根目录。&lt;/li&gt;
&lt;li&gt;运行脚本：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;python yuque_to_hugo.py
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;查看结果&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;脚本运行后，你会看到一系列处理日志。&lt;/li&gt;
&lt;li&gt;完成后，检查你的 &lt;code&gt;content/posts/yuque-import/&lt;/code&gt; 文件夹。你会发现原来的 &lt;code&gt;文章名.md&lt;/code&gt; 文件都变成了 &lt;code&gt;文章名/index.md&lt;/code&gt; 的结构，并且每个文件夹里都包含了该文章下载好的图片。&lt;/li&gt;
&lt;li&gt;打开任意一个 &lt;code&gt;index.md&lt;/code&gt; 文件，你会看到图片链接 &lt;code&gt;[...](https://cdn.nlark.com/...)&lt;/code&gt; 已经被替换成了 &lt;code&gt;[...](abcdef12.png)&lt;/code&gt; 这样的本地链接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;脚本工作流程详解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转换为页面捆绑&lt;/strong&gt;: 脚本首先将 &lt;code&gt;文章A.md&lt;/code&gt; 这样的文件，转换成 &lt;code&gt;文章A/index.md&lt;/code&gt; 的目录结构。这是 Hugo 的最佳实践，便于管理与文章相关的资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正则匹配&lt;/strong&gt;: 使用正则表达式精准地找出所有语雀的图片链接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成唯一文件名&lt;/strong&gt;: 直接使用 URL 中的文件名可能很长或包含特殊字符。脚本通过计算 URL 的 MD5 哈希值来生成一个简短、唯一且安全的新文件名，如 &lt;code&gt;abcdef12.png&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载与保存&lt;/strong&gt;: 模拟浏览器下载图片，并将其保存在与 &lt;code&gt;index.md&lt;/code&gt; 同一个文件夹下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径替换&lt;/strong&gt;: 将 Markdown 中的长 URL 替换为新的、简单的本地文件名。因为图片和 &lt;code&gt;index.md&lt;/code&gt; 在同一目录，所以可以直接引用文件名，非常简洁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，你只需要 &lt;code&gt;hugo server&lt;/code&gt; 启动本地预览，或者直接 &lt;code&gt;git add .&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt; 部署，你的博客就会完美地显示所有图片，并且这些图片已经成为了你博客项目的一部分，不再依赖语雀的服务器。这个脚本可以反复运行，以后每次从语雀导出新文章，只需重复上述步骤即可。&lt;/p&gt;
</content:encoded></item><item><title>关于Da Vinci安装后无法启动的问题</title><link>https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8Eda-vinci%E5%AE%89%E8%A3%85%E5%90%8E%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8%E7%9A%84%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E5%85%B3%E4%BA%8Eda-vinci%E5%AE%89%E8%A3%85%E5%90%8E%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8%E7%9A%84%E9%97%AE%E9%A2%98/</guid><description>关于Da Vinci安装后无法启动的问题的解决方案和处理方法</description><pubDate>Mon, 24 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;具体的情况是：在安装了20并且patch了激活证之后，不论是E盘还是C盘，&lt;strong&gt;在打开Davinci都会出现在加载插件部分闪退，但是任务管理器中的后台任务仍然存活&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原理：直接删掉C:\Program Files\Common Files\OFX\Plugins里面的所有插件，有可能是FilmConvert, RedGiant, iZotope之类的其中某个插件跟达芬奇有冲突，可以一个个排查，我就是整个直接删掉TopazAI插件后达芬奇就能启动了/好吧其实是直接把插件放到了另一个地方&lt;/p&gt;
</content:encoded></item><item><title>VEX：如何沿用上组变量</title><link>https://fuwari.vercel.app/posts/vex%E5%A6%82%E4%BD%95%E6%B2%BF%E7%94%A8%E4%B8%8A%E7%BB%84%E5%8F%98%E9%87%8F-1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/vex%E5%A6%82%E4%BD%95%E6%B2%BF%E7%94%A8%E4%B8%8A%E7%BB%84%E5%8F%98%E9%87%8F-1/</guid><description>Houdini VEX编程中的变量使用技巧</description><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;偶然在用的时候看到了一行代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float rotation = radians(@rotation*360);
vector axis = v@axis; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里在引用属性的时候出现了两种情况：一种是@rotation;一种是v@axis。这之间其实是指定类型和自动查找类型之间的差异：&lt;/p&gt;
&lt;p&gt;在VEX中，&lt;code&gt;v@&lt;/code&gt;和单独的&lt;code&gt;@&lt;/code&gt;是属性访问的不同语法，区别在于数据类型的明确指定：&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;@&lt;/code&gt; 语法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@axis  // 自动推断数据类型
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;VEX会根据上下文&lt;strong&gt;自动推断&lt;/strong&gt;属性的数据类型&lt;/li&gt;
&lt;li&gt;如果无法明确推断，可能导致错误或意外结果&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;v@&lt;/code&gt; 语法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v@axis  // 明确指定为vector类型
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;明确指定&lt;/strong&gt;属性为vector类型&lt;/li&gt;
&lt;li&gt;即使原始属性是其他类型，也会尝试转换为vector&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;类型前缀列表&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i@&lt;/code&gt; - integer (整数)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f@&lt;/code&gt; - float (浮点数)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;v@&lt;/code&gt; - vector (向量)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p@&lt;/code&gt; - vector4 (四维向量)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s@&lt;/code&gt; - string (字符串)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u@&lt;/code&gt; - vector2 (二维向量)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实际区别示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 如果axis属性实际是float类型
vector axis1 = @axis;   // 可能报错或产生意外结果
vector axis2 = v@axis;  // 强制转换为vector，如{axis_value, 0, 0}

// 如果axis属性确实是vector类型
vector axis3 = @axis;   // 正常工作
vector axis4 = v@axis;  // 正常工作，等价
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最佳实践&lt;/h2&gt;
&lt;p&gt;使用类型前缀（如&lt;code&gt;v@&lt;/code&gt;）更安全、更明确，特别是在复杂场景中，能避免类型推断错误。&lt;/p&gt;
</content:encoded></item><item><title>模糊算法</title><link>https://fuwari.vercel.app/posts/%E6%A8%A1%E7%B3%8A%E7%AE%97%E6%B3%95/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%A8%A1%E7%B3%8A%E7%AE%97%E6%B3%95/</guid><description>模糊算法的原理解析和实现方法</description><pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;二维卷积模糊&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// GaussianBlur.hlsl
float4 GaussianBlur(sampler2D tex, float2 uv, float2 resolution, float radius)
{
    float4 color = float4(0, 0, 0, 0);
    float totalWeight = 0.0;
    int samples = 5; // Number of samples for blur

    for (int x = -samples; x &amp;lt;= samples; x++)
    {
        for (int y = -samples; y &amp;lt;= samples; y++)
        {
            float2 offset = float2(x, y) * radius / resolution;
            float weight = exp(-dot(offset, offset) / (2.0 * radius * radius));
            color += tex2D(tex, uv + offset) * weight;
            totalWeight += weight;
        }
    }

    return color / totalWeight;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;Frosted Glass Blur（毛玻璃模糊，随机采样模糊）&lt;/strong&gt;&lt;/h2&gt;
</content:encoded></item><item><title>Unity区域光（Area Light） 没有反应</title><link>https://fuwari.vercel.app/posts/unity%E5%8C%BA%E5%9F%9F%E5%85%89area-light-%E6%B2%A1%E6%9C%89%E5%8F%8D%E5%BA%94/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity%E5%8C%BA%E5%9F%9F%E5%85%89area-light-%E6%B2%A1%E6%9C%89%E5%8F%8D%E5%BA%94/</guid><description>Unity区域光（Area Light） 没有反应的技术原理和应用</description><pubDate>Sun, 09 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;确认被照射的物体是否勾选了&lt;code&gt;static&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;如果还是没有反应，可以检查&lt;code&gt;LightSettings&lt;/code&gt;面板，确保已经勾选了&lt;code&gt;Auto Generate&lt;/code&gt;选项。&lt;/p&gt;
&lt;p&gt;勾选 Static 的物体会告诉 Unity 这个物体可以用于特定的优化或功能。Static 选项下包含多个具体的静态属性，你可以单独启用它们，或者通过勾选 Static 启用所有静态属性。&lt;/p&gt;
&lt;p&gt;Static 的具体优化作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Lightmapping（光照贴图）&lt;br /&gt;
如果物体被标记为静态，Unity 会将它包括在光照贴图（Lightmap）的计算中。&lt;br /&gt;
光照贴图是一种预计算的光照技术，用于烘焙全局光照、阴影等效果，使运行时无需实时计算。&lt;br /&gt;
作用：&lt;br /&gt;
提高性能：光照贴图的渲染开销低，适合静态场景。&lt;br /&gt;
提供全局光照和间接光照：静态物体可以参与全局光的反射计算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Occlusion Culling（遮挡剔除）&lt;br /&gt;
静态物体会被包括在 Unity 的遮挡剔除计算中。&lt;br /&gt;
遮挡剔除会自动剔除被其他物体遮挡的物体，从而减少渲染工作。&lt;br /&gt;
作用：&lt;br /&gt;
提高性能：避免渲染玩家不可见的物体。&lt;br /&gt;
静态物体更适合参与遮挡剔除，因为它们的位置不会改变。&lt;/p&gt;
&lt;p&gt;但是请注意一下不要背面一剔除那么就会有些不合适的观感（&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Batching（批处理优化）&lt;br /&gt;
勾选 Static 后，静态物体可以参与以下两种批处理优化：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Static Batching（静态批处理）：&lt;/strong&gt;&lt;br /&gt;
静态物体会被合并为一个大网格（Mesh），减少绘制调用（Draw Calls）。&lt;br /&gt;
要求：静态物体必须使用相同的材质。&lt;br /&gt;
作用：&lt;br /&gt;
显著减少 Draw Calls，提高性能。&lt;br /&gt;
适用于数量多、变化少的静态物体。&lt;br /&gt;
动态物体无法使用静态批处理，即使它们共享材质。&lt;/p&gt;
&lt;p&gt;Navigation（导航网格计算）&lt;br /&gt;
静态物体会被包括在导航网格（NavMesh）计算中。&lt;br /&gt;
作用：&lt;br /&gt;
如果物体是静态的，则在生成导航网格时，Unity 会将其视为障碍物，供导航 AI 使用。&lt;br /&gt;
适用于地形、墙壁等不会移动的物体。&lt;br /&gt;
5. &lt;strong&gt;Reflection Probes（反射探针）&lt;/strong&gt;&lt;br /&gt;
静态物体会被包括在反射探针（Reflection Probe）数据中。&lt;br /&gt;
作用：&lt;br /&gt;
提供更准确的反射效果。&lt;br /&gt;
静态物体在反射探针中的数据是预计算的，提高性能。&lt;br /&gt;
6. &lt;strong&gt;Light Probes（光照探针）&lt;/strong&gt;&lt;br /&gt;
静态物体会影响光照探针（Light Probe）的预计算。&lt;br /&gt;
作用：&lt;br /&gt;
静态物体可用于影响动态物体的间接光照和反射。&lt;br /&gt;
提供更精确的光照环境。&lt;br /&gt;
7. Off-Mesh Links（导航链接）&lt;br /&gt;
如果启用 Navigation Static，静态物体可以作为导航网格的起点或终点，用于创建导航链接。&lt;br /&gt;
8.** Contribute GI（参与全局光照）**&lt;br /&gt;
勾选 Static 后，物体可以参与全局光照（Global Illumination）的预计算。&lt;br /&gt;
作用：&lt;br /&gt;
静态物体会反射全局光，为其他物体提供间接光照。&lt;br /&gt;
提供更逼真的光照效果。&lt;/p&gt;
</content:encoded></item><item><title>Unity阴影实现</title><link>https://fuwari.vercel.app/posts/unity-shadow-implementation/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity-shadow-implementation/</guid><description>Unity阴影实现的解决方案和处理方法</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Investigate how Unity renders shadows.&lt;br /&gt;
用大白话说阴影是如何产生的&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;阴影的产生来自于两个核心要素：来自摄像机的深度测试、以光源为中心进行shadowmap的产生，最后和摄像机的深度图进行测试比较的过程。&lt;/p&gt;
&lt;p&gt;有一些技术可以支持实时阴影。每个都有其优点和缺点。 除了Shadowmap技术，还有体积阴影、SSS屏幕空间阴影、Ray Tracing光线追踪阴影、区域光源阴影Area Light Shadow、阴影体Shadow Volumne。Unity使用了当今最常见的技术，那就是&lt;strong&gt;阴影贴图&lt;/strong&gt;。这意味着 Unity 以某种方式将阴影信息存储在纹理中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/280e33e9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这个例子中，我用的是forward渲染模式，并且用两个pass进行阴影的处理：Foraward Lit Pass;Shadow Caster Pass.Shadow catser pass&lt;strong&gt;仅负责将物体渲染到阴影贴图中，输出不会被显示&lt;/strong&gt;；Forward Lit Pass&lt;/p&gt;
&lt;p&gt;则不仅处理一般的光照，也会处理并生成阴影。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shader &quot;KelvinvanHoorn/ShadowOutlines&quot;
{
    Properties
    {
    }
    SubShader
    {
        Tags { &quot;RenderType&quot;=&quot;Opaque&quot; &quot;RenderPipeline&quot; = &quot;UniversalRenderPipeline&quot;}
        Cull Back

        Pass
        {
            Name &quot;ForwardLit&quot;
            Tags { &quot;LightMode&quot; = &quot;UniversalForward&quot; }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;

            struct Attributes
            {
                float4 vertex   : POSITION;
                float3 normal   : NORMAL;
            };

            struct Varyings
            {
                float4 posCS        : SV_POSITION;
                float3 posWS        : TEXCOORD0;
                float3 normalWS     : TEXCOORD1;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs vertexInput = GetVertexPositionInputs(IN.vertex.xyz);
                OUT.posCS = vertexInput.positionCS;
                OUT.posWS = vertexInput.positionWS;

                VertexNormalInputs normalInput = GetVertexNormalInputs(IN.normal);
                OUT.normalWS = normalInput.normalWS;

                return OUT;
            }

            float4 frag (Varyings IN) : SV_Target
            {
                float3 col = float3(1, 1, 1);

                return float4(col, 1);
            }
            ENDHLSL
        }
        pass
        {
            Name &quot;ShadowCaster&quot;
            Tags{&quot;LightMode&quot; = &quot;ShadowCaster&quot;}

            ZWrite On
            ZTest LEqual
            ColorMask 0

            HLSLPROGRAM

            #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;
            #include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;

            #pragma vertex vert
            #pragma fragment frag

            struct Attributes
            {
                float4 vertex   : POSITION;
                float3 normal   : NORMAL;
            };

            struct Varyings
            {
                float4 posCS        : SV_POSITION;
            };

            float3 _LightDirection;

            Varyings vert(Attributes IN)
            {
                    Varyings OUT = (Varyings)0;
                    VertexPositionInputs vertexInput = GetVertexPositionInputs(IN.vertex.xyz);
                    float3 posWS = vertexInput.positionWS;

                    VertexNormalInputs normalInput = GetVertexNormalInputs(IN.normal);
                    float3 normalWS = normalInput.normalWS;

                    // Shadow biased ClipSpace position
                    float4 positionCS = TransformWorldToHClip(ApplyShadowBias(posWS, normalWS, _LightDirection));

                    #if UNITY_REVERSED_Z
                        positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
                    #else
                        positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
                    #endif

                    OUT.posCS = positionCS;

                    return OUT;
            }

            float4 frag (Varyings IN) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分别拿到Clip Space和World Space下的各个坐标。&lt;/p&gt;
&lt;h3&gt;光源投影阶段：生成 Shadow Map&lt;/h3&gt;
&lt;p&gt;“当启用定向阴影时，Unity 会根据设置的阴影分辨率生成一个深度贴图（Shadow Map），其分辨率由开发者在质量设置或渲染管线中定义。这个功能由Unity的Light文件支持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在渲染ShadowMap之前，我们需要先获取ShadowCoord。需要调用TransformWorldToShadowCoord 函数（fragment），并向其传递我们的世界位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float4 frag (Varyings IN) : SV_Target
{
    float4 shadowCoord = TransformWorldToShadowCoord(IN.posWS);

    float3 col = float3(1, 1, 1);

    return float4(col, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;ShadowMap和ShadowCoord之间有什么区别？&lt;/p&gt;
&lt;p&gt;**ShadowCoord类比于书的页码，&lt;strong&gt;是用来查询的坐标（UV），而&lt;/strong&gt;ShadowMap类似于书的内容，**是被查询的数据源（纹理）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注：这里的ShadowCoord = TransformWorldToShadowCoord可以在vertex部分进行计算，因为frag的量远大于vertex的量，并且vertex之间的计算数据可以共享，在顶点着色器中计算的结果可以通过插值（interpolation）传递给片段着色器，这样可以在片段着色器中使用这些已经计算好的值，减少了重复计算。&lt;strong&gt;然而插值会造成精度误差，尤其是纹理和阴影这种需要高精度计算的地方&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;计算完Shadowcoord，就可以为ShadowMap做准备了！ShadowMap渲染整个场景，但仅记录每个片段的深度信息。  GPU 使用该深度信息来判断光线是否能够直接到达场景中的每个片段，从而确定哪些片段位于阴影中 。该数据对应于光源视角下的深度值，它表示光线从光源出发到达片段表面的距离，并存储在光源的&lt;strong&gt;投影空间中&lt;/strong&gt;。深度信息最终存储为 0-1 范围内的值。查看纹理时，附近的纹素显得很暗。纹理像素越远，它就越亮。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float4 frag (Varyings IN) : SV_Target
{
    float4 shadowCoord = TransformWorldToShadowCoord(IN.posWS);

    float shadowMap = MainLightRealtimeShadow(shadowCoord);
    
    float3 col = float3(1, 1, 1) * shadowMap;

    return float4(col, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;摄像机渲染阶段：从摄像机视角检测阴影&lt;/h3&gt;
&lt;p&gt;在摄像机渲染阶段，Unity 利用之前从光源视角生成的深度贴图，结合摄像机视角下的像素位置，判断哪些像素位于阴影中。实际上，灯充当了相机的作用。这意味着深度值告诉我们光线在撞击某物之前传播了多远。根据产生的shadowmap，我们可以确定哪些物体被遮挡，哪些物体没有被遮挡。&lt;/p&gt;
&lt;p&gt;因为我们使用的是定向光（不是点光源），所以他们的相机是正交的。因此，不存在透视投影，并且灯光相机的确切位置并不重要。 Unity 将定位相机，以便它可以看到普通相机视野中的所有对象。&lt;/p&gt;
&lt;p&gt;在#include上方定义三个关键字，第一个关键字是获取主光源阴影所必需的，另外 2 个关键字用于处理 1.2 节中 URP 资源的设置。在 includes 上方添加以下 3 个突出显示的行。每个multi_compile基本上告诉 Unity 创建 2 个着色器变体，一个是打开的，另一个是关闭的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;
#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一行是打开Shadows，可以通过修改Lighting模块下ShadowMap的分辨率获取更高的精度；第二个是CSM阴影；第三个是开启PCF进行阴影的多次采样最后进行加权以获得软阴影。&lt;/p&gt;
&lt;p&gt;**对于定向光，Unity 使用级联阴影贴图（Cascaded Shadow Maps, CSM，也就是第二行的compile定义_MAIN_LIGHT_SHADOWS_CASCADE）技术，这个技术可以在Lighting中或者是Settings中的渲染管线中可以修改;CSM会将摄像机视锥体划分为多个区域（级联），每个区域生成独立的阴影贴图。这使得近处物体的阴影更精确，而远处物体的阴影可以使用较低分辨率贴图  **。如果要切换到两个级联，则每个灯光的场景将渲染两次。如果没有级联，每个灯光仅渲染一次。当我们观察阴影的质量时，我们就会明白为什么 Unity 会这样做（喜欢我全是锯齿的阴影吗）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;阴影级联（&lt;strong&gt;Cascade Shadow Maps, CSM&lt;/strong&gt;）是一种优化技术，主要用于方向光（如太阳光）的阴影渲染中。它通过将视锥体划分为多个层级或“级联”，每个级分配有不同的分辨率和覆盖范围，从而提高阴影的质量和性能。这种技术解决了传统阴影贴图在大场景中容易出现的问题，比如远处物体的阴影质量差、近处物体的阴影精度不足等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最后还有Shadow Bias和Normal Bias，这两个选项用于抵消Shadow Acne问题，这两个选项最后会应用于&lt;strong&gt;ShadowCaster的vertex处理&lt;/strong&gt;。详见&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/366555785&quot;&gt;https://zhuanlan.zhihu.com/p/366555785&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从相机的角度来看，我们已经拥有场景的深度信息了，并且还从每个灯光的角度获得了这些信息。当然，这些数据存储在不同的剪辑空间中，但我们知道这些空间的相对位置和方向。所以我们可以从一个空间转换到另一个空间。这使我们能够从两个角度比较深度测量结果。从概念上讲，&lt;strong&gt;&amp;lt;u&amp;gt;我们有两个向量应该在同一点结束。如果他们这样做了，相机和灯光都可以看到该点，因此它会被点亮。如果光线的矢量在到达该点之前结束，则光线被阻挡，这意味着该点被遮挡。&amp;lt;/u&amp;gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Unity 通过渲染覆盖整个视图的单个四边形来创建这些纹理。它使用 &lt;em&gt;Hidden/Internal-ScreenSpaceShadows&lt;/em&gt; 此通道的着色器。每个片段从场景和灯光的深度纹理中进行采样，进行比较，并将最终阴影值渲染到屏幕空间阴影贴图。光照纹理元素设置为 1，阴影纹理元素设置为 0。&amp;lt;u&amp;gt;此时，Unity 还可以执行过滤，以创建软阴影。（软阴影在此时可以进行渲染、虽然unity里是以阴影过渡的方式间接实现的）&amp;lt;/u&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用NdotL阴影提高阴影精度&lt;/h2&gt;
&lt;p&gt;我们还可以通过根据光线方向和物体的法线方向计算阴影并取两个阴影中的最小值来规避阴影精度过低的问题。我们通过在主光源方向和对象法线方向之间取点积来计算表面是否被照亮。此结果通常缩写为 NdotL。然后，我们的&lt;strong&gt;最终阴影值就是阴影贴图和 NdotL 的最小值&lt;/strong&gt;。添加高亮显示的行并更改 col 的值以反映最终的阴影。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float4 shadowCoord = TransformWorldToShadowCoord(IN.posWS);
float shadowMap = MainLightRealtimeShadow(shadowCoord);

float NdotL = saturate(dot(_MainLightPosition.xyz, IN.normalWS));

float combinedShadow = min(NdotL, shadowMap);

float3 col = float3(1, 1, 1) * combinedShadow;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恭喜你，一个Lambert光照模型就完成了（绷）&lt;/p&gt;
&lt;p&gt;最后黑的地方太黑了，可以加一个ShadowMin对0的地方加权一下最后在col加起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Properties
{
    _ShadowStep (&quot;Shadow step value&quot;, Range(0, 1)) = 0.1
    _ShadowMin (&quot;Minimum shadow value&quot;, Range(0, 1)) = 0.2
}

...

Varyings vert(Attributes IN)
{}

float _ShadowStep, _ShadowMin;

float4 frag (Varyings IN) : SV_Target
{
    float4 shadowCoord = TransformWorldToShadowCoord(IN.posWS);
    float shadowMap = MainLightRealtimeShadow(shadowCoord);

    float NdotL = saturate(dot(_MainLightPosition.xyz, IN.normalWS));

    float combinedShadow = min(NdotL, shadowMap);
    float shadowValue = saturate( combinedShadow + _ShadowMin);
    //float shadowValue = saturate(step(_ShadowStep, combinedShadow) + _ShadowMin);
    //对于卡通着色器可以用step钳制后加上min，造成明显的二分效果

    float3 col = float3(1, 1, 1) * shadowValue;

    return float4(col, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;渲染之后：对阴影贴图进行采样、Sobel算子&lt;/h3&gt;
&lt;p&gt;渲染的每个片段都会对阴影贴图进行采样。还有最终隐藏在稍后绘制的其他对象后面的片段。因此，这些碎片最终可能会接收到最终隐藏它们的物体的阴影。Framedebugger也可以看到。还可以看到阴影出现在实际投射阴影的对象之前。当然，这些错误仅在渲染帧时才会显现出来。完成后，图像是正确的。&lt;/p&gt;
&lt;p&gt;说人话就是：在处理阴影贴图（Shadow Maps）时。它指出了一个潜在的问题：&lt;strong&gt;即使是在最终图像中看不到的片段（fragments），也会进行阴影贴图的采样。这可能导致阴影先出现，物体之后才被渲染，尽管这些异常只会在帧渲染的过程中短暂出现，最终输出的图像仍然是正确的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;渲染引擎通常按照一定的顺序绘制对象，&lt;strong&gt;以深度测试为基底的绘制顺序会先绘制背景或远处的对象，然后逐步绘制前景或近处的对象&lt;/strong&gt;。然而，阴影计算通常是基于光源视角下的场景深度信息，而不是摄像机视角下的深度信息。因此有可能造成阴影已经被渲染出来，而被遮挡出来的物体没有被渲染。不过只会在非常短的时间出现，总体的场景仍然是正确的。&lt;/p&gt;
&lt;p&gt;最后的Sobel算子则是进行边缘检测，通过对周边像素的加权得到一个像素的变化趋势。想要做阴影的检测可以用（至于人物我觉得还是法线外扩和后处理居多吧）&lt;a href=&quot;https://zhuanlan.zhihu.com/p/40491339&quot;&gt;https://zhuanlan.zhihu.com/p/40491339&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;常见的阴影问题&lt;/h3&gt;
&lt;h4&gt;shadow swimming&lt;/h4&gt;
&lt;p&gt;指的是当物体、阴影或摄像机移动时，阴影会出现不稳定、抖动或“游动”的现象。这种现象主要与实时阴影的计算方式以及深度贴图的精度限制有关。还记得我们说的第二个标题吗？引擎会生成一张从光源视角出发的深度贴图（Shadow Map）来确定场景中哪些像素被遮挡。如果摄像机或者灯光移动，由于shadowmap产生的精度有可能不够高，因此我们看出来计算的阴影就是具有“锯齿”的，并且随着摄像机/灯光的移动而不断抖动。&lt;/p&gt;
&lt;p&gt;此外，如果使用级联阴影贴图（CSM）技术，这种技术类似于mipmap技术，根据距离远近切换成为不同精度的阴影。阴影会根据摄像机距离被分割成多个级别。当摄像机移动时，级联边界可能会导致阴影在不同分级之间切换。&lt;/p&gt;
&lt;h4&gt;Shadow Acne&lt;/h4&gt;
&lt;p&gt;实时渲染中另一种常见的阴影伪影问题。它通常表现为物体表面上出现密集的、锯齿状的暗斑或条纹，看起来像一条一条的横白条纹。这种现象主要是由于深度贴图的精度不足或深度值计算误差引起的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/e715a547.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;产生Shadow acne的根本原因是**深度值的自相交（Self-Shadowing）**问题。在生成深度贴图时，渲染引擎会从光源视角计算场景中每个像素的深度值（即距离光源的距离）， 再从摄像机视角再将场景中的像素投影到深度贴图中，比较像素的深度值与深度贴图中的值来判断是否被遮挡。  但是阴影图并不是矢量，精度也不是无限的；当渲染分辨率和阴影深度分辨率出现了一定程度的精度误差，就会错误的认为像素被遮挡，进而产生错误的阴影。&lt;/p&gt;
&lt;p&gt;还有一个就是&lt;strong&gt;光源的角度也会影响&lt;/strong&gt;Acne的严重与否。如上所示，当光投射角度越接近于物体表面的法线，Acne的出现概率就会越高。所以Unity提供了优化的选项：调整 Unity 中的 Bias 和 Normal Bias 参数，可以有效避免深度值冲突问题）。但是调太高会出现没有阴影的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/dd23abf3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Anti-Aliasing（抗锯齿）&lt;/h4&gt;
&lt;p&gt;由于屏幕分辨率的限制，有些高对比度的边缘会显得不平滑，呈现出“锯齿状”或者“阶梯状”的效果。 所以需要抗锯齿。抗锯齿的核心思想是通过平滑颜色过渡来减少锯齿现象。&lt;/p&gt;
&lt;p&gt;常见的有SSAA（超采样抗锯齿）、FXAA（快速近似抗锯齿）、SMAA（多重采样抗锯齿）、TAA（时域采样抗锯齿）、DLAA/DLSS（深度学习抗锯齿，需要硬件实现）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/opus/583246319595473632&quot;&gt;从零开始分解UnityShader阴影，从AutoLight到PCF，从百草园杀到三味书屋 - 哔哩哔哩&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/KillerAery/p/15201310.html&quot;&gt;实时阴影技术（1）Shadow Mapping - KillerAery - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/zhyan8/p/17489694.html&quot;&gt;【Unity3D】阴影原理及应用 - little_fat_sheep - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/n5/article/details/115617598&quot;&gt;关于ShadowMap中Shadow acne现象的解释-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kelvinvanhoorn.com/2021/11/02/shadow-outlines-tutorial/#shadow-map-sampling&quot;&gt;Shadow outlines tutorial&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>quixel导入blender的bl插件以及中文化适配&amp;bug修改</title><link>https://fuwari.vercel.app/posts/quixel%E5%AF%BC%E5%85%A5blender%E7%9A%84bl%E6%8F%92%E4%BB%B6%E4%BB%A5%E5%8F%8A%E4%B8%AD%E6%96%87%E5%8C%96%E9%80%82%E9%85%8Dbug%E4%BF%AE%E6%94%B9/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/quixel%E5%AF%BC%E5%85%A5blender%E7%9A%84bl%E6%8F%92%E4%BB%B6%E4%BB%A5%E5%8F%8A%E4%B8%AD%E6%96%87%E5%8C%96%E9%80%82%E9%85%8Dbug%E4%BF%AE%E6%94%B9/</guid><description>Blender模型导入Unity的完整工作流程</description><pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Quixel是来自Epic的一个模型资产库，其中的Bridge可以将资产库下载到本地并且进行个性化导出，如Blender、3dsmax、unity等等。但是遗憾的是blender在2.8版本后就不再支持。&lt;/p&gt;
&lt;p&gt;不过Quixel将他们的导入blender插件进行了开源，有人将这个项目进行了fork并做修改。主要是适配了新版blender部分api和节点名字的变化。但是在导入的时候我们会发现导入后的物体大小放大了100倍，而且此时的旋转出现了绕x轴旋转了-90°。（其实旋转是fbx的通病，因为fbx和blender默认的坐标轴的定义不一致出现的错误，主要是着手修改放大问题）&lt;/p&gt;
&lt;p&gt;所以我们找到blender的插件地址进行修改。地址在&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&quot;C:\Users\Danny\AppData\Roaming\Blender Foundation\Blender\4.2\scripts\addons\NodePreview\__init__.py&quot;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;修改一：将Principled BSDF改为”原理化 BSDF“&lt;/h2&gt;
&lt;p&gt;位于代码的&lt;strong&gt;340&lt;/strong&gt;行。这是节点生成器的一部分，由于blender更改语言会导致节点api名称也发生变化，因此我们将&lt;/p&gt;
&lt;p&gt;&lt;code&gt; self.parentName = &quot;Principled BSDF&quot;&lt;/code&gt;修改为&lt;/p&gt;
&lt;p&gt;&lt;code&gt; self.parentName = &quot;原理化 BSDF&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;修改二：增加导入后的缩放锁定&amp;amp;旋转适配&lt;/h2&gt;
&lt;p&gt;位于代码的&lt;strong&gt;198&lt;/strong&gt;行。导入fbx后我们需要对缩放进行调整。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if meshFormat.lower() == &quot;fbx&quot;:
                        bpy.ops.import_scene.fbx(filepath=meshPath)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们要修改成为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if meshFormat.lower() == &quot;fbx&quot;:
                        bpy.ops.import_scene.fbx(filepath=meshPath,global_scale=0.1,axis_forward=&apos;Z&apos;,axis_up=&apos;Y&apos;)
                        # 缩放和旋转调整
                        imported_objects = [obj for obj in bpy.context.selected_objects if obj.type == &apos;MESH&apos;]
                        for obj in imported_objects:
                            obj.scale = (0.01, 0.01, 0.01)  # 将模型缩小100倍
                            obj.rotation_euler[0] += 1.5708  # 绕X轴旋转 90 度 (1.5708弧度 ≈ 90度)

                        # get selected objects
                        obj_objects = [ o for o in bpy.context.scene.objects if o.select_get() ]
                        self.selectedObjects += obj_objects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对bpy.ops.import_scene.fbx(filepath=meshPath,global_scale=0.1,axis_forward=&apos;Z&apos;,axis_up=&apos;Y&apos;)之后进行了缩放调整和绕x轴旋转90°的调整。&lt;/p&gt;
&lt;p&gt;最后附上修改后的源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ##### QUIXEL AB - MEGASCANS PLugin FOR BLENDER #####
#
# The Megascans Plugin  plugin for Blender is an add-on that lets
# you instantly import assets with their shader setup with one click only.
#
# Because it relies on some of the latest 2.80 features, this plugin is currently
# only available for Blender 2.80 and forward.
#
# You are free to modify, add features or tweak this add-on as you see fit, and
# don&apos;t hesitate to send us some feedback if you&apos;ve done something cool with it.
#
# ##### QUIXEL AB - MEGASCANS PLUGIN FOR BLENDER #####

import bpy, threading, os, time, json, socket
from bpy.app.handlers import persistent

globals()[&apos;Megascans_DataSet&apos;] = None

# This stuff is for the Alembic support
globals()[&apos;MG_Material&apos;] = []
globals()[&apos;MG_AlembicPath&apos;] = []
globals()[&apos;MG_ImportComplete&apos;] = False


bl_info = {
    &quot;name&quot;: &quot;Megascans Plugin Fork&quot;,
    &quot;description&quot;: &quot;Connects Blender to Quixel Bridge for one-click imports with shader setup and geometry&quot;,
    &quot;author&quot;: &quot;Quixel \ Sören Schmidt-Clausen&quot;,
    &quot;version&quot;: (3, 7, 0),
    &quot;blender&quot;: (3, 4, 0),
    &quot;location&quot;: &quot;File &amp;gt; Import&quot;,
    &quot;warning&quot;: &quot;&quot;, # used for warning icon and text in addons panel
    &quot;wiki_url&quot;: &quot;https://docs.quixel.org/bridge/livelinks/blender/info_quickstart.html&quot;,
    &quot;tracker_url&quot;: &quot;https://docs.quixel.org/bridge/livelinks/blender/info_quickstart#release_notes&quot;,
    &quot;support&quot;: &quot;COMMUNITY&quot;,
    &quot;category&quot;: &quot;Import-Export&quot;
}


# MS_Init_ImportProcess is the main asset import class.
# This class is invoked whenever a new asset is set from Bridge.

class MS_Init_ImportProcess():

    # This initialization method create the data structure to process our assets
    # later on in the initImportProcess method. The method loops on all assets
    # that have been sent by Bridge.
    def __init__(self):
        print(&quot;Initialized import class...&quot;)
        try:
            # Check if there&apos;s any incoming data
            if globals()[&apos;Megascans_DataSet&apos;] != None:

                globals()[&apos;MG_AlembicPath&apos;] = []
                globals()[&apos;MG_Material&apos;] = []
                globals()[&apos;MG_ImportComplete&apos;] = False

                self.json_Array = json.loads(globals()[&apos;Megascans_DataSet&apos;])

                # Start looping over each asset in the self.json_Array list
                for js in self.json_Array:

                    self.json_data = js

                

                    self.selectedObjects = []
                    
                    self.IOR = 1.45
                    self.assetType = self.json_data[&quot;type&quot;]
                    self.assetPath = self.json_data[&quot;path&quot;]
                    self.assetID = self.json_data[&quot;id&quot;]
                    self.isMetal = bool(self.json_data[&quot;category&quot;] == &quot;Metal&quot;)
                    # Workflow setup.
                    self.isHighPoly = bool(self.json_data[&quot;activeLOD&quot;] == &quot;high&quot;)
                    self.activeLOD = self.json_data[&quot;activeLOD&quot;]
                    self.minLOD = self.json_data[&quot;minLOD&quot;]
                    self.RenderEngine = bpy.context.scene.render.engine.lower() # Get the current render engine. i.e. blender_eevee or cycles
                    self.Workflow = self.json_data.get(&apos;pbrWorkflow&apos;, &apos;specular&apos;)
                    self.DisplacementSetup = &apos;regular&apos;
                    self.isCycles = bool(self.RenderEngine == &apos;cycles&apos;)
                    self.isScatterAsset = self.CheckScatterAsset()
                    self.textureList = []
                    self.isBillboard = self.CheckIsBillboard()
                    self.ApplyToSelection = False
                    self.isSpecularWorkflow = True
                    self.isAlembic = False

                    self.NormalSetup = False
                    self.BumpSetup = False

                    if &quot;workflow&quot; in self.json_data.keys():
                        self.isSpecularWorkflow = bool(self.json_data[&quot;workflow&quot;] == &quot;specular&quot;)

                    if &quot;applyToSelection&quot; in self.json_data.keys():
                        self.ApplyToSelection = bool(self.json_data[&quot;applyToSelection&quot;])

                    if (self.isCycles):
                        if(bpy.context.scene.cycles.feature_set == &apos;EXPERIMENTAL&apos;):
                            self.DisplacementSetup = &apos;adaptive&apos;
                    
                    texturesListName = &quot;components&quot;
                    if(self.isBillboard):
                        texturesListName = &quot;components&quot;

                    # Get a list of all available texture maps. item[1] returns the map type (albedo, normal, etc...).
                    self.textureTypes = [obj[&quot;type&quot;] for obj in self.json_data[texturesListName]]
                    self.textureList = []

                    for obj in self.json_data[texturesListName]:
                        texFormat = obj[&quot;format&quot;]
                        texType = obj[&quot;type&quot;]
                        texPath = obj[&quot;path&quot;]

                        if texType == &quot;displacement&quot; and texFormat != &quot;exr&quot;:
                            texDir = os.path.dirname(texPath)
                            texName = os.path.splitext(os.path.basename(texPath))[0]

                            if os.path.exists(os.path.join(texDir, texName + &quot;.exr&quot;)):
                                texPath = os.path.join(texDir, texName + &quot;.exr&quot;)
                                texFormat = &quot;exr&quot;
                        # Replace diffuse texture type with albedo so we don&apos;t have to add more conditions to handle diffuse map.
                        if texType == &quot;diffuse&quot; and &quot;albedo&quot; not in self.textureTypes:
                            texType = &quot;albedo&quot;
                            self.textureTypes.append(&quot;albedo&quot;)
                            self.textureTypes.remove(&quot;diffuse&quot;)

                        # Normal / Bump setup checks
                        if texType == &quot;normal&quot;:
                            self.NormalSetup = True
                        if texType == &quot;bump&quot;:
                            self.BumpSetup = True

                        self.textureList.append((texFormat, texType, texPath))

                    # Create a tuple list of all the 3d meshes  available.
                    # This tuple is composed of (meshFormat, meshPath)
                    self.geometryList = [(obj[&quot;format&quot;], obj[&quot;path&quot;]) for obj in self.json_data[&quot;meshList&quot;]]

                    # Create name of our asset. Multiple conditions are set here
                    # in order to make sure the asset actually has a name and that the name
                    # is short enough for us to use it. We compose a name with the ID otherwise.
                    if &quot;name&quot; in self.json_data.keys():
                        self.assetName = self.json_data[&quot;name&quot;].replace(&quot; &quot;, &quot;_&quot;)
                    else:
                        self.assetName = os.path.basename(self.json_data[&quot;path&quot;]).replace(&quot; &quot;, &quot;_&quot;)
                    if len(self.assetName.split(&quot;_&quot;)) &amp;gt; 2:
                        self.assetName = &quot;_&quot;.join(self.assetName.split(&quot;_&quot;)[:-1])

                    self.materialName = self.assetName + &apos;_&apos; + self.assetID
                    self.colorSpaces = [&quot;sRGB&quot;, &quot;Non-Color&quot;, &quot;Linear&quot;]

                    # Initialize the import method to start building our shader and import our geometry
                    self.initImportProcess()
                    print(&quot;Imported asset from &quot; + self.assetName + &quot; Quixel Bridge&quot;)
        
            if len(globals()[&apos;MG_AlembicPath&apos;]) &amp;gt; 0:
                globals()[&apos;MG_ImportComplete&apos;] = True        
        except Exception as e:
            print( &quot;Megascans Plugin Error initializing the import process. Error: &quot;, str(e) )
        
        globals()[&apos;Megascans_DataSet&apos;] = None
    
    # this method is used to import the geometry and create the material setup.
    def initImportProcess(self):
        try:
            if len(self.textureList) &amp;gt;= 1:
                
                if(self.ApplyToSelection and self.assetType not in [&quot;3dplant&quot;, &quot;3d&quot;]):
                    self.CollectSelectedObjects()

                self.ImportGeometry()
                self.CreateMaterial()
                self.ApplyMaterialToGeometry()
                if(self.isScatterAsset and len(self.selectedObjects) &amp;gt; 1):
                    self.ScatterAssetSetup()
                elif (self.assetType == &quot;3dplant&quot; and len(self.selectedObjects) &amp;gt; 1):
                    self.PlantAssetSetup()

                self.SetupMaterial()

                if self.isAlembic:
                    globals()[&apos;MG_Material&apos;].append(self.mat)

        except Exception as e:
            print( &quot;Megascans Plugin Error while importing textures/geometry or setting up material. Error: &quot;, str(e) )

    def ImportGeometry(self):
        try:
            # Import geometry
            abcPaths = []
            if len(self.geometryList) &amp;gt;= 1:
                for obj in self.geometryList:
                    meshPath = obj[1]
                    meshFormat = obj[0]

                    if meshFormat.lower() == &quot;fbx&quot;:
                        bpy.ops.import_scene.fbx(filepath=meshPath,global_scale=0.1,axis_forward=&apos;Z&apos;,axis_up=&apos;Y&apos;)
                        # 缩放和旋转调整
                        imported_objects = [obj for obj in bpy.context.selected_objects if obj.type == &apos;MESH&apos;]
                        for obj in imported_objects:
                            obj.scale = (0.01, 0.01, 0.01)  # 将模型缩小100倍
                            obj.rotation_euler[0] += 1.5708  # 绕X轴旋转 90 度 (1.5708弧度 ≈ 90度)

                        # get selected objects
                        obj_objects = [ o for o in bpy.context.scene.objects if o.select_get() ]
                        self.selectedObjects += obj_objects

                    elif meshFormat.lower() == &quot;obj&quot;:
                        if bpy.app.version &amp;lt; (2, 92, 0):
                            bpy.ops.import_scene.obj(filepath=meshPath, use_split_objects = True, use_split_groups = True, global_clight_size = 1.0)
                        else:
                            bpy.ops.import_scene.obj(filepath=meshPath, use_split_objects = True, use_split_groups = True, global_clamp_size  = 1.0)
                        # get selected objects
                        obj_objects = [ o for o in bpy.context.scene.objects if o.select_get() ]
                        self.selectedObjects += obj_objects

                    elif meshFormat.lower() == &quot;abc&quot;:
                        self.isAlembic = True
                        abcPaths.append(meshPath)
            
            if self.isAlembic:
                globals()[&apos;MG_AlembicPath&apos;].append(abcPaths)
        except Exception as e:
            print( &quot;Megascans Plugin Error while importing textures/geometry or setting up material. Error: &quot;, str(e) )

    def dump(self, obj):
        for attr in dir(obj):
            print(&quot;obj.%s = %r&quot; % (attr, getattr(obj, attr)))

    def CollectSelectedObjects(self):
        try:
            sceneSelectedObjects = [ o for o in bpy.context.scene.objects if o.select_get() ]
            for obj in sceneSelectedObjects:
                if obj.type == &quot;MESH&quot;:
                    self.selectedObjects.append(obj)
        except Exception as e:
            print(&quot;Megascans Plugin Error::CollectSelectedObjects::&quot;, str(e) )

    def ApplyMaterialToGeometry(self):
        for obj in self.selectedObjects:
            # assign material to obj
            obj.active_material = self.mat

    def CheckScatterAsset(self):
        if(&apos;scatter&apos; in self.json_data[&apos;categories&apos;] or &apos;scatter&apos; in self.json_data[&apos;tags&apos;] or &apos;cmb_asset&apos; in self.json_data[&apos;categories&apos;] or &apos;cmb_asset&apos; in self.json_data[&apos;tags&apos;]):
            return True
        return False

    def CheckIsBillboard(self):
        # Use billboard textures if importing the Billboard LOD.
        if(self.assetType == &quot;3dplant&quot;):
            if (self.activeLOD == self.minLOD):
                return True
        return False

    #Add empty parent for the scatter assets.
    def ScatterAssetSetup(self):
        bpy.ops.object.empty_add(type=&apos;ARROWS&apos;)
        emptyRefList = [ o for o in bpy.context.scene.objects if o.select_get() and o not in self.selectedObjects ]
        for scatterParentObject in emptyRefList:
            scatterParentObject.name = self.assetID + &quot;_&quot; + self.assetName
            for obj in self.selectedObjects:
                obj.parent = scatterParentObject
            break
    
    #Add empty parent for plants.
    def PlantAssetSetup(self):
        bpy.ops.object.empty_add(type=&apos;ARROWS&apos;)
        emptyRefList = [ o for o in bpy.context.scene.objects if o.select_get() and o not in self.selectedObjects ]
        for plantParentObject in emptyRefList:
            plantParentObject.name = self.assetID + &quot;_&quot; + self.assetName
            for obj in self.selectedObjects:
                obj.parent = plantParentObject
            break

    # def AddModifiersToGeomtry(self, geo_list, mat):
    #     for obj in geo_list:
    #         # assign material to obj
    #         bpy.ops.object.modifier_add(type=&apos;SOLIDIFY&apos;)

    #Shader setups for all asset types. Some type specific functionality is also handled here.
    def SetupMaterial (self):
        if &quot;albedo&quot; in self.textureTypes:
            if &quot;ao&quot; in self.textureTypes:
                self.CreateTextureMultiplyNode(&quot;albedo&quot;, &quot;ao&quot;, -250, 320, -640, 460, -640, 200, 0, 1, True, &quot;Base Color&quot;)
            else:
                self.CreateTextureNode(&quot;albedo&quot;, -640, 420, 0, True, &quot;Base Color&quot;)
        
        if self.isSpecularWorkflow:
            if &quot;specular&quot; in self.textureTypes:
                self.CreateTextureNode(&quot;specular&quot;, -1150, 200, 0, True, &quot;Specular&quot;)
            
            if &quot;gloss&quot; in self.textureTypes:
                glossNode = self.CreateTextureNode(&quot;gloss&quot;, -1150, -60)
                invertNode = self.CreateGenericNode(&quot;ShaderNodeInvert&quot;, -250, 60)
                # Add glossNode to invertNode connection
                self.node_group.links.new(invertNode.inputs[&quot;Color&quot;], glossNode.outputs[&quot;Color&quot;])
                # Connect roughness node to the material parent node.
                self.ConnectNodeToMaterial(&quot;Roughness&quot;, invertNode)
            elif &quot;roughness&quot; in self.textureTypes:
                self.CreateTextureNode(&quot;roughness&quot;, -1150, -60, 1, True, &quot;Roughness&quot;)
        else:
            if &quot;metalness&quot; in self.textureTypes:
                self.CreateTextureNode(&quot;metalness&quot;, -1150, 200, 1, True, &quot;Metallic&quot;)
            
            if &quot;roughness&quot; in self.textureTypes:
                self.CreateTextureNode(&quot;roughness&quot;, -1150, -60, 1, True, &quot;Roughness&quot;)
            elif &quot;gloss&quot; in self.textureTypes:
                glossNode = self.CreateTextureNode(&quot;gloss&quot;, -1150, -60)
                invertNode = self.CreateGenericNode(&quot;ShaderNodeInvert&quot;, -250, 60)
                # Add glossNode to invertNode connection
                self.node_group.links.new(invertNode.inputs[&quot;Color&quot;], glossNode.outputs[&quot;Color&quot;])
                # Connect roughness node to the material parent node.
                self.node_group.links.new(self.nodes.get(self.parentName).inputs[&quot;Roughness&quot;], invertNode.outputs[&quot;Color&quot;])
                self.ConnectNodeToMaterial(&quot;Roughness&quot;, invertNode)
            
        if &quot;opacity&quot; in self.textureTypes:
            self.CreateTextureNode(&quot;opacity&quot;, -1550, -160, 1, True, &quot;Alpha&quot;)
            self.mat.blend_method = &apos;HASHED&apos;

        if &quot;translucency&quot; in self.textureTypes:
            self.CreateTextureNode(&quot;translucency&quot;, -1550, -420, 0, True, &quot;Transmission&quot;)
        elif &quot;transmission&quot; in self.textureTypes:
            self.CreateTextureNode(&quot;transmission&quot;, -1550, -420, 1, True, &quot;Transmission&quot;)

        # If HIGH POLY selected &amp;gt; use normal_bump and no displacement
        # If LODs selected &amp;gt; use corresponding LODs normal + displacement
        if self.isHighPoly:
            self.BumpSetup = False
        self.CreateNormalNodeSetup(True, &quot;Normal&quot;)

        if &quot;displacement&quot; in self.textureTypes and not self.isHighPoly:
            self.CreateDisplacementSetup(True)

    def CreateMaterial(self):
        self.mat = (bpy.data.materials.get( self.materialName ) or bpy.data.materials.new( self.materialName ))
        self.mat.use_nodes = True
        self.nodes = self.mat.node_tree.nodes
        self.parentName = &quot;原理化 BSDF&quot;
        self.materialOutputName = &quot;Material Output&quot;

        self.mat.node_tree.nodes[self.parentName].distribution = &apos;MULTI_GGX&apos;
        #self.mat.node_tree.nodes[self.parentName].inputs[&quot;Metallic&quot;].default_value = 1 if self.isMetal else 0 # Metallic value
        #self.mat.node_tree.nodes[self.parentName].inputs[&quot;IOR&quot;].default_value = self.IOR
        
        #self.mat.node_tree.nodes[self.parentName].inputs[&quot;Specular&quot;].default_value = 0 Macht kein Sinn! Sieht mit Specular besser aus.
        #self.mat.node_tree.nodes[self.parentName].inputs[&quot;Clearcoat&quot;].default_value = 0

        
        #Create Node Group
        self.node_group = bpy.data.node_groups.new(name=self.assetType + &quot;_&quot; + self.materialName, type=&quot;ShaderNodeTree&quot;)

        #Create Input
        self.node_group_in = self.node_group.nodes.new(&quot;NodeGroupInput&quot;)
        self.node_group_in.location = (-1500, 0)
        self.node_group.interface.new_socket(name=&quot;NodeSocketVector&quot;, socket_type=&quot;NodeSocketVector&quot;)

        #Create Output
        self.node_group_out = self.node_group.nodes.new(&quot;NodeGroupOutput&quot;)
        self.node_group_out.location = (500, 0)
        #Outputs are assigned later
        
        
        #Instance it in node Tree
        self.node_group_inst = self.mat.node_tree.nodes.new(&quot;ShaderNodeGroup&quot;)
        self.node_group_inst.node_tree = self.node_group
        self.node_group_inst.location = (-600, 297)
    
        
        self.mappingNode = None

        #Hehe do it anyway
        #if self.isCycles and self.assetType not in [&quot;3d&quot;, &quot;3dplant&quot;]:
        # Create mapping node.
        self.mappingNode = self.mat.node_tree.nodes.new(&quot;ShaderNodeMapping&quot;)
        self.mappingNode.location = (-1000, 389)
        self.mappingNode.vector_type = &apos;TEXTURE&apos;
        # Create texture coordinate node.
        texCoordNode = self.mat.node_tree.nodes.new(&quot;ShaderNodeTexCoord&quot;)
        texCoordNode.location = (-1200, 389)
        # Connect texCoordNode to the mappingNode
        self.mat.node_tree.links.new(self.mappingNode.inputs[&quot;Vector&quot;], texCoordNode.outputs[&quot;UV&quot;])

        #Connect mapping node to Group Input
        self.mat.node_tree.links.new(self.mappingNode.outputs[&quot;Vector&quot;], self.node_group_inst.inputs[0])

    def CreateTextureNode(self, textureType, PosX, PosY, colorspace = 1, connectToMaterial = False, materialInputIndex = &quot;&quot;):
        texturePath = self.GetTexturePath(textureType)
        textureNode = self.CreateGenericNode(&apos;ShaderNodeTexImage&apos;, PosX, PosY)
        textureNode.image = bpy.data.images.load(texturePath)
        textureNode.show_texture = True
        textureNode.image.colorspace_settings.name = self.colorSpaces[colorspace] # &quot;sRGB&quot;, &quot;Non-Color&quot;, &quot;Linear&quot;
        
        if textureType in [&quot;albedo&quot;, &quot;specular&quot;, &quot;translucency&quot;]:
            if self.GetTextureFormat(textureType) in &quot;exr&quot;:
                textureNode.image.colorspace_settings.name = self.colorSpaces[2] # &quot;sRGB&quot;, &quot;Non-Color&quot;, &quot;Linear&quot;

        if connectToMaterial:
            self.ConnectNodeToMaterial(materialInputIndex, textureNode)

        #Connect Uvs to Vector unput
        self.node_group.links.new(textureNode.inputs[&quot;Vector&quot;], self.node_group_in.outputs[0])

        return textureNode

    def CreateTextureMultiplyNode(self, aTextureType, bTextureType, PosX, PosY, aPosX, aPosY, bPosX, bPosY, aColorspace, bColorspace, connectToMaterial, materialInputIndex):
        #Add Color&amp;gt;MixRGB node, transform it in the node editor, change it&apos;s operation to Multiply and finally we colapse the node.
        multiplyNode = self.CreateGenericNode(&apos;ShaderNodeMixRGB&apos;, PosX, PosY)
        multiplyNode.blend_type = &apos;MULTIPLY&apos;
        #Setup A and B nodes
        textureNodeA = self.CreateTextureNode(aTextureType, aPosX, aPosY, aColorspace)
        textureNodeB = self.CreateTextureNode(bTextureType, bPosX, bPosY, bColorspace)
        # Conned albedo and ao node to the multiply node.
        self.node_group.links.new(multiplyNode.inputs[&quot;Color1&quot;], textureNodeA.outputs[&quot;Color&quot;])
        self.node_group.links.new(multiplyNode.inputs[&quot;Color2&quot;], textureNodeB.outputs[&quot;Color&quot;])

        if connectToMaterial:
            self.ConnectNodeToMaterial(materialInputIndex, multiplyNode)

        return multiplyNode

    def CreateNormalNodeSetup(self, connectToMaterial, materialInputIndex):
        
        bumpNode = None
        normalNode = None
        bumpMapNode = None
        normalMapNode = None

        if self.NormalSetup and self.BumpSetup:
            bumpMapNode = self.CreateTextureNode(&quot;bump&quot;, -640, -130)
            normalMapNode = self.CreateTextureNode(&quot;normal&quot;, -1150, -580)
            bumpNode = self.CreateGenericNode(&quot;ShaderNodeBump&quot;, -250, -170)
            bumpNode.inputs[&quot;Strength&quot;].default_value = 0.1
            normalNode = self.CreateGenericNode(&quot;ShaderNodeNormalMap&quot;, -640, -400)
            # Add normalMapNode to normalNode connection
            self.node_group.links.new(normalNode.inputs[&quot;Color&quot;], normalMapNode.outputs[&quot;Color&quot;])
            # Add bumpMapNode and normalNode connection to the bumpNode
            self.node_group.links.new(bumpNode.inputs[&quot;Height&quot;], bumpMapNode.outputs[&quot;Color&quot;])
            if (2, 81, 0) &amp;gt; bpy.app.version:
                self.node_group.links.new(bumpNode.inputs[&quot;Normal&quot;], normalNode.outputs[&quot;Normal&quot;])
            else:
                self.node_group.links.new(bumpNode.inputs[&quot;Normal&quot;], normalNode.outputs[&quot;Normal&quot;])
            # Add bumpNode connection to the material parent node
            if connectToMaterial:
                self.ConnectNodeToMaterial(materialInputIndex, bumpNode)
        elif self.NormalSetup:
            normalMapNode = self.CreateTextureNode(&quot;normal&quot;, -640, -207)
            normalNode = self.CreateGenericNode(&quot;ShaderNodeNormalMap&quot;, -250, -170)
            # Add normalMapNode to normalNode connection
            self.node_group.links.new(normalNode.inputs[&quot;Color&quot;], normalMapNode.outputs[&quot;Color&quot;])
            # Add normalNode connection to the material parent node
            if connectToMaterial:
                self.ConnectNodeToMaterial(materialInputIndex, normalNode)
        elif self.BumpSetup:
            bumpMapNode = self.CreateTextureNode(&quot;bump&quot;, -640, -207)
            bumpNode = self.CreateGenericNode(&quot;ShaderNodeBump&quot;, -250, -170)
            bumpNode.inputs[&quot;Strength&quot;].default_value = 0.1
            # Add bumpMapNode and normalNode connection to the bumpNode
            self.node_group.links.new(bumpNode.inputs[&quot;Height&quot;], bumpMapNode.outputs[&quot;Color&quot;])
            # Add bumpNode connection to the material parent node
            if connectToMaterial:
                self.ConnectNodeToMaterial(materialInputIndex, bumpNode)

    def CreateDisplacementSetup(self, connectToMaterial):
        #Achtung könnte was kaputt machen was ich hier gemnacht hab SOEREN
        displacementMapNode = self.CreateTextureNode(&quot;displacement&quot;, -640, -740)
        self.JustConnectToGroupOutput(displacementMapNode, &quot;NodeSocketFloat&quot;, &quot;Height&quot;)

    def JustConnectToGroupOutput(self, textureNode, socket_type, name):
        #Create the output for the node_group:
        output = self.node_group.interface.new_socket(name=name, in_out=&quot;OUTPUT&quot;, socket_type=socket_type)

        #Connect to node group
        self.node_group.links.new(textureNode.outputs[0], self.node_group_out.inputs[name])

    def ConnectNodeToMaterial(self, materialInputIndex, textureNode):
        #Get the input in the Principled BSDF:
        #Name for the Material
        bsdf_in = self.nodes.get(self.parentName).inputs[materialInputIndex] 

        # Remove Factor from ID, I actually have no idea why its even standing there
        node_socket_id = bsdf_in.bl_idname.replace(&quot;Factor&quot;, &quot;&quot;)

        self.JustConnectToGroupOutput(textureNode, node_socket_id, bsdf_in.name)

        #Note this doesnt work for duplicate names, but this shouldn&apos;t happen here
        self.mat.node_tree.links.new(self.node_group_inst.outputs[bsdf_in.name], bsdf_in)

    def CreateGenericNode(self, nodeName, PosX, PosY):
        genericNode = self.node_group.nodes.new(nodeName)
        genericNode.location = (PosX, PosY)
        return genericNode

    def GetTexturePath(self, textureType):
        for item in self.textureList:
            if item[1] == textureType:
                path = item[2]

                path = path.replace(&quot;\\\\&quot;, &quot;*&quot;)
                path = path.replace(&quot;\\&quot;, &quot;/&quot;)
                path = path.replace(&quot;*&quot;, &quot;\\\\&quot;)

                print(path)
                return path

    def GetTextureFormat(self, textureType):
        for item in self.textureList:
            if item[1] == textureType:
                return item[0].lower()

class ms_Init(threading.Thread):
    
	#Initialize the thread and assign the method (i.e. importer) to be called when it receives JSON data.
    def __init__(self, importer):
        threading.Thread.__init__(self)
        self.importer = importer

	#Start the thread to start listing to the port.
    def run(self):
        try:
            run_livelink = True
            host, port = &apos;localhost&apos;, 28888
            #Making a socket object.
            socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            #Binding the socket to host and port number mentioned at the start.
            socket_.bind((host, port))

            #Run until the thread starts receiving data.
            while run_livelink:
                socket_.listen(5)
                #Accept connection request.
                client, addr = socket_.accept()
                data = &quot;&quot;
                buffer_size = 4096*2
                #Receive data from the client. 
                data = client.recv(buffer_size)
                if data == b&apos;Bye Megascans&apos;:
                    run_livelink = False
                    break

                #If any data is received over the port.
                if data != &quot;&quot;:
                    self.TotalData = b&quot;&quot;
                    self.TotalData += data #Append the previously received data to the Total Data.
                    #Keep running until the connection is open and we are receiving data.
                    while run_livelink:
                        #Keep receiving data from client.
                        data = client.recv(4096*2)
                        if data == b&apos;Bye Megascans&apos;:
                            run_livelink = False
                            break
                        #if we are getting data keep appending it to the Total data.
                        if data : self.TotalData += data
                        else:
                            #Once the data transmission is over call the importer method and send the collected TotalData.
                            self.importer(self.TotalData)
                            break
        except Exception as e:
            print( &quot;Megascans Plugin Error initializing the thread. Error: &quot;, str(e) )

class thread_checker(threading.Thread):
    
	#Initialize the thread and assign the method (i.e. importer) to be called when it receives JSON data.
    def __init__(self):
        threading.Thread.__init__(self)

	#Start the thread to start listing to the port.
    def run(self):
        try:
            run_checker = True
            while run_checker:
                time.sleep(3)
                for i in threading.enumerate():
                    if(i.getName() == &quot;MainThread&quot; and i.is_alive() == False):
                        host, port = &apos;localhost&apos;, 28888
                        s = socket.socket()
                        s.connect((host,port))
                        data = &quot;Bye Megascans&quot;
                        s.send(data.encode())
                        s.close()
                        run_checker = False
                        break
        except Exception as e:
            print( &quot;Megascans Plugin Error initializing thread checker. Error: &quot;, str(e) )
            pass

class MS_Init_LiveLink(bpy.types.Operator):

    bl_idname = &quot;bridge.plugin&quot;
    bl_label = &quot;Megascans Plugin&quot;
    socketCount = 0

    def execute(self, context):

        try:
            globals()[&apos;Megascans_DataSet&apos;] = None
            self.thread_ = threading.Thread(target = self.socketMonitor)
            self.thread_.start()
            bpy.app.timers.register(self.newDataMonitor)
            return {&apos;FINISHED&apos;}
        except Exception as e:
            print( &quot;Megascans Plugin Error starting blender plugin. Error: &quot;, str(e) )
            return {&quot;FAILED&quot;}

    def newDataMonitor(self):
        try:
            if globals()[&apos;Megascans_DataSet&apos;] != None:
                MS_Init_ImportProcess()
                globals()[&apos;Megascans_DataSet&apos;] = None       
        except Exception as e:
            print( &quot;Megascans Plugin Error starting blender plugin (newDataMonitor). Error: &quot;, str(e) )
            return {&quot;FAILED&quot;}
        return 1.0


    def socketMonitor(self):
        try:
            #Making a thread object
            threadedServer = ms_Init(self.importer)
            #Start the newly created thread.
            threadedServer.start()
            #Making a thread object
            thread_checker_ = thread_checker()
            #Start the newly created thread.
            thread_checker_.start()
        except Exception as e:
            print( &quot;Megascans Plugin Error starting blender plugin (socketMonitor). Error: &quot;, str(e) )
            return {&quot;FAILED&quot;}

    def importer (self, recv_data):
        try:
            globals()[&apos;Megascans_DataSet&apos;] = recv_data
        except Exception as e:
            print( &quot;Megascans Plugin Error starting blender plugin (importer). Error: &quot;, str(e) )
            return {&quot;FAILED&quot;}

class MS_Init_Abc(bpy.types.Operator):

    bl_idname = &quot;ms_livelink_abc.py&quot;
    bl_label = &quot;Import ABC&quot;

    def execute(self, context):

        try:
            if globals()[&apos;MG_ImportComplete&apos;]:
                
                assetMeshPaths = globals()[&apos;MG_AlembicPath&apos;]
                assetMaterials = globals()[&apos;MG_Material&apos;]
                
                if len(assetMeshPaths) &amp;gt; 0 and len(assetMaterials) &amp;gt; 0:

                    materialIndex = 0
                    old_materials = []
                    for meshPaths in assetMeshPaths:
                        for meshPath in meshPaths:
                            bpy.ops.wm.alembic_import(filepath=meshPath, as_background_job=False)
                            for o in bpy.context.scene.objects:
                                if o.select_get():
                                    old_materials.append(o.active_material)
                                    o.active_material = assetMaterials[materialIndex]
                                    
                        
                        materialIndex += 1
                    
                    for mat in old_materials:
                        try:
                            if mat is not None:
                                bpy.data.materials.remove(mat)
                        except:
                            pass

                    globals()[&apos;MG_AlembicPath&apos;] = []
                    globals()[&apos;MG_Material&apos;] = []
                    globals()[&apos;MG_ImportComplete&apos;] = False

            return {&apos;FINISHED&apos;}
        except Exception as e:
            print( &quot;Megascans Plugin Error starting MS_Init_Abc. Error: &quot;, str(e) )
            return {&quot;CANCELLED&quot;}

@persistent
def load_plugin(scene):
    try:
        bpy.ops.bridge.plugin()
    except Exception as e:
        print( &quot;Bridge Plugin Error::Could not start the plugin. Description: &quot;, str(e) )

def menu_func_import(self, context):
    self.layout.operator(MS_Init_Abc.bl_idname, text=&quot;Megascans: Import Alembic Files&quot;)

def register():
    if len(bpy.app.handlers.load_post) &amp;gt; 0:
        # Check if trying to register twice.
        if &quot;load_plugin&quot; in bpy.app.handlers.load_post[0].__name__.lower() or load_plugin in bpy.app.handlers.load_post:
            return
    bpy.utils.register_class(MS_Init_LiveLink)
    bpy.utils.register_class(MS_Init_Abc)
    bpy.app.handlers.load_post.append(load_plugin)
    bpy.types.TOPBAR_MT_file_import.append(menu_func_import)

def unregister():
    bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
    if len(bpy.app.handlers.load_post) &amp;gt; 0:
        # Check if trying to register twice.
        if &quot;load_plugin&quot; in bpy.app.handlers.load_post[0].__name__.lower() or load_plugin in bpy.app.handlers.load_post:
            bpy.app.handlers.load_post.remove(load_plugin)

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>菲涅尔计算式以及近似公式</title><link>https://fuwari.vercel.app/posts/%E8%8F%B2%E6%B6%85%E5%B0%94%E8%AE%A1%E7%AE%97%E5%BC%8F%E4%BB%A5%E5%8F%8A%E8%BF%91%E4%BC%BC%E5%85%AC%E5%BC%8F/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E8%8F%B2%E6%B6%85%E5%B0%94%E8%AE%A1%E7%AE%97%E5%BC%8F%E4%BB%A5%E5%8F%8A%E8%BF%91%E4%BC%BC%E5%85%AC%E5%BC%8F/</guid><description>菲涅尔计算式以及近似公式的原理解析和实现方法</description><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;比较有名的Schink近似式。本身的计算式并不难，就是描述光在两个介质中反射光强度和折射光强度的规律。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;菲涅尔方程（Fresnel equations），也称为菲涅尔条件，是描述光在两种不同介质界面处反射和折射行为的一组公式。这些方程由法国物理学家奥古斯丁·让·菲涅耳于19世纪初提出，主要用于计算入射光、反射光和折射光之间的振幅比。根据光的偏振状态，菲涅尔方程分为两种情况：平行偏振光（s偏振或TE模式）和垂直偏振光（p偏振或TM模式）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;菲涅尔方程的基本形式&lt;/h3&gt;
&lt;p&gt;对于从介质1（折射率为(n_1)）以角度(\theta_1)入射到介质2（折射率为(n_2)）的平面波，其反射率(R)和透射率(T)可以通过以下方程计算：&lt;/p&gt;
&lt;h4&gt;对于s偏振光：&lt;img src=&quot;/images/posts/8064f38a.svg&quot; alt=&quot;image&quot; /&gt;&lt;/h4&gt;
&lt;h4&gt;对于p偏振光：&lt;img src=&quot;/images/posts/0090ff12.svg&quot; alt=&quot;image&quot; /&gt;&lt;/h4&gt;
&lt;p&gt;其中，&lt;img src=&quot;/images/posts/69f9bb51.svg&quot; alt=&quot;image&quot; /&gt;是折射角，遵循斯涅尔定律（Snell&apos;s law）: &lt;img src=&quot;/images/posts/1e1e0bd0.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;反射率(R)可以由反射系数的模&lt;strong&gt;平方&lt;/strong&gt;给出，即R=|r|&amp;lt;sup&amp;gt;2&amp;lt;/sup&amp;gt;&lt;/p&gt;
&lt;h3&gt;什么是P光、什么是S光？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;P光和S光是针对光的入射反射和折射来说的，此时，将入射光与反射光/折射光所在的平面定义为参考面，S光和P光分别为偏振方向垂直以及平行于该参考面的线偏振光&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;举例而言，如下图所示，一束光照射到入射面上，形成反射和折射，将入射光束与法线所形成的平面定义为入射平面。P光（德文Parallel的首字母，意为平行的）是偏振方向与入射平面平行的光，而S光（德文Senkrecht的首字母，意为竖直的）是偏振方向与入射平面垂直的光。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/751bb8d4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Schlink近似&lt;/h2&gt;
&lt;p&gt;但是在图形学中，&lt;strong&gt;一般不会使用这种p光和s光的表达式&lt;/strong&gt;，用于估算光在介质界面发生反射时的反射系数的一种简化公式，特别适用于计算机图形学中的光线追踪算法。&lt;/p&gt;
&lt;p&gt;对于s偏振光和p偏振光，Schlick近似可以将p光和s光的反射率统一表达为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/10d23d41.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用代码可以表示为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vec3 fresnelSchlick(float cosTheta, vec3 R0)
{
    return R0 + (1.0 - R0) * pow(1.0 - cosTheta, 5.0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/f3548a27.svg&quot; alt=&quot;image&quot; /&gt;是在给定入射角_θ_下的反射率。&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/7431a2a6.svg&quot; alt=&quot;image&quot; /&gt;是当光垂直入射（即_θ_=0）时的反射率，可以通过两种介质的折射率n1和n2计算得出：&lt;img src=&quot;/images/posts/d64b7151.svg&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;li&gt;cos⁡θ是入射角的余弦值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;利用这个特点可以制作外发光。&lt;img src=&quot;/images/posts/e32807ac.png&quot; alt=&quot;不同介质的反射率参考&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;引用：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/375746359&quot;&gt;https://zhuanlan.zhihu.com/p/375746359&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Houdini：从Mantra切换为Karma的材质工作流切换</title><link>https://fuwari.vercel.app/posts/houdini%E4%BB%8Emantra%E5%88%87%E6%8D%A2%E4%B8%BAkarma%E7%9A%84%E6%9D%90%E8%B4%A8%E5%B7%A5%E4%BD%9C%E6%B5%81%E5%88%87%E6%8D%A2/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/houdini%E4%BB%8Emantra%E5%88%87%E6%8D%A2%E4%B8%BAkarma%E7%9A%84%E6%9D%90%E8%B4%A8%E5%B7%A5%E4%BD%9C%E6%B5%81%E5%88%87%E6%8D%A2/</guid><description>Houdini VEX编程中的变量使用技巧</description><pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我在跟教程的时候，发现principal shader节点再用Solaris的karma渲染的时候会出现概率性的材质丢失；并且有的材质节点的channels不再会被渲染出来（比如粗糙度，法线映射）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/76d6ccd2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Mantra渲染&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/adfab117.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
Karma渲染&lt;/p&gt;
&lt;p&gt;很明显材质贴图出现了部分参数的丢失，并且整个球都变得圆润起来了（其实一开始的球甚至全是绿色，也就是再obj的group分组材质分配节点不再会被渲染）&lt;/p&gt;
&lt;p&gt;结合Karma可以调用gpu进行渲染 mantra的光线追踪和噪点都十分难绷的情况下，切换为Karma工作流显得迫在眉睫，但是我很多节点都用到了材质分配节点啊！我要怎么改？&lt;/p&gt;
&lt;p&gt;问了一下claude，它是这么回答的：&lt;/p&gt;
&lt;p&gt;这是从Mantra切换到Karma时的常见问题。材质丢失的原因和解决方法：&lt;/p&gt;
&lt;h2&gt;主要原因&lt;/h2&gt;
&lt;h3&gt;1. &lt;strong&gt;材质节点不兼容&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Mantra使用&lt;strong&gt;VOP网络&lt;/strong&gt;和&lt;strong&gt;Principled Shader&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Karma使用&lt;strong&gt;MaterialX&lt;/strong&gt;和&lt;strong&gt;USD材质&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;某些Mantra特有的节点在Karma中不支持&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. &lt;strong&gt;自动转换局限&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Houdini会尝试自动转换，但不是100%完整&lt;/li&gt;
&lt;li&gt;复杂的材质网络可能转换不完全&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;方法1：使用Material Builder&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 在Material Builder中重新构建材质
2. 使用Karma兼容的节点
3. 主要使用 mtlxstandard_surface 节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;方法2：检查Material Network&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在Solaris中添加&lt;strong&gt;Material Library&lt;/strong&gt;节点&lt;/li&gt;
&lt;li&gt;确保材质正确分配给几何体&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;方法3：手动转换&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 创建新的Material Builder
2. 使用 mtlxstandard_surface 替代 principled shader
3. 重新连接贴图和参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;方法4：使用Karma Material&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;直接使用Karma专用的材质节点&lt;/li&gt;
&lt;li&gt;避免使用Mantra特有的材质&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;推荐工作流程&lt;/h2&gt;
&lt;p&gt;为了避免材质丢失，建议从项目开始就使用&lt;strong&gt;MaterialX标准&lt;/strong&gt;，这样在Mantra和Karma之间切换时兼容性更好。&lt;/p&gt;
&lt;p&gt;你具体丢失了哪种类型的材质？我可以提供更针对性的解决方案。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;MaterialX标准，我在Houdini官网上看了一下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MaterialX是一种开放源代码的标准，用于描述着色网络，旨在跨应用和渲染器之间实现材料的可移植性。通过UsdMaterialX插件，USD（Universal Scene Description）能够导入MaterialX着色器，而Houdini允许用户在VOP中构建MaterialX材质，并自动将其转换为UsdShade节点。尽管存在将VOP节点转换为.mtx文件的脚本，但Solaris主要支持作为USD prim的MaterialX节点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说MaterialX是一个开放源代码的标准，&lt;strong&gt;可以描述着色网络，并且被设计成可以在不同的应用程序和渲染器之间共享&lt;/strong&gt;。此外，Karma XPU不会支持传统基于VEX的着色器，由于优异的跨平台性，MaterialX是构建Karma渲染器的未来。&lt;/p&gt;
&lt;h3&gt;那要怎么切换为支持karma的材质？&lt;/h3&gt;
&lt;p&gt;Karma的材质节点一般是以mtlx开头；比如说mtlxstandard__surface shader；_在这里“mtlx”是MaterialX的文件扩展名，它是一种用于描述着色网络的开放源代码标准。在Solaris中，用户可以创建纯MaterialX材质或Karma中心化的材质。而Karma只支持体积材质，不支持表面和体积材质同时存在于同一个prim上。此外，Compositing节点也不受Karma支持。&lt;/p&gt;
&lt;p&gt;直接重命名方法&lt;strong&gt;不可行&lt;/strong&gt;，因为节点类型不同，参数结构也不一样。但有几种批量替换的方法：&lt;/p&gt;
&lt;h2&gt;推荐方法&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;方法1：再Material Network中使用Type Properties批量替换&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1. 选中所有principled节点
2. 右键 → Change Type → mtlxstandard_surface
3. Houdini会尝试自动映射相似参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/98da62a3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Mark小知识：parameter和name在材质节点之间的区别：&lt;/h3&gt;
&lt;h4&gt;1. &lt;strong&gt;在 OBJ 级（比如 &lt;strong&gt;&lt;code&gt;/obj/geo1&lt;/code&gt;&lt;/strong&gt;）使用 material 节点分配材质时&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;Material 节点分配材质的机制&lt;/strong&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Material 节点要求你&lt;strong&gt;指定一个材质路径&lt;/strong&gt;，比如 &lt;code&gt;/mat/test_mat&lt;/code&gt; 或 &lt;code&gt;/shop/my_shader&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;你可以为不同 primitive 或 group 分配不同的材质路径。&lt;/li&gt;
&lt;li&gt;这里的“路径”其实就是&lt;strong&gt;材质节点的名字或其路径&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;2. &lt;strong&gt;“Name”和“Parameter”在这里的含义&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;strong&gt;Keep Name&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;指的是材质节点本身在网络树中的节点名称，比如 &lt;code&gt;test_mat&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Material 节点分配材质时，是&lt;strong&gt;通过路径或名字找到材质节点&lt;/strong&gt;，并将材质“指派”给几何体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Keep Parameters&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;指的是&lt;strong&gt;材质节点上的参数值&lt;/strong&gt;（比如颜色、roughness、贴图路径等）。&lt;/li&gt;
&lt;li&gt;这些参数决定了材质的具体外观。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;3. &lt;strong&gt;答案总结&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;strong&gt;你在 OBJ 里用 material 节点分配材质时——&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;起决定作用的是“材质节点的名字/路径”&lt;/strong&gt;&lt;br /&gt;
（即你在 material 节点里填的 &lt;code&gt;/mat/xxx&lt;/code&gt;，这里的 xxx 是材质节点的名字）&lt;/li&gt;
&lt;li&gt;当你切换材质类型时，如果&lt;strong&gt;材质节点的名字没有变&lt;/strong&gt;，material 节点仍然能找到并分配它。&lt;/li&gt;
&lt;li&gt;如果你只保留了参数，但节点名字变了，material 节点找不到那个名字就&lt;strong&gt;不会分配成功&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;换句话说：&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Material 节点分配材质时，主要靠“名字/路径”起作用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;“参数”决定了材质本身的内容（外观），如果没有参数，材质就用默认值渲染。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;&lt;strong&gt;图示理解&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;影响对象&lt;/th&gt;
&lt;th&gt;现实例子&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;（节点名、路径）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mat/test_mat&lt;/code&gt; 这个路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parameters&lt;/strong&gt;（参数值）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Base Color = 红色&lt;/code&gt;、&lt;code&gt;Roughness=0.2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;Material 节点分配时是通过 &lt;strong&gt;路径/名字&lt;/strong&gt; 找到材质节点。&lt;/li&gt;
&lt;li&gt;节点的&lt;strong&gt;参数&lt;/strong&gt;决定材质的最终效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;结论&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;在 OBJ 层 material 节点分配材质时，&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;决定绑定是否成功的是“Name”（节点名字/路径）；&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;而“Parameters”决定最终材质外观。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;方法2：使用Python脚本批量替换&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 在Python Shell中运行
import hou

# 获取当前材质网络
matnet = hou.pwd()  # 确保在material network中

# 找到所有principled节点
principled_nodes = [n for n in matnet.children() if n.type().name() == &quot;principled&quot;]

for node in principled_nodes:
    # 获取节点位置和连接
    pos = node.position()
    
    # 创建新的mtlx节点
    new_node = matnet.createNode(&quot;mtlxstandard_surface&quot;)
    new_node.setPosition(pos)
    new_node.setName(node.name())
    
    # 复制相似参数（需要手动映射）
    # base_color = node.parm(&quot;basecolor&quot;).eval()
    # new_node.parm(&quot;base_color&quot;).set(base_color)
    
    # 复制连接
    # ... 需要详细的连接复制代码
    
    # 删除旧节点
    node.destroy()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;方法3：创建HDA (资产)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;创建一个包含mtlxstandard_surface的HDA，然后批量替换引用。&lt;/p&gt;
&lt;h2&gt;最实用的方法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;方法1&lt;/strong&gt;是最简单的，Houdini会自动尝试参数映射。但是可能并不会替换完全。比如如下从principal切换为mtlx类型的材质出现报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/b7c881f2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里其实是参数引用错误，说明从&lt;strong&gt;principled shader转换到mtlxstandard_surface后，参数名称不匹配，换句话说，也可以认为是Channels出现了不匹配的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;方法1：清理无效引用&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;选中mtlxstandard_surface节点&lt;/li&gt;
&lt;li&gt;右键 → &lt;strong&gt;Channels and Keyframes&lt;/strong&gt; → &lt;strong&gt;Delete Channels&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;选择删除所有无效的参数引用&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;方法2：手动修复参数映射&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;主要的参数名称对应关系：&lt;/p&gt;
&lt;p&gt;複製&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;principled → mtlxstandard_surface
metallic → metalness
reflectint → specular (需要调整数值)
baseBumpAndNormal_* → normal相关参数
dispInput_* → displacement相关参数
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;方法3：重新创建材质(推荐)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;由于转换后问题较多，建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建新的mtlxstandard_surface节点&lt;/li&gt;
&lt;li&gt;手动重新连接贴图&lt;/li&gt;
&lt;li&gt;重新设置参数值&lt;/li&gt;
&lt;li&gt;删除转换后有问题的节点&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;方法4：使用参数表达式修复&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在有问题的参数中，将引用改为正确的参数名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;metallic&lt;/code&gt; → &lt;code&gt;metalness&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reflectint&lt;/code&gt; → &lt;code&gt;specular&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>基本渲染管线</title><link>https://fuwari.vercel.app/posts/basic-rendering-pipeline/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/basic-rendering-pipeline/</guid><description>基本渲染管线的技术原理和应用</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;/images/posts/46b632a3.png&quot; alt=&quot;渲染的基本结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;游戏里的&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=184868332&amp;amp;content_type=Article&amp;amp;match_order=2&amp;amp;q=%E6%B8%B2%E6%9F%93%E6%B5%81%E6%B0%B4%E7%BA%BF&amp;amp;zhida_source=entity&quot;&gt;渲染流水线&lt;/a&gt;（渲染管线）分为几个阶段，不同的文献里可能有不同的分法，但都大同小异，大概可以分为应用阶段、几何阶段、&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=184868332&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E5%85%89%E6%A0%85%E5%8C%96&amp;amp;zhida_source=entity&quot;&gt;光栅化&lt;/a&gt;阶段。&lt;/p&gt;
&lt;h2&gt;应用阶段&lt;/h2&gt;
&lt;p&gt;应用阶段的主要职责是输出渲染所需的几何信息（渲染图元，可以是点、线等）和渲染设置给GPU，并调用DrawCall指令开启渲染：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/35fd1b74.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;几何阶段&lt;/h2&gt;
&lt;p&gt;几何阶段主要负责将三维的模型顶点坐标转换为屏幕空间的二维顶点坐标，输出各顶点的深度值等信息到光栅化阶段：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/ca7a1bf5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=184868332&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E9%A1%B6%E7%82%B9%E7%9D%80%E8%89%B2%E5%99%A8&amp;amp;zhida_source=entity&quot;&gt;顶点着色器&lt;/a&gt;中，顶点着色器输出的顶点数据输出后的数据将会马上被组装成图元（如三角形、线段等），我们也称之为图元组装阶段（当然也有一种观点认为，图元装配应该属于光栅化阶段的三角形设置部分）。把顶点从&lt;strong&gt;模型空间转换到裁剪空间&lt;/strong&gt;的过程，这里使用MVP矩阵完成这个操作，&lt;strong&gt;完成后的顶点只是转换到了&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=184868332&amp;amp;content_type=Article&amp;amp;match_order=2&amp;amp;q=%E8%A3%81%E5%89%AA%E7%A9%BA%E9%97%B4&amp;amp;zhida_source=entity&quot;&gt;裁剪空间&lt;/a&gt;(Cliip Space)里，还不是屏幕空间(Screen Space)&lt;/strong&gt;，这一步只是为后面的裁剪步骤做准备。&lt;/p&gt;
&lt;p&gt;顶点转换到裁剪坐标系(Clip Space)后，&lt;strong&gt;在屏幕映射步骤里会由硬件做透视除法（&lt;/strong&gt;&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=184868332&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E9%BD%90%E6%AC%A1%E9%99%A4%E6%B3%95&amp;amp;zhida_source=entity&quot;&gt;&lt;strong&gt;齐次除法&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;），最后得到归一化的设备坐标（Normalized Device Coordinates， NDC）&lt;/strong&gt;，这个坐标是归一化的，xy 的取值范围是 -1~1，OpenGL中 z的取值范围应该是 -1~1 ，DirectX 中 z 的取值范围是 0~1。&lt;/p&gt;
&lt;p&gt;NDC空间坐标最后会进行映射运算得到二维的电脑屏幕的坐标。要注意：OpenGL的屏幕坐标原点左下角：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/91c9579c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而DirectX的屏幕坐标原点是左上角：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1057e9f4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Unity 选择了OpenGL 的规范，微软的许多窗口都是用左上角为原点的坐标系统，因为这样和我们的阅读方式：从左到右、从上到下的习惯是匹配的。&lt;/p&gt;
&lt;h2&gt;光栅化阶段&lt;/h2&gt;
&lt;p&gt;光栅化步骤主要负责：每个渲染图元（点、线、面）中的哪些像素应该被绘制在屏幕上，也需要对上一个阶段得到的逐顶点数据进行插值赋值给每个像素，再逐像素处理光照。三角形在设置后会随后生成片元，通过遍历三角形覆盖了那些像素，随后会生成片元。fragment是光栅化阶段生成的&lt;strong&gt;候选像素&lt;/strong&gt;，包含颜色、深度、纹理坐标等属性。在确定片元序列，后就会开始进行片元着色（fragment shader），并开始通过各种测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bb80ff03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;有关透明度测试和提前深度测试(Early Z)&lt;/h3&gt;
&lt;p&gt;如果你在片元代码里调用了 clip 函数，那么就是开启了透明度测试，不过 clip 函数传递进参数的可以不是颜色的透明度，只要是一个 float 值就行。&lt;/p&gt;
&lt;p&gt;提前深度测试可以把被不透明物体遮挡的片元提前舍弃掉，提高渲染的效率，但我们都知道一个片元如果通过了深度测试，默认情况下他是会向深度缓冲区写入自己的深度的，试想一下：如果开启了透明度测试的物体 B 在另一个物体 A 之前，假设物体 B 先渲染，并且假设进行了提前深度测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1163316a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于物体B第一个渲染，他发现深度缓冲里没有值，所以通过了提前深度测试并写入了自己的深度值，这导致后面的物体 A 在进行深度测试的时候，无法通过测试，抛弃了这部分的片元。如果物体B像一个正常的不透明物体那样渲染这没什么问题，但是问题是现在的物体B会进行透明度测试，如果物体B的一部分没有通过透明度测试，被裁剪，那么将会出现错误的现象：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c8e4cca4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此提前深度测试和透明度测试是互相冲突的，当你开启了透明度测试，也就意味着这个片元不能进行提前深度测试。由于提前深度测试对渲染性能有所提升，所以说透明度测试会导致渲染性能下降。&lt;/p&gt;
&lt;p&gt;屏幕后处理技术是指当这一帧的画面渲染完毕的时候，在输出到电脑屏幕之前，我们可以对这个画面进行一些特殊的处理，比如泛光效果等，值得注意的是虽然此时处理的图元是一个完整的长方形的二维的图画（可以理解为一个png）但是其中每个像素点都是记录了一些信息的，比如深度等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/435326086&quot;&gt;https://zhuanlan.zhihu.com/p/435326086&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;有关于渲染管线中涉及到的空间&lt;/h1&gt;
&lt;p&gt;一个物体从&lt;strong&gt;模型数据&lt;/strong&gt;到&lt;strong&gt;最终屏幕像素&lt;/strong&gt;，需要经过一系列&lt;strong&gt;空间变换&lt;/strong&gt;。这些空间变换是3D渲染的基础，也是Shader开发常考的知识点。&lt;/p&gt;
&lt;p&gt;下面详细梳理一下&lt;strong&gt;完整的空间变换流程&lt;/strong&gt;和各空间坐标的计算方式：&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. &lt;strong&gt;主要的空间坐标体系&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;按渲染流程顺序，一个物体的坐标会依次经过：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模型空间（Model Space / Object Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;物体自身的本地坐标系。&lt;/li&gt;
&lt;li&gt;顶点坐标就是建模时定义的坐标，原点一般在物体中心或底部。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;世界空间（World Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;场景统一的坐标系。&lt;/li&gt;
&lt;li&gt;所有物体都放在这个空间，原点和轴向一般由世界场景定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视图空间（View Space / Camera Space / Eye Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;以摄像机为原点、摄像机方向为Z轴的空间。&lt;/li&gt;
&lt;li&gt;所有物体会被“转换到以摄像机为中心的空间”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;裁剪空间（Clip Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;经过投影变换后的坐标空间，已应用视锥体截取（Near/Far、FOV等）。&lt;/li&gt;
&lt;li&gt;用于裁剪视锥外的顶点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;齐次裁剪空间（Homogeneous Clip Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;裁剪空间坐标还没除以w。&lt;/li&gt;
&lt;li&gt;用于进一步的几何处理和裁剪。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NDC空间（Normalized Device Coordinates）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;齐次除法后得到的[-1,1]范围坐标，左下(-1,-1)，右上(1,1)。&lt;/li&gt;
&lt;li&gt;用于光栅化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;屏幕空间（Screen Space / Pixel Space）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;归一化坐标映射到屏幕像素坐标系。&lt;/li&gt;
&lt;li&gt;(0,0)通常为左下角，(width-1, height-1)为右上角。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;strong&gt;各空间之间的变换矩阵&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;每一步坐标变换，都需要一个&lt;strong&gt;变换矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;空间&lt;/th&gt;
&lt;th&gt;变换矩阵&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;模型空间 → 世界空间&lt;/td&gt;
&lt;td&gt;模型矩阵 (M)&lt;/td&gt;
&lt;td&gt;物体在世界中的位置、旋转、缩放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;世界空间 → 观察空间&lt;/td&gt;
&lt;td&gt;视图矩阵 (V)&lt;/td&gt;
&lt;td&gt;摄像机的变换（相机逆矩阵）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;观察空间 → 裁剪空间&lt;/td&gt;
&lt;td&gt;投影矩阵 (P)&lt;/td&gt;
&lt;td&gt;透视投影/正交投影成视锥体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;裁剪空间 → NDC&lt;/td&gt;
&lt;td&gt;齐次除法（/w）&lt;/td&gt;
&lt;td&gt;x/w, y/w, z/w&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NDC → 屏幕空间&lt;/td&gt;
&lt;td&gt;屏幕映射&lt;/td&gt;
&lt;td&gt;视口变换（[-1,1]到[0,width/height]）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;总变换公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;模型空间坐标 → 世界空间:     p_world = M * p_model
世界空间 → 观察空间:        p_view = V * p_world
观察空间 → 裁剪空间:        p_clip = P * p_view
裁剪空间 → NDC:            p_ndc = p_clip.xyz / p_clip.w
NDC → 屏幕空间:             p_screen = （p_ndc.xy + 1）* 0.5 * 屏幕尺寸
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. &lt;strong&gt;Unity中的矩阵名称说明&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;unity_ObjectToWorld&lt;/strong&gt;：模型到世界&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unity_WorldToObject&lt;/strong&gt;：世界到模型&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNITY_MATRIX_V&lt;/strong&gt;：世界到观察&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNITY_MATRIX_VP&lt;/strong&gt;：世界到裁剪（V * P）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNITY_MATRIX_P&lt;/strong&gt;：观察到裁剪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNITY_MATRIX_MVP&lt;/strong&gt;：模型到裁剪（M * V * P）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. &lt;strong&gt;Shader中的具体使用举例&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;顶点着色器常见流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 顶点模型空间
float4 pos_model = v.vertex;

// 变到世界空间
float4 pos_world = mul(unity_ObjectToWorld, pos_model);

// 变到观察空间
float4 pos_view = mul(UNITY_MATRIX_V, pos_world);

// 变到裁剪空间
float4 pos_clip = mul(UNITY_MATRIX_P, pos_view);

// 或者直接一步到裁剪空间
float4 pos_clip = mul(UNITY_MATRIX_MVP, pos_model);

// 输出到光栅化
o.pos = pos_clip;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. &lt;strong&gt;坐标空间的用途总结&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模型空间&lt;/strong&gt;：贴图、法线、动画蒙皮等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;世界空间&lt;/strong&gt;：灯光、阴影、物体交互&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;观察空间&lt;/strong&gt;：视锥裁剪、摄像机对齐操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;裁剪/NDC空间&lt;/strong&gt;：光栅化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;屏幕空间&lt;/strong&gt;：屏幕特效、后处理、UI&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. &lt;strong&gt;一句话总结&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Unity渲染管线中，一个物体从模型空间，经过世界空间、观察空间、裁剪空间、NDC空间，最终被光栅化到屏幕空间。每一步都通过特定的变换矩阵进行转换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;如果你想看更详细的Shader代码、矩阵推导、或各空间的可视化示例，欢迎继续提问！&lt;/p&gt;
</content:encoded></item><item><title>什么是帧缓冲（Frame Buffer）？</title><link>https://fuwari.vercel.app/posts/%E4%BB%80%E4%B9%88%E6%98%AF%E5%B8%A7%E7%BC%93%E5%86%B2frame-buffer/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E4%BB%80%E4%B9%88%E6%98%AF%E5%B8%A7%E7%BC%93%E5%86%B2frame-buffer/</guid><description>什么是帧缓冲（Frame Buffer）？的技术原理和应用</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当然可以！&lt;strong&gt;帧缓冲（Framebuffer）是现代图形渲染管线中的核心概念之一。下面我从渲染管线的结构、帧缓冲的作用和所在阶段&lt;/strong&gt;来系统讲解。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. &lt;strong&gt;什么是帧缓冲（Framebuffer）？&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;帧缓冲本质上是&lt;strong&gt;一块显存中的内存区域&lt;/strong&gt;，用于&lt;strong&gt;存储最终要显示在屏幕上的像素数据&lt;/strong&gt;。注意是&lt;strong&gt;最终。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;它通常包含&lt;strong&gt;颜色缓冲区&lt;/strong&gt;（Color Buffer）、&lt;strong&gt;深度缓冲区&lt;/strong&gt;（Depth Buffer）、有时还包括&lt;strong&gt;模板缓冲区&lt;/strong&gt;（Stencil Buffer）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;strong&gt;帧缓冲在渲染管线中的位置&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;A. 图形渲染管线主要阶段&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用阶段&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;场景管理、逻辑、摄像机、渲染命令的下达&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;几何处理阶段&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;顶点着色（Vertex Shader）、投影、变换、裁剪&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化阶段&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;将三角形转为像素（Fragment/Pixel）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;片元处理阶段&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;片元着色（Fragment/Pixel Shader），决定每个像素的颜色和深度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出合并阶段（Output Merger）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;将片元着色器输出的颜色、深度等数据，&lt;strong&gt;写入帧缓冲&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示（Scan-out）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;将帧缓冲内容传给显示器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;帧缓冲正好处在&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;片元着色器输出&lt;/strong&gt;和&lt;strong&gt;最终显示&lt;/strong&gt;之间。&lt;/li&gt;
&lt;li&gt;是所有像素“最终落地”的地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. &lt;strong&gt;帧缓冲的结构&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;颜色缓冲区&lt;/strong&gt;：存储每个像素的最终颜色（通常是RGBA，8bit/10bit/16bit/float等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度缓冲区&lt;/strong&gt;：存储每个像素的深度值（用于遮挡判断）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板缓冲区&lt;/strong&gt;：用于高级像素操作（如遮罩、分区渲染等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;多重帧缓冲（MRT）&lt;/strong&gt;：现代引擎还支持一次渲染输出到多个帧缓冲（G-buffer、延迟渲染等）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. &lt;strong&gt;帧缓冲的作用&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;存储最终像素结果&lt;/strong&gt;：所有片元着色器输出的颜色/深度/模板值都写入帧缓冲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后处理/屏幕特效的输入&lt;/strong&gt;：如Bloom、色调映射、抗锯齿等后期处理都以帧缓冲为输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多阶段渲染的中转站&lt;/strong&gt;：比如先把一帧渲染到离屏帧缓冲（Render Texture），再做特效，最后再输出到主帧缓冲/屏幕。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;5. &lt;strong&gt;帧缓冲的“生命周期”&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每一帧渲染开始时，帧缓冲会&lt;strong&gt;清空/重置&lt;/strong&gt;（clear）。&lt;/li&gt;
&lt;li&gt;场景中所有可见物体经过渲染管线，最终像素值被写入帧缓冲。&lt;/li&gt;
&lt;li&gt;一帧渲染完毕，帧缓冲的内容被扫描输出到显示器，成为你看到的画面。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;6. &lt;strong&gt;帧缓冲和色彩空间/色深的关系&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;帧缓冲的&lt;strong&gt;格式&lt;/strong&gt;决定它能存储的色深（8bit/10bit/float等），影响HDR/色彩精度。&lt;/li&gt;
&lt;li&gt;后处理通常直接操作帧缓冲的浮点数值，实现色彩调整、色调映射等。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;7. &lt;strong&gt;一句话总结&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;帧缓冲是渲染管线中负责存储最终像素颜色、深度等信息的内存区域，处于片元着色器输出和画面显示之间，是所有像素“最终汇合与输出”的关键节点。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;可视化流程（简化）&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[顶点数据] 
   ↓
[顶点着色器]
   ↓
[光栅化/插值]
   ↓
[片元着色器]
   ↓
[帧缓冲（颜色、深度、模板）] ← 后处理等特效也在此基础上进行
   ↓
[显示器]
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Blender小技巧：用噪声消除重复纹理</title><link>https://fuwari.vercel.app/posts/blender%E5%B0%8F%E6%8A%80%E5%B7%A7%E7%94%A8%E5%99%AA%E5%A3%B0%E6%B6%88%E9%99%A4%E9%87%8D%E5%A4%8D%E7%BA%B9%E7%90%86/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/blender%E5%B0%8F%E6%8A%80%E5%B7%A7%E7%94%A8%E5%99%AA%E5%A3%B0%E6%B6%88%E9%99%A4%E9%87%8D%E5%A4%8D%E7%BA%B9%E7%90%86/</guid><description>Blender小技巧：用噪声消除重复纹理的解决方案和处理方法</description><pubDate>Sun, 26 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在创建虚拟环境中的草地或者使用混凝土/路面的纹理时，我们常常面临的一个挑战是如何避免纹理重复带来的不真实感。为了解决这个问题可以有两种方法：一个快速的方法是基于沃罗诺伊噪声的旋转；二是利用砖块纹理进行分区旋转。&lt;/p&gt;
&lt;p&gt;首先，添加沃罗诺伊纹理节点来引入沃罗诺伊噪声。什么是沃洛诺伊噪声？&lt;a href=&quot;https://docs.blender.org/manual/zh-hans/dev/render/shader_nodes/textures/voronoi.html&quot;&gt;沃罗诺伊纹理 - Blender 4.4 Manual&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这种类型的噪声以其随机分布的点和区域而闻名，非常适合模拟自然界中的非规律性模式。这个噪波很适合打乱由于缩放造成的重复性，原理是在纹理坐标层面进行打乱，使重新映射过的坐标发生变化，进而造成“没有重复性”的假象。&lt;/p&gt;
&lt;p&gt;但是这样造成的边缘比较明显怎么办？有一个小技巧是在从物体的纹理坐标传递到沃罗洛伊噪声的时候再引入白噪声节点与纹理坐标进行颜色混合，将颜色混合的强度调低一些；白噪声可以让坐标边缘起到类似于模糊的效果。&lt;/p&gt;
&lt;p&gt;最后的节点图如下（只是对于坐标层面的改变所以很容易）&lt;img src=&quot;/images/posts/24a9992c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另一种方法是利用砖块纹理进行分区旋转。砖块纹理具有天然的分区特性，恰好blender/maya/C4D中都可以对每一个砖块进行随机化，非常适合用来划分对于不同草地的不同区域。这个步骤同样可以通过映射节点完成，这次的原理是使用了砖块纹理的输出来驱动旋转值的变化，而不是单纯的使用噪声打乱纹理坐标。优点是可以由于只是对坐标进行旋转处理，而不是用噪声打乱坐标，从而实现了更加真实的效果。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1HLcmewEj9/?spm_id_from=333.1387.favlist.content.click&amp;amp;vd_source=30f07a01a885038de48c259c5cfe892d&quot;&gt;【中字】Blender 材质教程：无缝贴图, PBR随机化无缝衔接 -修复初学者在着色方面的主要错误！_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Unity实现体积光（GodRay）的三种方案</title><link>https://fuwari.vercel.app/posts/unity%E5%AE%9E%E7%8E%B0%E4%BD%93%E7%A7%AF%E5%85%89godray%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E6%A1%88/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/unity%E5%AE%9E%E7%8E%B0%E4%BD%93%E7%A7%AF%E5%85%89godray%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E6%A1%88/</guid><description>Unity中实现体积光效果的三种不同方案对比</description><pubDate>Fri, 24 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;&lt;strong&gt;Unity中实现体积光的三种方式&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;体积光就是让光看起来“有体积”，像是穿过雾气的光柱，或者空气中漂浮着的尘埃被光线照亮的效果。它比普通的光照多了一种在空间中传播的感觉，让游戏画面更有氛围感。&lt;/p&gt;
&lt;p&gt;实现体积光的方式有很多，但总的来说可以分为三种：&lt;strong&gt;面片方法&lt;/strong&gt;、&lt;strong&gt;屏幕空间方法&lt;/strong&gt;和&lt;strong&gt;光线步进方法&lt;/strong&gt;。这三种方法从简单到复杂各有优缺点。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;1. 面片方法/粒子特效方法&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这里先讲面片的实现方法。面片方法很好理解，假设你拿着一根手电筒，手电筒打出了一束光柱。我们并不真的去模拟这些光线在空气中传播，而是直接在手电筒前面摆一个锥形的“透明罩子”，让它看上去像光柱一样。这个“罩子”就是一个模型（比如锥体或者圆柱体），通过材质的透明效果让它变得像一束光。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;实现思路：&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;对于直接使用模型模拟的情况，可以创建一个几何体&lt;/strong&gt;，比如锥体或者一个平面面片。把这个几何体放在光源位置，并让它的方向和光源的方向一致。给几何体加一个材质，材质可以通过渐变纹理让中央部分更亮，边缘更透明。为了更真实，可以再叠加一个噪声纹理，模拟空气中的尘埃。如果光源会动，比如手电筒或者舞台灯光，那么这个几何体也需要随着光源移动和旋转。&lt;/p&gt;
&lt;p&gt;还有一个方法比较Trick、就是利用&lt;strong&gt;Unity自带的粒子系统&lt;/strong&gt;进行动态体积光的模拟,这个方法比上一个方法更加富有动态上的美感（但是需要配合Bloom效果，意味整个画面需要有所改变），适合作为一个附加项。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;优点和缺点&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单，一个几何体加一个材质/使用粒子就能搞定。&lt;/li&gt;
&lt;li&gt;性能开销很小，因为是基于物体的材质或者是引擎自带的粒子系统，非常适合性能敏感的场景，比如移动端游戏。&lt;/li&gt;
&lt;li&gt;艺术表现力强，通过调整材质的渐变、纹理、透明度等参数，可以轻松制作出各种风格的光柱效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真实感有限，因为它只是一个几何体，并没有模拟光线与空气的真实交互。&lt;/li&gt;
&lt;li&gt;遮挡问题比较明显。如果光柱穿过了场景中的墙壁或者其他物体，它不会被正确遮挡，看起来就很假。&lt;/li&gt;
&lt;li&gt;靠近光柱时可能看起来很平面，缺乏立体感。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;面片方法特别适合那些对真实感要求不高的场景，比如舞台灯光、探照灯、或者卡通渲染风格的游戏。如果你的项目对性能要求比较高，这种方法并不是最优选择。&lt;/p&gt;
&lt;h4&gt;参考&lt;/h4&gt;
&lt;h4&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=kbsd6askiCY&amp;amp;t=195s&quot;&gt;https://www.youtube.com/watch?v=kbsd6askiCY&amp;amp;t=195s&lt;/a&gt; Using Particle System&lt;/h4&gt;
&lt;h3&gt;&lt;strong&gt;2. 屏幕空间方法：光柱和场景的互动&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;面片方法的问题在于，它不会考虑光柱和场景中的物体之间的遮挡关系。而屏幕空间方法解决了这个问题。它的核心思想是：我们不在三维空间中画光，而是在屏幕上“画光”。可以理解为，我们直接在屏幕上计算光柱的效果，而不是在场景中放一个几何体。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;实现思路&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;首先，把光源的位置投影到屏幕上，&lt;strong&gt;确定光柱在屏幕上的范围&lt;/strong&gt;。从光源的屏幕位置出发，向外扩散光线。沿着这些光线的方向，我们对屏幕上的像素进行采样。&lt;strong&gt;通过深度缓冲（Depth Buffer），判断光线是否被遮挡&lt;/strong&gt;。比如，如果光柱前面有一堵墙，通过深度值可以知道光柱应该被遮挡。计算完光柱的强度和遮挡效果后，把它作为一个后处理效果叠加到最终画面上。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;优点和缺点&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态遮挡效果逼真。比如光柱被墙挡住后，遮挡关系是自动处理的，真实感更好。&lt;/li&gt;
&lt;li&gt;可以处理多个光源的体积光效果，比如汽车大灯或者手电筒光柱。&lt;/li&gt;
&lt;li&gt;性能开销可以调节，比如可以减少采样点来提升性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它只能处理屏幕上看得见的部分，屏幕外的光柱是无法计算。也就是说如果不出现在屏幕空间则没有体积光。&lt;/li&gt;
&lt;li&gt;效果和分辨率密切相关，分辨率越高，计算量越大。&lt;/li&gt;
&lt;li&gt;遮挡的精度受限于深度缓冲，如果场景比较复杂，可能会有一些不准确的地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;屏幕空间方法特别适合那些需要动态光柱效果的场景，比如手电筒、汽车大灯，或者光线穿过窗户洒进房间的画面。它对硬件的要求不算太高，适用于中高端设备。&lt;/p&gt;
&lt;h4&gt;参考&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=vnBfbZeV928&quot;&gt;https://www.youtube.com/watch?v=vnBfbZeV928&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;3. 光线步进方法&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;上面两种方法都是“假的”体积光，而光线步进方法则是“真的”体积光。它直接在三维空间中模拟了光线在空气、雾气、烟尘中的传播过程。简单来说，它会从摄像机出发，沿着每一条视线（即屏幕上的每个像素）不断采样，计算光线在这些点上的散射和吸收。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;实现思路&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;从摄像机出发，沿视线方向，对每条光线进行步进采样（类似于光线追踪）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/e3561098.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每个采样点都会计算光线的强度变化，比如步进一步之后光线在传播中因为散射而变弱，使用数组对当前光线进行记录。&lt;/p&gt;
&lt;p&gt;如果采样点被光源照亮，还要计算光线的直接照明效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/f505edc0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;把所有采样点的结果累加起来，得到当前像素的体积光效果。&lt;/p&gt;
&lt;p&gt;最后，把体积光效果和场景颜色合成，生成最终画面。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;优点和缺点&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真实感爆表！它可以精确模拟光线在体积介质中的散射、吸收和遮挡。&lt;/li&gt;
&lt;li&gt;支持动态雾气、烟尘等复杂效果，比如爆炸后的烟雾中透出的一束光。&lt;/li&gt;
&lt;li&gt;灵活性极高，可以表现各种复杂的光照效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算量巨大，&lt;strong&gt;每条光线都需要多次采样（比如说你的步长）&lt;/strong&gt;，对性能要求非常高。&lt;/li&gt;
&lt;li&gt;实现难度较大，需要对体积渲染和光照模型有深入理解。&lt;/li&gt;
&lt;li&gt;对硬件要求高，通常只适合高端设备和次世代游戏。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;光线步进方法是高质量渲染的终极选择，适合那些需要表现复杂雾气、烟尘或者动态光线传播的场景，比如次世代游戏、电影级别的渲染效果。如果你追求极致的真实感，并且硬件性能允许，那这就是你的最佳选择。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;面片方法&lt;/td&gt;
&lt;td&gt;简单高效，性能占用低&lt;/td&gt;
&lt;td&gt;真实感不足，无法处理遮挡&lt;/td&gt;
&lt;td&gt;卡通风格、移动端游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;屏幕空间方法&lt;/td&gt;
&lt;td&gt;动态效果好，可处理遮挡&lt;/td&gt;
&lt;td&gt;受屏幕限制，分辨率依赖&lt;/td&gt;
&lt;td&gt;手电筒、大灯等动态光柱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;光线步进方法&lt;/td&gt;
&lt;td&gt;真实感最高，支持复杂动态光线传播&lt;/td&gt;
&lt;td&gt;性能开销大，硬件要求高&lt;/td&gt;
&lt;td&gt;次世代游戏、电影级别渲染&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;总的来说，如果性能要求高，可以选择面片方法。如果需要动态效果并且硬件性能允许，屏幕空间方法会是一个很好的平衡。而如果你追求极致的真实感，那就选择光线步进方法。&lt;/p&gt;
&lt;h4&gt;参考&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=0G8CVQZhMXw&amp;amp;t=199s&quot;&gt;https://www.youtube.com/watch?v=0G8CVQZhMXw&amp;amp;t=199s&lt;/a&gt;  这采用了houdini生成3D纹理并使用Shadergraph实现Raymarching的效果&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=hXYOlXVRRL8&amp;amp;t=570s&quot;&gt;https://www.youtube.com/watch?v=hXYOlXVRRL8&amp;amp;t=570s&lt;/a&gt;  上面的省流版&lt;/p&gt;
</content:encoded></item><item><title>色彩空间</title><link>https://fuwari.vercel.app/posts/color-space/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/color-space/</guid><description>色彩空间的原理解析和实现方法</description><pubDate>Thu, 23 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;引言&lt;/h1&gt;
&lt;p&gt;这是我偶然在Houdini的时候在导出的时候遇到的问题，Houdini为了更加可定制化的工作流直接采用了exr作为输出；但是exr文件在ps里面打开后的效果和达芬奇的效果完全不一致，恰好当时色彩空间也是我的心头之痛，而且几乎没有从图形学上解释色彩的文章，比较深的也只是讲到了色度图；因此打算写这篇文章。&lt;/p&gt;
&lt;h1&gt;色彩的表示&lt;/h1&gt;
&lt;p&gt;我们都知道，在选择颜色的时候，不同的软件有不同的办法来调整颜色。如果我们把三原色R,G,B来表示为坐标系的三个数据，就可以得到科研中常用于色彩分析的三维空间：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/0e4d01d7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然，这是计算机领域的表示法，艺术家们需要一个更加直观的表示方法，于是除了简单的RGB直接相加，另一种表示方法HSL应运而生，HSL模型可以更直观地反映了人类对颜色的感知方式，直接利用Hue(色相),Saturation(饱和度),Lightness(明度）表示。我们Ps中的选色盘和选色图都是基于这个标准。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/5e96d269.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但其实HSL并不是最终发送给计算机的，计算机最后还是用RGB来进行色彩的表示。因为实际上RGB的数据结构只有(x,y,z)，即colorRGB = vec3(x,y,z)。也就是说，这个数值实际上只能直接表示Hue和Saturation，而Lightness则是用RGB进行一套公式转换出来的；在一些shader中，比如要进行亮度阈值计算，就会直接提取RGB来算出Lightness，再套亮度判断。&lt;/p&gt;
&lt;p&gt;另外HSL和RGB之间是可以无损转换的，虽然两者的表示方法不一致(HSL:0-360°,0-100%,0-100%;RGB &lt;strong&gt;8bit&lt;/strong&gt;:0-255，如果都取int则360x101x101=3672360（360万色）和256x256x256=16777216 （1670万色），亦可以用16进制表示:#000000-#FFFFFF，一个FF表示为2的8次幂255)，&lt;strong&gt;HSL 色轮的某些组合（特别是高亮度或低饱和度）会收敛到白色、灰色、黑色&lt;/strong&gt;，导致一些 HSL 组合映射到相同的 RGB 值；但是理论上，在算法下只要表达的精度够高，两者之间仍然是可以进行色彩转换的。&lt;/p&gt;
&lt;p&gt;有一个理论是说HSL能够表达的色彩要少于RGB，这个说法属于是理所当然的认为HSL认为为整数/一位小数了，**我在色轮中给你看到一位小数 != 这个颜色的实际HSL值。**除此以外，HSL有一个特点就是因为引入了Lightness，那么Lightness约接近于0/1，那么颜色重合度越高（因为越接近于白色和黑色）&lt;/p&gt;
&lt;p&gt;因此HSL的色彩坐标图更加接近于双锥体，和上图的RGB三维坐标图作为对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H（Hue，色相）&lt;/strong&gt;：绕圆周旋转，360°，代表颜色种类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S（Saturation，饱和度）&lt;/strong&gt;：从中心（灰色）到边缘（最纯色）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L（Lightness，亮度）&lt;/strong&gt;：从底部（黑）到顶部（白），中间是最大纯度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/62818472.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并且现代计算机为了进行图层叠加或者混合（也就是alpha test、alpha clipping等等和透明度有关的计算），会引入一个新的量Alpha，也会使用colorRGBA = vec4(x,y,z,a)，不过至于引入alpha之后色彩要如何叠加表示我就先不讲了，这里///又是一个大坑...&lt;/p&gt;
&lt;h2&gt;扩展：CIE色度图&lt;/h2&gt;
&lt;p&gt;实际上，rgb(255, 0, 0) 或者 #FF0000 这样的色值参数，仅仅只是告诉计算机显示设备：请你展示纯红色，不掺杂一点蓝也不掺杂一点绿的纯红色。此时，显示设备会尽自己的全力去显示一个 100% 饱和度的纯红色，但不同的显示设备，他们的展示效果是不同的，其中某些显示设备的红色看起来可能会比另外一些更鲜艳（比如手机屏幕看起来比电脑屏色彩艳丽）。这就带来了一个问题：RGB 色值的表现是基于显示设备的，不同的显示设备对 RGB 的表达能力有所不同，虽然他们接受到的色值参数是一致的，但呈现出来的颜色的视觉感受却并不相同，换言之就是他们并没有展示出相同的颜色，那到底哪个红色才是准确的呢？&lt;/p&gt;
&lt;p&gt;实际上这个问题早在 1931 年就得到了解决，为了定义一种与设备无关的颜色模型，让色彩能够脱离设备而准确定义，1931 年国际照明委员会（CIE）的色彩科学家们在 &lt;a href=&quot;https://zhida.zhihu.com/search?content_id=114846309&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=RGB+%E6%A8%A1%E5%9E%8B&amp;amp;zhida_source=entity&quot;&gt;RGB 模型&lt;/a&gt;基础上，通过数学的方法推导出了理论的 RGB 三基色，并以此创建了一个标准的色彩系统。而这一色彩系统的常见展示方式，就是 CIE 色度图（CIE chromaticity diagram）（如下图），我们之后所讨论的色彩空间往往都是基于这个色度图去展示的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c0933ac2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为什么 CIE 色度图会长这个样子，一个看起来没什么规则的奇怪三角形，之前的圆形色盘不是更美观吗？事实上这个形状是通过数学计算得来的，之所以计算结果长这样，其中一个重要规则是 “&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=114846309&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=%E6%84%9F%E7%9F%A5%E5%9D%87%E5%8C%80%E6%80%A7&amp;amp;zhida_source=entity&quot;&gt;感知均匀性&lt;/a&gt;”，说人话就是这个形状更容易表现色彩之间的关系。还是没明白？没关系，我们举个例子:&lt;/p&gt;
&lt;p&gt;比如图一中，如果 A 点（绿色） 和 B 点（红色）两个点的颜色等比例混合，那么得到的新的颜色黄色一定在 A 点和 B 点连线的中点 E 点上，反观（图二）在圆形色盘（非感知均匀性图形）上去连接纯绿色 A 和纯红色 B 两个点，他们的中点 E 并不是纯黄色，纯黄色在圆周 F 点上；换言之，如果 A 点和 B 点两个颜色通过任意比例混合，那么得到的新的颜色也必然在 A B 两点的连线上。也就是说，（图三）R G B 三个点的颜色通过任意比例混合，他们所表现出来的新颜色也必然在这个三角范围内。&lt;/p&gt;
&lt;p&gt;这里要声明一下，除了几何上的追求，还有一个就是生物学上对于感知的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CIE 色度图&lt;/strong&gt;（如 &lt;strong&gt;CIE 1931 xy 色度图&lt;/strong&gt;，最常见的显示器色域可视化，除了1931还有很多版本，比如说1960UCS系列、1976系列、CIECAM系列）的边界是根据人眼对颜色的实际感知和混合结果、而不是简单的几何规则推算出来的。&lt;/li&gt;
&lt;li&gt;它追求的是“感知均匀性”——即色彩空间的距离对应人眼感知的色差，空间中距离相等的两点，人的感知色差也尽量相等。&lt;/li&gt;
&lt;li&gt;红色和绿色在这两个色彩空间中差异最大，也就是说如果采用蓝色调作为界面主色的话，在不同的色彩空间设备上差异会小一些，相反，采用红色和绿色，表现差异会比较大，越是饱和度高的红、绿色，差异越明显。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/163c8778.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;为什么都是红色，相同的数值（如P3，sRGB都是0~255的RGB表示），在CIE的坐标映射却不同？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;sRGB、P3等色彩空间的红绿蓝基色的CIE xy坐标&lt;/strong&gt;，其实是由其标准定义直接给出的，来源于它们的物理“基色光”的CIE 1931 xy色度坐标。&lt;s&gt;&lt;strong&gt;（直接去CIE委员会查坐标就行了）   还有P3和sRGB其实用的数据共通只是显示器协议&lt;/strong&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;换而言之，每个RGB色彩空间（比如sRGB、P3、AdobeRGB）都规定了其R、G、B三基色的CIE xyY坐标（通常Y=1，因为y代表亮度，这个已经不是我们色域考虑的范围），再加上白点（D65等），这样就能唯一确定该空间的全部色域。&lt;/p&gt;
&lt;h1&gt;色彩的数据结构&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;色深（bit depth）在数据结构上如何体现&lt;/strong&gt;，其实就是图像文件、帧缓冲、内存等如何“用多少比特来存每个像素/通道的颜色值”。&lt;/p&gt;
&lt;h2&gt;1. &lt;strong&gt;单个像素的数据结构&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;A. 8bit色深的RGB图片（最常见）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个像素有3个通道（R、G、B），每个通道用8位（1字节）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个通道能表达0~255（256种）数值。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;一个像素一共占3字节（24位）。&lt;/li&gt;
&lt;li&gt;数据排列方式通常是交错（R, G, B, R, G, B...）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例（像素值）：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;R&lt;/th&gt;
&lt;th&gt;G&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;二进制数据：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;``plain
01001110 10000001 11111111&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;


### **B. 10bit、12bit色深的RGB图片（高端显示/渲染/视频）**
+ 每通道用10位或12位，能表示1024或4096级。
+ 数据排列： 
    - 常见存储方式是16位对齐（即每通道用16位，但高位有效，低位补0）。
    - 也有“打包存储”：比如30位图像（R10G10B10A2），4字节打包3个10bit加2bit alpha。

**示例（10bit每通道）：**

| R | G | B |
| --- | --- | --- |
| 278 | 800 | 1023 |


**二进制数据（打包方式，举例）：**

``plain
000100010110 1100100000 1111111111
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;C. 16bit、32bit色深（高精度浮点）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每通道用16位无符号整数（0~65535），或者32位float（如OpenEXR格式）。&lt;/li&gt;
&lt;li&gt;用于HDR渲染、科学图像、电影特效等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. &lt;strong&gt;整个图像的数据结构&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;以&lt;strong&gt;2x2像素RGB 8bit&lt;/strong&gt;图片为例：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(R0,G0,B0)&lt;/td&gt;
&lt;td&gt;(R1,G1,B1)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(R2,G2,B2)&lt;/td&gt;
&lt;td&gt;(R3,G3,B3)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;内存中数据排列（交错存储）：&lt;/p&gt;
&lt;p&gt;R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3&lt;/p&gt;
&lt;p&gt;总共需要2×2×3=12字节。&lt;/p&gt;
&lt;h2&gt;3. &lt;strong&gt;色深在文件格式中的表现&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PNG&lt;/strong&gt;：支持8bit和16bit色深（每通道）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JPEG&lt;/strong&gt;：大多数只支持8bit（有部分高端JPEG支持12bit）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIFF&lt;/strong&gt;：支持8/16/32bit，甚至浮点数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenEXR&lt;/strong&gt;：支持16bit半精度浮点、32bit浮点、高端影视通用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BMP&lt;/strong&gt;：8/16/24/32bit，各种排列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raw（相机底片）&lt;/strong&gt;：常见12bit、14bit、16bit，通常是单通道bayer阵列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. &lt;strong&gt;显卡/显示器/操作系统的色深支持&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;帧缓冲&lt;/strong&gt;（&lt;strong&gt;framebuffer&lt;/strong&gt;）&lt;strong&gt;可以是24bit（8bit×3）、30bit（10bit×3）、36bit等。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HDMI、DisplayPort&lt;/strong&gt;等接口支持10bit、12bit输出（取决于显示器和显卡）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示器&lt;/strong&gt;也分8bit、10bit、12bit面板。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;色深在数据结构上，就是每个颜色通道用多少位来存储。8bit每通道就是1字节，10bit/12bit通常用16位对齐，16bit/32bit直接用高精度数值。色深越高，单像素占用空间越大，能表达的颜色级数越多。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;色彩的传输&lt;/h1&gt;
&lt;h2&gt;1. &lt;strong&gt;基本流程概述&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用输出 RGB 数值（通常是 sRGB 空间，gamma 编码）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统或图形驱动进行色彩空间转换（如 sRGB → P3/Rec.2020/显示器色彩空间）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进行 gamma/degamma 处理（线性化和显示端编码）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送到显示器，显示器解读并显示实际的光学颜色&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;2. &lt;strong&gt;详细流程分解&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;A. 应用程序输出（一般为 sRGB）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;图片、网页、视频等，通常以 sRGB 编码的 RGB 数值输出。&lt;/li&gt;
&lt;li&gt;这些数值是&lt;strong&gt;经过 gamma 编码的&lt;/strong&gt;（非线性，适合存储和人的感知）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;B. 操作系统/色彩管理系统处理&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;1）&lt;strong&gt;degamma：sRGB → 线性空间（Linear RGB）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;首先，把 sRGB 的 gamma 编码 RGB 数值“解码”为&lt;strong&gt;线性光强度&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这一步叫&lt;strong&gt;degamma&lt;/strong&gt;或&lt;strong&gt;gamma解码&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;公式如下（sRGB为例）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;如果 C_srgb &amp;lt;= 0.04045:
    C_linear = C_srgb / 12.92
否则:
    C_linear = ((C_srgb + 0.055) / 1.055) ^ 2.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 C_srgb ∈ [0,1]。&lt;/p&gt;
&lt;p&gt;gamma和degamma是描述 &lt;strong&gt;亮度信号与显示亮度之间非线性关系&lt;/strong&gt; 的关键参数。它通过调整输入信号（如数字图像中的RGB值）与输出亮度之间的映射关系。&lt;/p&gt;
&lt;p&gt;但其实gamma设计的初衷是为了适配CRT显示器，CRT显示器的特性就是亮度不随电压的变化而线性变化。因此在传输到显示器之前还要做一遍gamma，也就是gamma2.2（sRGB标准）.反过来，还有gamma0.45（用于degamma恢复到线性响应）。因为现在的显示器已经可以基本做到&lt;strong&gt;线性的亮度显示&lt;/strong&gt;，而现代操作系统仍然保持gamma工作流（即从主机端输出到显示器端仍然是&lt;strong&gt;被gamma后&lt;/strong&gt;的色彩数据），因此显示器端要做一遍degamma。&lt;/p&gt;
&lt;h4&gt;2）&lt;strong&gt;色彩空间变换（色域映射）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;把线性 sRGB 转换为显示器的色彩空间（比如显示器是 P3、Rec.709、Rec.2020、AdobeRGB等）。&lt;/li&gt;
&lt;li&gt;这一步通过&lt;strong&gt;矩阵变换&lt;/strong&gt;完成：&lt;br /&gt;
&lt;code&gt;线性sRGB → 线性显示器RGB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;变换用的是不同空间的基色矩阵（色度坐标）和白点适配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3）&lt;strong&gt;gamma 编码&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;线性显示器 RGB 需要“重新编码”（gamma），以适配显示器的电子特性和人眼感知。&lt;/li&gt;
&lt;li&gt;比如普通 SDR 显示器用 sRGB gamma，HDR 显示器用 PQ（ST2084）、HLG等。&lt;/li&gt;
&lt;li&gt;这一步叫&lt;strong&gt;gamma编码&lt;/strong&gt;或&lt;strong&gt;EOTF（Electro-Optical Transfer Function）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 gamma 编码（以 sRGB 为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;如果 C_linear &amp;lt;= 0.0031308:
    C_display = C_linear * 12.92
否则:
    C_display = 1.055 * (C_linear ^ (1/2.4)) - 0.055
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4）&lt;strong&gt;Dithering/Bit-depth 降位&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;有时候还会做抖动处理，以便更好地显示低位深色彩。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;C. 信号传输到显示器&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;最终的 RGB 数值（已编码，适配显示器色域和 gamma）通过 HDMI/DP 等传给显示器。&lt;/li&gt;
&lt;li&gt;显示器解读信号，发光显示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. &lt;strong&gt;流程图简化&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;[应用输出(sRGB, gamma编码)]
          |
degamma，先统一转换为线性空间计算
（Windows因为软件支持不同，由系统收集应用显示后统一在系统层WCS/DWM转换）
（iOS系列则在架构设计之初就会启用全局色彩管理，即：在构建应用时苹果就已经嵌入色彩转换引擎ColorSync）
          ↓
[framebuffer上所有出现的应用都将遍历以上两步，将所有色彩空间转为线性空间]
          |
色彩空间变换（如果有不同色域出现在屏幕上则在这一步进行统一的色彩重映射）
          ↓
统一gamma压缩，将数据传输到显示器
          |
显示器接收到gamma后的色彩
          ↓
[显示器RGB, degamma编码]
          |
显示器显示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/1759d84d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4. &lt;strong&gt;实际例子&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Photoshop 打开一张 sRGB 图片，在 P3 屏幕上查看：
&lt;ol&gt;
&lt;li&gt;Photoshop 输出 sRGB (gamma编码)&lt;/li&gt;
&lt;li&gt;操作系统 degamma → 线性sRGB&lt;/li&gt;
&lt;li&gt;线性sRGB → 线性P3（色彩空间变换）&lt;/li&gt;
&lt;li&gt;gamma编码 → P3 gamma（或保持线性，视显示器而定）&lt;/li&gt;
&lt;li&gt;输出到显示器，正确显示原色&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. &lt;strong&gt;补充说明&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;色彩空间管理&lt;/strong&gt;（ICC Profile）：操作系统和应用程序要正确读取、嵌入、转换色彩空间信息（profile），否则会色偏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未管理流程&lt;/strong&gt;：如果没有色彩管理（很多PC默认如此），sRGB数值直接发到P3或其他色域显示器，会导致“溢色”或“错误显示”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. &lt;strong&gt;关键词解释&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;gamma&lt;/strong&gt;：一种将线性光强度非线性编码为接近人眼感知的数值的数学函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;degamma&lt;/strong&gt;：将非线性数值还原为线性光强度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;色彩空间变换&lt;/strong&gt;：不同色域之间的RGB值变换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EOTF/OETF&lt;/strong&gt;：光电/电光转移函数，描述信号和实际亮度的关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;操作系统的色彩管理流程，会先把应用输出的 gamma 编码 RGB 数值“degamma”成线性空间，再做色彩空间变换，最后重新 gamma 编码后输出到显示器，确保在不同色域和显示特性下，色彩能准确还原。&lt;/p&gt;
&lt;h2&gt;Tips：从主机到显示器会传输什么数据？&lt;/h2&gt;
&lt;p&gt;主机通过&lt;strong&gt;显卡&lt;/strong&gt;（GPU）把帧缓冲区里的像素数据输出到显示器，主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;像素 RGB 数值流&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;每个像素的 RGB（或 YCbCr，也称为YUV我们的采集卡经常使用。Y（亮度）、Cb（蓝色色度）、Cr（红色色度））数值，通常是 8bit/10bit/12bit，色彩空间、gamma 编码方式由上下文决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信号格式&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;比如 HDMI、DisplayPort、DVI、USB-C Alt Mode 等。&lt;/li&gt;
&lt;li&gt;协议规定了分辨率、刷新率、色深、色彩格式（RGB/YCbCr）、压缩方式等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步信号&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;时序信号（VSync、HSync、DE），确保图像正确显示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部分元数据&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;比如 HDR 标记、色度子采样信息、颜色量化范围（全/限幅）等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;1. EDID（显示器能力描述）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EDID&lt;/strong&gt;（Extended Display Identification Data）是显示器通过 I²C 通道（DDC/CI）向主机报告“我能支持什么”的标准协议。&lt;/li&gt;
&lt;li&gt;包含：
&lt;ul&gt;
&lt;li&gt;支持的分辨率、刷新率、色深、色彩格式&lt;/li&gt;
&lt;li&gt;支持的色域（如 sRGB、AdobeRGB、P3、BT.2020）、色度坐标&lt;/li&gt;
&lt;li&gt;支持的 HDR 标准（如 HDR10、Dolby Vision）、EOTF（Gamma/PQ/HLG 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;主机读取EDID流程&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;操作系统启动/显示设备热插拔时，显卡读取显示器 EDID 信息&lt;/li&gt;
&lt;li&gt;操作系统/驱动据此列出分辨率、刷新率、色深、色域等选项&lt;/li&gt;
&lt;li&gt;部分操作系统会自动根据 EDID 选择合适的色彩输出（如 macOS 优先输出 P3）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;2. 色域握手的历史和现状&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统 SDR 环境（sRGB 时代）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;大多数显示器只支持 sRGB，EDID 并不强调色域&lt;/li&gt;
&lt;li&gt;主机默认以 sRGB 方式输出，广色域显示器通常手动切换模式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现代广色域/高端显示器&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;EDID 里会声明 P3、AdobeRGB、Rec.2020 等支持情况&lt;/li&gt;
&lt;li&gt;新版 HDMI/DP 协议和 VESA DisplayID 标准允许更细致的色度坐标、色域声明&lt;/li&gt;
&lt;li&gt;主机可以据此自动优化输出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;3. HDR 和高级色域的元数据&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当启用 HDR 或 BT.2020 色域时，主机不仅通过 EDID 得知显示器能力，&lt;strong&gt;还会在视频流里插入 HDR 元数据（如 EOTF、色域主色点、白点等），显示器据此调整显示模式&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;比如 Windows 11、macOS、部分 Linux 桌面环境都已支持 HDR 色域和 EOTF 自动协商。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;4. 实际操作流程&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;主机读取显示器 EDID/DisplayID，了解其支持的分辨率、色深、色域等能力&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主机/操作系统选择合适的输出配置：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;比如输出 P3 10bit，或 sRGB 8bit，或 HDR BT.2020 10bit&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;（可选）操作系统/专业软件做色彩空间变换，把内容适配到目标色域&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主机编码好信号，通过 HDMI/DP 发送到显示器&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示器根据输入信号和自身设置，选择合适的显示模式（有的能自动切换色域/模式）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;为什么Gamma如今还存在？&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;其实就是船大难调头了你不可能在已经成熟的市场重新定义一个完全线性的显示流&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;以前Gamma的存在是因为CRT显示器的亮度并不随电压的变化而均匀变化；因此在传输视频信号给显示器的时候，主机端就要做一遍Gamma2.2（Linear to sRGB），使主机的输出Gamma视频能和CRT的显示劣势相抵消。&lt;/p&gt;
&lt;p&gt;虽然现代显示屏已经实现了将视频信号几乎以线性亮度显示出来，但是因此还要说到Gamma的另一个特性——暗部的低压缩。&lt;/p&gt;
&lt;h2&gt;人眼的非线性感知&lt;/h2&gt;
&lt;p&gt;其实这是在使用Gamma抵消CRT显示器显示缺陷的副产物——Gamma矫正会使暗部的灰阶被拉伸，而亮部的灰阶会被压缩。&lt;/p&gt;
&lt;p&gt;但是人眼对亮度的感知是非线性的：&lt;strong&gt;在暗部区域，人眼对亮度变化的敏感度更高&lt;/strong&gt;，而在亮部区域，亮度变化对人眼的影响会减弱。&lt;/p&gt;
&lt;p&gt;RGB的每个通道（红、绿、蓝）的取值范围为0到255，这表示每个颜色通道有256个灰阶（0到255共256个值）。如果用人眼的范围感知打比方则有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;暗部敏感&lt;/strong&gt;：在暗部区域，亮度的微小变化（如从1到2）会显著影响视觉感知。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;亮部迟钝&lt;/strong&gt;：在亮部区域，亮度的大幅变化（如从200到255）对视觉感知的影响较小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Gamma校正的补偿作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过非线性映射，Gamma校正将更多灰阶分配到人眼敏感的暗部区域。例如，将256个灰阶中的一部分（如前100个灰阶）用于暗部，而仅用少量灰阶（如后50个）覆盖亮部。这样，在有限的256个灰阶中，暗部能呈现更丰富的细节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用数学方式来表达Gamma的灰阶拉伸作用的话，有：&lt;/p&gt;
&lt;p&gt;假设Gamma值为2.2（常见标准），输入线性亮度值为 &lt;img src=&quot;/images/posts/d3a6c2bc.svg&quot; alt=&quot;image&quot; /&gt;，输出Gamma编码值为 &lt;img src=&quot;/images/posts/a098efef.svg&quot; alt=&quot;image&quot; /&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/c5a412d0.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;暗部&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;假设线性亮度 &lt;img src=&quot;/images/posts/6c4db7a8.svg&quot; alt=&quot;image&quot; /&gt;（10%亮度），则Gamma编码值为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/57fb659b.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在Gamma空间中，0.1的亮度被映射到像素值91，而非线性空间中的0.1亮度对应像素值25.5（25.5 × 0.1 = 2.55）。显然，Gamma空间中暗部的灰阶被拉伸了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;亮部&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;线性亮度 &lt;img src=&quot;/images/posts/607d6716.svg&quot; alt=&quot;image&quot; /&gt;（90%亮度），Gamma编码值为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/0ca41ed7.svg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在Gamma空间中，0.9的亮度仅映射到像素值242，而非线性空间中的0.9亮度对应像素值229.5。亮部的灰阶被压缩了。&lt;/p&gt;
&lt;h2&gt;带宽的压缩&lt;/h2&gt;
&lt;p&gt;我们知道通过非线性映射（如Gamma值2.2），将暗部灰阶拉伸，亮部灰阶压缩(我们用sRGB 8bit来做比方，即灰阶由RGB加权后。但这个压缩的gamma具体的色阶分配是多少？又压缩了多少？&lt;/p&gt;
&lt;p&gt;假设图像为8位通道（256级灰阶），亮度范围为0-1（0=黑，1=白）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;亮度区间&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;未使用Gamma校正（线性）&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;使用Gamma校正（Gamma=2.2）&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;暗部（0-0.2）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;占256级中的51级（0-51）&lt;/td&gt;
&lt;td&gt;占256级中的约150级（0-150）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;中灰（0.2-0.5）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;占256级中的76级（52-127）&lt;/td&gt;
&lt;td&gt;占256级中的约80级（151-230）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;亮部（0.5-1.0）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;占256级中的129级（128-255）&lt;/td&gt;
&lt;td&gt;占256级中的约26级（231-256）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;而实际上我们日常的显示几乎是高灰阶为主的，比如我们平时的浏览器；&lt;strong&gt;人们对高灰阶的感知并不明显，经过gamma压缩后高灰阶的数据量也被减少了，这也就变相减少了主机到显示器的数据传输量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实这个做法也和摄影圈“&lt;strong&gt;向右曝光&lt;/strong&gt;”的思路一致：因为人眼对于暗部细节更加敏感，所以向右曝光至少不会产生欠曝（因为曝光过低导致暗部拉不回来的叫法），&lt;strong&gt;保留暗部细节&amp;gt;保留亮部细节。这句话用在Gamma的作用也同样适用&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;ICC配置文件——文件的自身色彩声明&lt;/h1&gt;
&lt;p&gt;如果我们在打开一个图像文件的时候，在查看元数据的时候可以看到这一行：&lt;/p&gt;
&lt;p&gt;&lt;a&gt;photoshop:ICCProfile&lt;/a&gt;sRGB IEC61966-2.1 (线性 RGB 配置文件)&amp;lt;/photoshop:ICCProfile&amp;gt;&lt;/p&gt;
&lt;p&gt;这就是嵌入到文件中的ICC Profile，用于告诉系统要如何处理这个色域/degamma的数值。&lt;/p&gt;
&lt;p&gt;其实一般情况下ICC配置文件并没有什么用，&lt;strong&gt;因为大家都是约定俗成的使用sRGB&lt;/strong&gt;，包括所有网站的图像压缩都会自动转换成为sRGB格式，有用的情况是在高色域显示器进行创作编辑时可以直接读取并且直接渲染到屏幕上。&lt;/p&gt;
&lt;h2&gt;ICC数据结构&lt;/h2&gt;
&lt;p&gt;当一个图像文件嵌入了 &lt;strong&gt;ICC Profile（ICC配置文件）&lt;/strong&gt; 时，该文件中会包含以下关键数据，用于描述图像的色彩特性并确保跨设备的颜色一致性：&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. 设备描述文本（Device Description）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设备类型&lt;/strong&gt;：说明该ICC配置文件是为哪种设备设计的（如显示器、打印机、扫描仪等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;制造商信息&lt;/strong&gt;：记录配置文件的创建者或设备制造商。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建时间&lt;/strong&gt;：配置文件生成的时间戳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校准条件&lt;/strong&gt;：例如显示器的白点色温（如D65）、亮度、环境光条件等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：帮助用户和软件识别配置文件的来源及适用性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;2. 颜色空间与色域信息&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;色彩空间&lt;/strong&gt;：描述图像数据所基于的色彩空间（如 sRGB、Adobe RGB、ProPhoto RGB 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;色域（Color Gamut）&lt;/strong&gt;：通过 &lt;strong&gt;CIE XYZ 或 Lab&lt;/strong&gt; 坐标表示设备或图像能再现的颜色范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白点（White Point）&lt;/strong&gt;：定义设备的白色参考值（如 D65 表示色温 6500K）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gamma 曲线&lt;/strong&gt;：描述设备对亮度的响应关系（如 sRGB 的 Gamma=2.2）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：确保图像在不同设备上以相同的色彩空间和色域呈现。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;3. 颜色转换规则&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设备到 PCS 的转换&lt;/strong&gt;：将设备色彩空间（如 RGB）转换为标准色彩空间（如 CIE XYZ 或 Lab）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PCS 到设备的转换&lt;/strong&gt;：将标准色彩空间转换回目标设备色彩空间（如 CMYK）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;颜色转换表（LUT）&lt;/strong&gt;：通过表格形式存储输入值到输出值的映射关系（如 3D LUT 或 1D LUT）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：实现图像颜色在不同设备间的精确转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;4. 渲染意图（Rendering Intent）&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Perceptual（感知意图）&lt;/strong&gt;：保持颜色之间的视觉关系，适合图像和照片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relative Colorimetric（相对色度意图）&lt;/strong&gt;：保留可再现的颜色，超出目标色域的颜色裁剪或替换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Absolute Colorimetric（绝对色度意图）&lt;/strong&gt;：严格匹配白点，用于特定行业（如医疗影像）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saturation（饱和度意图）&lt;/strong&gt;：优先保持颜色的鲜艳度，适合图表和图形。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：定义颜色转换时的策略，处理色域不匹配的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;5. 校准数据&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;校准测量值&lt;/strong&gt;：通过专业校色工具（如 Datacolor Spyder）测量设备的色彩特性后生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D LUT（查找表）&lt;/strong&gt;：高级配置文件可能包含 3D LUT，直接定义输入 RGB 值到输出 RGB 值的映射关系。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：确保图像颜色在设备上准确再现（如显示器校准后的颜色校正）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;6. 特定设备信息&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入设备&lt;/strong&gt;：如扫描仪或数码相机的色彩捕捉能力（如色深、动态范围）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出设备&lt;/strong&gt;：如打印机的墨水特性（如光泽纸与哑光纸的色差）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示设备&lt;/strong&gt;：如显示器的亮度、对比度、色域覆盖范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：描述设备的物理特性，确保颜色在设备间传递的一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;7. 标准化格式与兼容性&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ICC 标准格式&lt;/strong&gt;：遵循 ICC 于 1995 年制定的规范（ICC.1:2001-12）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件扩展名&lt;/strong&gt;：通常为 &lt;code&gt;.icc&lt;/code&gt; 或 &lt;code&gt;.icm&lt;/code&gt;（Windows）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：确保跨平台（Windows、macOS）和跨设备（RGB、CMYK、扫描仪等）的通用性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;8. 示例：嵌入的 ICC Profile 数据&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;假设一个图像文件嵌入了 &lt;strong&gt;sRGB IEC61966-2.1&lt;/strong&gt; 配置文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;色域&lt;/strong&gt;：sRGB 标准色域（覆盖约 35% 的 NTSC 色域）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白点&lt;/strong&gt;：D65（6500K 色温）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gamma 曲线&lt;/strong&gt;：Gamma=2.2。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：广泛用于网页、电子设备，默认支持所有浏览器和操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;9. 嵌入 ICC Profile 的实际作用&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;确保颜色一致性&lt;/strong&gt;：嵌入的 ICC Profile 会告诉软件（如 Photoshop、浏览器）图像数据基于哪种色彩空间，从而在不同设备上正确解释颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免色差&lt;/strong&gt;：例如，若图像基于 Adobe RGB 色域，而显示器仅支持 sRGB，嵌入的 ICC Profile 会提示软件进行色域转换，避免颜色失真。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持专业工作流&lt;/strong&gt;：摄影师、设计师通过嵌入 ICC Profile，确保图像从拍摄到打印的全链路颜色一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;10. 常见嵌入场景&lt;/strong&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;文件类型&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;是否支持嵌入 ICC Profile&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;典型用途&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PSD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;专业设计、摄影后期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TIFF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;高精度图像存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JPEG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅（部分压缩可能丢失数据）&lt;/td&gt;
&lt;td&gt;网络图片、社交媒体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PNG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;无损压缩图像&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;嵌入的 ICC Profile 包含了 &lt;strong&gt;设备色彩特性描述、颜色转换规则、渲染意图、校准数据&lt;/strong&gt; 等核心信息。它通过标准化格式（如 &lt;code&gt;.icc&lt;/code&gt;）确保图像在不同设备（显示器、打印机、扫描仪）之间传递时，颜色表现一致，是专业设计、摄影和印刷领域不可或缺的工具。&lt;/p&gt;
&lt;h1&gt;Windows屏幕的多软件色彩管理&lt;/h1&gt;
&lt;p&gt;在Windows，由于没有苹果的强制标准，~~所有人都可以插上一脚 ~~不同软件对于色彩理解有不同的显示方法。这就要聊到我们高贵的渲染管线了&lt;/p&gt;
&lt;p&gt;一般来说，Framebuffer（储存于显存）desktop的一般是24位RGB+8位Alpha通道，合计32位深度，也就是说我们传统的8bit。当然可以自适应改善成为40位（4x10bit）、48位（4x12bit）。&lt;/p&gt;
&lt;p&gt;但是在Windows中，&lt;strong&gt;CPU在提交渲染信息给GPU之前，WDDM会查询所有在屏幕中的软件渲染API是否有声明这个软件的色彩空间&lt;/strong&gt;。具体也就是在DirectX中写出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DXGI_COLOR_SPACE_TYPE colorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; // sRGB
swapChain-&amp;gt;SetColorSpace1(colorSpace);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;支持的色彩空间类型&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709&lt;/code&gt;（sRGB）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709&lt;/code&gt;（Adobe RGB）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709&lt;/code&gt;（DCI-P3）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_YCBCR709&lt;/code&gt;（BT.709）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_YCBCR601&lt;/code&gt;（BT.601）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020&lt;/code&gt;（HDR10 / BT.2020`）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DWM 的处理流程&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用声明色彩空间&lt;/strong&gt;：通过 &lt;code&gt;SetColorSpace1&lt;/code&gt; 告知 DWM 输出的色彩空间（如 HDR10）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DWM 合成阶段&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;将应用输出的色彩空间转换为 &lt;strong&gt;CCCS（规范合成色彩空间）&lt;/strong&gt;（scRGB + FP16 线性伽马）。&lt;/li&gt;
&lt;li&gt;再将 CCCS 转换为显示器的物理色彩空间（如 BT.2020 或 ST.2084）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终输出&lt;/strong&gt;：通过 GPU 显式控制显示器的色彩输出，确保颜色准确性。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;(2) 实际效果&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HDR 应用&lt;/strong&gt;：如果应用声明为 HDR10（&lt;code&gt;DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020&lt;/code&gt;），DWM 会启用 HDR 模式，使用 ST.2084 EOTF 进行转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDR 应用&lt;/strong&gt;：如果应用声明为 sRGB，DWM 会使用 sRGB 伽马（Gamma=2.2）进行合成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;在应用内实现色彩空间转换（Bypass DWM）&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果希望完全绕过 DWM 的色彩管理，应用可以在自身内部实现完整的色彩空间转换逻辑，并直接输出到显示器。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;全屏独占模式（Full-Screen Exclusive Mode）&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;应用通过 DirectX 的 &lt;strong&gt;全屏独占模式&lt;/strong&gt; 直接控制显示器，绕过 DWM 的合成阶段。&lt;/li&gt;
&lt;li&gt;此模式下，应用可以完全控制帧缓冲区的色彩空间和输出格式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;这对吗？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;实则不然&lt;/strong&gt;。这个说法在历史上&lt;strong&gt;部分正确&lt;/strong&gt;，但现在已经&lt;strong&gt;基本过时&lt;/strong&gt;了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统全屏模式（Exclusive Fullscreen）的确实情况：&lt;/strong&gt;&lt;br /&gt;
在Windows 7/8时代，独占全屏模式确实可以绕过很多系统层面的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统的独占全屏
DXGI_SWAP_CHAIN_DESC desc = {};
desc.Windowed = FALSE;  // 独占全屏
desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时应用直接控制显示器，DWM被绕过，色彩管理也相对简单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现代情况的变化：&lt;/strong&gt;&lt;br /&gt;
从Windows 10开始，为了能支持未来的HDR显示器，并且适配新的Dither功能等新技术（比如我们常说的显示器8抖10，8bit用dithering升格到10bit），情况发生了重大变化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fullscreen Optimizations&lt;/strong&gt;: 即使是&quot;全屏&quot;应用，系统也可能将其作为全屏窗口运行，仍然通过DWM处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DWM色彩管理增强&lt;/strong&gt;: 现代DWM有更完善的色彩管理，即使在全屏模式下也会参与色彩处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HDR和Wide Color支持&lt;/strong&gt;: 系统需要在全屏模式下也能正确处理HDR/WCG内容&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;专业应用的实际做法：&lt;/strong&gt;&lt;br /&gt;
现代专业软件更依赖于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正确的DirectX DXGI色彩空间声明&lt;/li&gt;
&lt;li&gt;与系统色彩管理的协作而非对抗&lt;/li&gt;
&lt;li&gt;硬件级的校色bypass（通过显卡驱动扩展）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以简单的&quot;开全屏就能bypass色彩管理&quot;这种说法现在并不可靠，特别是对于需要精确色彩的专业应用。&lt;/p&gt;
&lt;h1&gt;扩展：HDR&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SDR&lt;/th&gt;
&lt;th&gt;HDR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;峰值亮度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100-400 nits&lt;/td&gt;
&lt;td&gt;400-10,000+ nits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;位深度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8位 (偶尔10位)&lt;/td&gt;
&lt;td&gt;10位/12位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;色彩空间&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rec.709 (sRGB)&lt;/td&gt;
&lt;td&gt;Rec.2020 (DCI-P3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传递函数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gamma 2.4&lt;/td&gt;
&lt;td&gt;PQ (SMPTE ST 2084) / HLG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;动态范围&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~6-7档光圈&lt;/td&gt;
&lt;td&gt;10-14档光圈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;对比度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,000:1 - 5,000:1&lt;/td&gt;
&lt;td&gt;100,000:1 - 1,000,000:1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;编码方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;相对编码&lt;/td&gt;
&lt;td&gt;PQ:绝对编码 / HLG:相对编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;显示器握手&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无需握手，手动调节亮度&lt;/td&gt;
&lt;td&gt;需要EDID信息交换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;色调映射&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;td&gt;需要Tone mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;向后兼容&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;HDR显示器可显示SDR内容(HLG编码)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;文件大小&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;相对较小&lt;/td&gt;
&lt;td&gt;更大 (更高位深度)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;制作成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较低&lt;/td&gt;
&lt;td&gt;较高 (需要HDR监视器等)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;主要标准&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rec.709，约等于sRGB&amp;lt;br/&amp;gt;AdobeRGB, Display-P3(iOS)&lt;/td&gt;
&lt;td&gt;Rec.2020,DCI-P3(色域标准)&amp;lt;br/&amp;gt;HDR10, HDR10+, Dolby Vision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;广播应用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;传统电视广播&lt;/td&gt;
&lt;td&gt;新一代广播 (HLG主导)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;流媒体&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用支持&lt;/td&gt;
&lt;td&gt;Netflix, YouTube等支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;游戏支持&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;全平台支持&lt;/td&gt;
&lt;td&gt;PS5, Xbox Series, PC支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;显示设备要求&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;普通显示器&lt;/td&gt;
&lt;td&gt;支持HDR的显示器/电视&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键差异总结&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HDR最大优势是亮度范围和色彩表现力&lt;/li&gt;
&lt;li&gt;SDR更成熟，兼容性更好&lt;/li&gt;
&lt;li&gt;HDR需要完整的端到端支持链条&lt;/li&gt;
&lt;li&gt;两者可以共存，HDR设备通常向下兼容SDR&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了HDR和SDR在数据结构上还有几个关键区别：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;位深度差异&lt;/strong&gt;：SDR通常使用8位或10位编码，而HDR一般需要10位或12位来表示更大的动态范围。更高的位深度允许HDR存储更多的亮度级别，避免在高动态范围下出现色带问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;色彩空间&lt;/strong&gt;：SDR主要使用Rec.709色彩空间，而HDR使用更广的色彩空间如Rec.2020，能够表示更丰富的颜色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传递函数&lt;/strong&gt;：这是最核心的区别。SDR使用Gamma 2.4编码，而HDR使用PQ（SMPTE ST 2084）或HLG（Hybrid Log-Gamma）编码。&lt;/p&gt;
&lt;p&gt;关于编码实现：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gamma编码&lt;/strong&gt;：SDR的Gamma 2.4是一个简单的幂函数，将线性光信号压缩到0-1范围。公式大致是 V = L^(1/2.4)，其中L是线性亮度值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PQ编码&lt;/strong&gt;：PQ是感知量化器，专门为人眼视觉系统设计。它使用复杂的数学函数将0到10000 nits的亮度范围映射到0-1的编码值。PQ曲线基于人眼对不同亮度的敏感度，在暗部分配更多编码值，亮部相对较少。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HLG编码&lt;/strong&gt;：HLG结合了传统Gamma和对数编码的优势。在低亮度区域使用类似Gamma的平方根函数，在高亮度区域切换到对数编码。这种混合方式既保持了与SDR的向后兼容性，又能处理高动态范围内容。&lt;/p&gt;
&lt;p&gt;PQ是绝对编码，每个编码值对应固定的物理亮度；而HLG是相对编码，需要根据显示设备的峰值亮度来解释编码值。这使得HLG在广播应用中更加实用。&lt;/p&gt;
&lt;h1&gt;扩展：LutsLUT（Look-Up Table）完全解析：从原理到GPU实现&lt;/h1&gt;
&lt;h2&gt;什么是LUT？&lt;/h2&gt;
&lt;p&gt;LUT（Look-Up Table，查找表）是色彩处理中的核心工具，本质上是一个&lt;strong&gt;三维色彩映射表&lt;/strong&gt;。它将输入的RGB值直接映射到输出的RGB值，实现快速而精确的色彩变换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;基本流程：输入RGB → LUT查表 → 输出RGB
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LUT的数据结构&lt;/h2&gt;
&lt;p&gt;以常见的32×32×32 LUT为例：&lt;/p&gt;
&lt;h3&gt;逻辑结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;维度&lt;/strong&gt;：R、G、B三个维度，每维32个采样点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总容量&lt;/strong&gt;：32 × 32 × 32 = 32,768个预设映射点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据内容&lt;/strong&gt;：每个格子存储 输入(R,G,B) → 输出(R&apos;,G&apos;,B&apos;) 的映射关系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件大小&lt;/strong&gt;：通常几百KB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入处理：8位输入(0-255) → 缩放到LUT坐标(0-31)
例如：RGB(128,64,192) → LUT坐标(16,8,24)

查表映射：LUT[16][8][24] = (新R值, 新G值, 新B值)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GPU中的实现&lt;/h2&gt;
&lt;p&gt;虽然逻辑上是三维结构，但在GPU中有更巧妙的实现方式：&lt;/p&gt;
&lt;h3&gt;存储方式：2D纹理数组&lt;/h3&gt;
&lt;p&gt;32×32×32的LUT实际存储为&lt;strong&gt;32张32×32的2D纹理&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Blue=0  → 第1张32×32纹理
Blue=1  → 第2张32×32纹理
...
Blue=31 → 第32张32×32纹理
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查找过程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一步：B值确定纹理层&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入RGB(R=128, G=64, B=192)
B值192 → 缩放到0-31 → B_index = 24
选择第24层纹理
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二步：RG值作为UV坐标&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R值128 → 缩放到0-1 → U = 0.502
G值64  → 缩放到0-1 → V = 0.251
在第24层纹理的UV(0.502, 0.251)位置采样
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三步：三线性插值&lt;/strong&gt;&lt;br /&gt;
为确保平滑过渡，需要在相邻B层间插值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// GLSL着色器实现
vec3 applyLUT(vec3 inputColor) {
    float b_index = inputColor.b * 31.0;
    int b_floor = int(b_index);
    int b_ceil = b_floor + 1;
    float b_weight = fract(b_index);
    
    vec2 uv = inputColor.rg;
    vec3 color1 = texture(lutTexture, vec3(uv, b_floor)).rgb;
    vec3 color2 = texture(lutTexture, vec3(uv, b_ceil)).rgb;
    
    return mix(color1, color2, b_weight);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，三线性插值同样可以在R和G（即UV采样中）实现。&lt;/p&gt;
&lt;h2&gt;LUT的实际应用&lt;/h2&gt;
&lt;h3&gt;电影调色&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原始素材&lt;/strong&gt;：平淡的原始RGB值&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LUT处理&lt;/strong&gt;：增强对比度、调整色温、营造氛围&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果&lt;/strong&gt;：具有电影级视觉效果的画面&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HDR到SDR的色调映射&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入：HDR (R=1024, G=512, B=256) [超出SDR范围]
LUT处理：智能压缩到SDR范围，保持视觉层次
输出：(R=240, G=180, B=120) [适合SDR显示]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;色彩空间转换&lt;/h3&gt;
&lt;p&gt;将不同色彩标准间的复杂转换预计算并存储在LUT中，实现实时转换。&lt;/p&gt;
&lt;h2&gt;LUT的优势与限制&lt;/h2&gt;
&lt;h3&gt;优势&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;处理速度快&lt;/strong&gt; - 直接查表，无需实时复杂计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精确度高&lt;/strong&gt; - 可以实现任意复杂的非线性调整&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果一致&lt;/strong&gt; - 同样输入永远得到相同输出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件友好&lt;/strong&gt; - GPU纹理采样天然适合LUT查找&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易于分发&lt;/strong&gt; - 调色师可以将复杂的调色方案打包成LUT文件&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;限制&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;精度限制&lt;/strong&gt; - 32×32×32只有32,768个采样点，可能产生色彩断层。但是可以进行改进，或者使用双线性/三线性插值算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件大小&lt;/strong&gt; - 更高精度的LUT（如64×64×64）占用更多存储空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单向映射&lt;/strong&gt; - LUT变换通常不可逆&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用&lt;/strong&gt; - 需要将整个LUT作为纹理加载到显存中&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>反射算法</title><link>https://fuwari.vercel.app/posts/reflection-algorithm/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/reflection-algorithm/</guid><description>Blender模型导入Unity的完整工作流程</description><pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://docs.unity3d.com/cn/2018.4/Manual/PostProcessing-ScreenSpaceReflection.html#:~:text=%E5%B1%8F%E5%B9%95%E7%A9%BA%E9%97%B4%E5%8F%8D%E5%B0%84%E6%98%AF%E7%94%A8%E4%BA%8E%E9%99%90%E5%88%B6%20%E9%95%9C%E9%9D%A2%E5%8F%8D%E5%B0%84%E5%85%89%20%E6%B3%84%E6%BC%8F%E9%87%8F%E7%9A%84%E7%90%86%E6%83%B3%E6%95%88%E6%9E%9C%E3%80%82%20%E5%B1%8F%E5%B9%95%E7%A9%BA%E9%97%B4%E5%8F%8D%E5%B0%84%20%E6%95%88%E6%9E%9C%E6%9B%B4%E6%B3%A8%E9%87%8D%E6%80%A7%E8%83%BD%E8%80%8C%E9%9D%9E%E8%B4%A8%E9%87%8F%EF%BC%8C%E5%9B%A0%E6%AD%A4%E6%98%AF%E5%9C%A8%E6%9C%80%E6%96%B0%E6%AC%BE%E6%B8%B8%E6%88%8F%E4%B8%BB%E6%9C%BA%E5%92%8C%E6%A1%8C%E9%9D%A2%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%8A%E8%BF%90%E8%A1%8C%E7%9A%84%E9%A1%B9%E7%9B%AE%E7%9A%84%E7%90%86%E6%83%B3%E9%80%89%E6%8B%A9%E3%80%82%20%E6%AD%A4%E6%95%88%E6%9E%9C%E4%B8%8D%E9%80%82%E5%90%88%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%BC%80%E5%8F%91%E3%80%82,%E7%94%B1%E4%BA%8E%E6%AD%A4%E6%95%88%E6%9E%9C%E4%BE%9D%E8%B5%96%E4%BA%8E%E6%B3%95%E7%BA%BF%20G%20%E7%BC%93%E5%86%B2%E5%8C%BA%20%28Normals%20G-Buffer%29%EF%BC%8C%E5%9B%A0%E6%AD%A4%E4%BB%85%E5%9C%A8%20%E5%BB%B6%E8%BF%9F%E6%B8%B2%E6%9F%93%E8%B7%AF%E5%BE%84%20%E4%B8%AD%E5%8F%AF%E7%94%A8%E3%80%82&quot;&gt;屏幕空间反射 - Unity 手册&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.unity3d.com/cn/Packages/com.unity.render-pipelines.high-definition@10.4/manual/Planar-Reflection-Probe.html&quot;&gt;平面反射探针 (Planar Reflection Probe) | High Definition RP | 10.4.0&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;平面反射探针仅限于HDRP？这么离谱&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.blender.org/manual/zh-hans/4.2/render/shader_nodes/shader/glass.html?utm_source=blender-4.2.0#bpy-types-shadernodebsdfglass&quot;&gt;Just a moment...&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/5cc51a8a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>无厚度_低厚度的zfighting问题</title><link>https://fuwari.vercel.app/posts/%E6%97%A0%E5%8E%9A%E5%BA%A6_%E4%BD%8E%E5%8E%9A%E5%BA%A6%E7%9A%84zfighting%E9%97%AE%E9%A2%98/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%97%A0%E5%8E%9A%E5%BA%A6_%E4%BD%8E%E5%8E%9A%E5%BA%A6%E7%9A%84zfighting%E9%97%AE%E9%A2%98/</guid><description>Blender模型导入Unity的完整工作流程</description><pubDate>Mon, 06 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近在水qq群的时候遇到了一个问题：无厚度的披风zfighting问题。&lt;/p&gt;
&lt;p&gt;好的，我们来分析一下这个关于Unity中双面材质披风Z-fighting（Z冲突，也常被称作深度冲突）的问题。&lt;/p&gt;
&lt;p&gt;对话中的核心问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么一个两面颜色不同的披风，&lt;code&gt;Cull Back&lt;/code&gt; (背面剔除) 可以缓解Z-fighting？&lt;/li&gt;
&lt;li&gt;Z-fighting不是精度问题吗？怎么能用剔除来解决？&lt;/li&gt;
&lt;li&gt;为什么有些角色两边颜色不同的披风就不会出现这种情况？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;让我们一步步来解释。&lt;/p&gt;
&lt;h2&gt;理解Z-fighting&lt;/h2&gt;
&lt;p&gt;首先，我们要明白什么是Z-fighting。当两个或多个多边形（模型面片）在屏幕上的同一像素位置，并且它们的深度值（Z值，表示离摄像机的远近）非常接近甚至相同时，在光栅化阶段的ztest时，显卡就难以通过深度图来判断哪个面应该显示在前面。这会导致这些面片在该像素上交替闪烁，或者出现“缝合”一样的破碎效果，尤其在摄像机或物体移动时更为明显。这就是Z-fighting。 具体参见基本渲染管线：ztest发生在光栅化的片元着色后。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yuque.com/shuangfeidu/txwa8w/ixp5ze7hg1infbbm&quot;&gt;基本渲染管线&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;主要原因有两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;深度缓冲精度有限&lt;/strong&gt;：深度缓冲区（Z-buffer）用来存储每个像素的深度值，它的精度是有限的（比如16位、24位、32位浮点数）。当物体离摄像机非常远，或者摄像机的近裁剪面（Near Clip Plane）和远裁剪面（Far Clip Plane）设置的范围过大时，深度值的精度会下降，导致原本不同深度的表面被量化到相同的深度值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共面或几乎共面&lt;/strong&gt;：如果两个面片在几何上就是重叠的，或者非常非常接近，那么它们的深度值自然也会非常接近，容易引发Z-fighting。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;为什么&lt;code&gt;Cull Back&lt;/code&gt;可以缓解特定情况下的Z-fighting？&lt;/h2&gt;
&lt;p&gt;现在来解释这个情况：一个两面颜色不同的披风。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;双面渲染与Z-fighting&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果这个披风是用&lt;strong&gt;单个面片&lt;/strong&gt;（thin geometry/single-sided mesh）建模的，为了让它两面都显示不同颜色（或者都能被看见），你可能会在Shader中设置 &lt;code&gt;Cull Off&lt;/code&gt;（关闭剔除）。 &lt;a href=&quot;https://blog.csdn.net/qq_51603875/article/details/132585634&quot;&gt;[4]&lt;/a&gt;&lt;a href=&quot;https://docs.unity.cn/cn/2021.2/ScriptReference/Rendering.CullMode.html&quot;&gt;[5]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;当设置为 &lt;code&gt;Cull Off&lt;/code&gt; 时，同一个面片的“正面”和“背面”都会被尝试渲染。关键在于，对于一个无限薄的单个面片来说，它的“正面”和“背面”在空间中的位置是&lt;strong&gt;完全相同的&lt;/strong&gt;。这意味着它们的深度值也是完全相同的。&lt;/li&gt;
&lt;li&gt;此时，GPU在渲染这个面片时，正面和背面都在争抢同一个像素的写入权，因为它们的深度值一样。这就直接导致了Z-fighting。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cull Back&lt;/code&gt;&lt;strong&gt;的作用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cull Back&lt;/code&gt; 的意思是“剔除背面”。在Unity（以及大多数3D引擎）中，默认情况下，多边形的顶点是按特定顺序（通常是顺时针或逆时针，称为Winding Order）定义的，这个顺序决定了哪个是正面，哪个是背面。&lt;/li&gt;
&lt;li&gt;当你将剔除模式设置为 &lt;code&gt;Cull Back&lt;/code&gt; 时，只有面片的正面会朝向摄像机时才会被渲染，背面则会被丢弃不渲染。 &lt;a href=&quot;https://blog.csdn.net/qq_51603875/article/details/132585634&quot;&gt;[4]&lt;/a&gt;&lt;a href=&quot;https://docs.unity.cn/cn/2021.2/ScriptReference/Rendering.CullMode.html&quot;&gt;[5]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;对于你那个单面建模的披风，如果开启了 &lt;code&gt;Cull Back&lt;/code&gt;，那么当摄像机看到披风的“正面”时，只有正面被渲染；当摄像机转到能看到披风“背面”的角度时（此时原来的“正面”就成了背向摄像机的面），这个面片因为是“背面”（相对于其法线方向和剔除设置）而不会被渲染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所以，&lt;/strong&gt;&lt;code&gt;Cull Back&lt;/code&gt;** 实际上是阻止了同一个面片的两个“虚拟”的、在同一深度的面（正面和背面）同时参与渲染竞争。它并没有改变深度缓冲区的精度，而是通过只渲染一面，直接消除了冲突的根源。**&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精度问题怎么能用剔除解决的？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;严格来说，剔除并没有“解决”深度缓冲区的精度问题本身。深度缓冲区的精度依然是那么多。&lt;/li&gt;
&lt;li&gt;但是，对于由 &lt;code&gt;Cull Off&lt;/code&gt; 导致的、&lt;strong&gt;同一个几何体的正反两面在完全相同深度上竞争&lt;/strong&gt;所引发的Z-fighting，剔除（比如&lt;code&gt;Cull Back&lt;/code&gt;或&lt;code&gt;Cull Front&lt;/code&gt;）通过只允许其中一个面参与渲染，从而避免了这场“战斗”。如果只有一个参与者，自然就没有战斗了。&lt;/li&gt;
&lt;li&gt;所以，这里的“解决”更应该理解为“规避”了导致Z-fighting的特定条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;为什么有些角色两边颜色不同的披风就不会出现这种情况？&lt;/h2&gt;
&lt;p&gt;这种情况通常有以下几种原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不同的建模方式&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用两个独立的、略微分开的面片（Shell/Double Mesh）&lt;/strong&gt;：最常见的做法是，披风不是一个无限薄的单面片，而是由两层略有间距的面片组成，或者是一个有实际厚度的“壳”状模型。一层代表披风的外面，另一层代表披风的内面。这两层有各自的法线方向，并且在空间上有微小的物理分离。这样，它们的深度值本身就有差异，不容易发生Z-fighting。每一层都可以使用标准的 &lt;code&gt;Cull Back&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这种情况下，你可以给外面和内面的材质分别指定不同的颜色或纹理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更高级的Shader技巧（即使是单面片）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;**使用 &lt;strong&gt;&lt;code&gt;VFACE&lt;/code&gt;&lt;/strong&gt; 或 **&lt;code&gt;gl_FrontFacing&lt;/code&gt;：现代Shader语言（如HLSL, GLSL）提供了一个语义（如 &lt;code&gt;VFACE&lt;/code&gt; 在ShaderLab/HLSL中，或 &lt;code&gt;gl_FrontFacing&lt;/code&gt; 在GLSL中），它可以在片元着色器（Fragment Shader/Pixel Shader）中判断当前正在处理的片元是属于正面还是背面。 &lt;a href=&quot;https://blender.stackexchange.com/questions/230199/how-do-i-get-double-sided-normals-without-adding-vertices&quot;&gt;[7]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;通过这个判断，即使设置了 &lt;code&gt;Cull Off&lt;/code&gt;，也可以在同一个Shader Pass里，根据是正面还是背面，来采样不同的颜色或纹理，或者执行不同的光照计算。这样只渲染一次，但根据朝向选择不同的外观，避免了两个pass在相同深度上的竞争。&lt;/li&gt;
&lt;li&gt;例如，在Shader中可以这样写（伪代码）：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Cull Off // 关闭剔除，让两面都进入渲染管线

float4 fragment_shader(VertexOutput input, fixed facing : VFACE) : SV_Target {
    float4 color;
    if (facing &amp;gt; 0) { // 或者 facing &amp;gt; 0 对于某些平台表示正面
        color = SampleTextureForFrontFace(input.uv);
    } else {
        color = SampleTextureForBackFace(input.uv);
        // 对于背面，法线可能需要反转才能正确光照
        input.normal = -input.normal;
    }
    // ... 进行光照计算等
    return color;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不同的摄像机或场景设置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果其他披风距离摄像机非常近，或者摄像机的近裁剪面和远裁剪面之间的范围（&lt;code&gt;far/near&lt;/code&gt; ratio）设置得比较合理（比例较小），那么深度缓冲的精度在那个距离上可能足够高，即使是微小的深度差异也能被区分开。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对于你的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;披风很可能是用&lt;strong&gt;单层非常薄的几何体&lt;/strong&gt;制作的。&lt;/li&gt;
&lt;li&gt;当你使用一个希望两面都显示（可能用了 &lt;code&gt;Cull Off&lt;/code&gt; 或者一个强制双面渲染的Shader）的材质时，这个几何体的“正面”和“背面”在完全相同的深度上，导致Z-fighting。&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;Cull Back&lt;/code&gt; 后，只渲染了其中一面（通常是法线朝外的那一面），另一面被剔除，冲突消失，所以Z-fighting“缓解”了。但代价是，如果你的披风需要从反面看也有不同的颜色，那么 &lt;code&gt;Cull Back&lt;/code&gt; 会导致反面不可见或者显示的是正面的颜色（取决于你如何处理法线和光照）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决方案建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;最佳实践：模型层面处理&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果希望披风内外两侧有不同材质且稳定显示，最好的方法是在3D建模软件中就将披风做成&lt;strong&gt;两层独立的网格&lt;/strong&gt;，或者给予它一定的厚度。然后给内外两层分别赋予不同的材质，并都使用默认的 &lt;code&gt;Cull Back&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shader层面处理 (如果必须用单层网格)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;使用一个支持 &lt;code&gt;VFACE&lt;/code&gt; (或等效功能) 的自定义Shader。在Shader中设置 &lt;code&gt;Cull Off&lt;/code&gt;，然后在片元着色器中根据 &lt;code&gt;VFACE&lt;/code&gt; 的值来判断是正面还是背面，并据此应用不同的颜色/纹理和光照逻辑。这样可以只用一个渲染遍（Pass）就实现双面不同材质的效果，且能有效避免Z-fighting。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整深度偏移 (Polygon Offset / Shader Offset)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在某些情况下，可以通过在Shader中对其中一个“面”（比如背面）的深度值进行微小的偏移（Polygon Offset，在Unity ShaderLab中可以用 &lt;code&gt;Offset&lt;/code&gt; 命令）来尝试解决Z-fighting。但这需要小心调整，且可能在某些角度或距离下仍然出现问题或产生其他视觉瑕疵。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对话中提到的 &quot;shadowmap&quot; 和 &quot;shadow acne&quot; 是与阴影渲染相关的深度问题，虽然也和深度精度有关，但和你描述的披风自身两面Z-fighting是略有不同的问题。不过，解决Z-fighting的很多原则是相通的，即确保深度值能够被正确区分。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.pig1024.me/posts/616975d01225572af327c122&quot;&gt;Z-Fighting - 博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/zh-tw/azure/remote-rendering/overview/features/z-fighting-mitigation&quot;&gt;Z-fighting 緩和措施- Azure Remote Rendering - Learn Microsoft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.bimant.com/blog/z-fighting-definitive-guide/&quot;&gt;Z-Fighting 权威指南 - BimAnt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_51603875/article/details/132585634&quot;&gt;Unity中Shader的面剔除Cull_cull front cull back-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.unity.cn/cn/2021.2/ScriptReference/Rendering.CullMode.html&quot;&gt;CullMode - Unity 脚本API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=3WWKHt92XKQ&quot;&gt;Unity Basics: Triangle Winding, Culling Modes &amp;amp; Double Sided Materials ✔️ 2020.3 | Game Dev Tutorial - YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blender.stackexchange.com/questions/230199/how-do-i-get-double-sided-normals-without-adding-vertices&quot;&gt;How do I get double sided normals without adding vertices? - Blender Stack Exchange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7124879046684442632&quot;&gt;Unity解决同材质物体重叠产生Z-Fighting的问题 - 稀土掘金&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Name.com配合CloudFlare实现极空间的ddns托管域名</title><link>https://fuwari.vercel.app/posts/name-com-cloudflare-ddns/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/name-com-cloudflare-ddns/</guid><description>通过Name.com配合CloudFlare实现极空间的ddns托管域名</description><pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;关于注册域名&lt;/h1&gt;
&lt;p&gt;注册域名这一方面我用的是Github Education Pack。里面的name.student可以允许创建一个免费一年的域名。必须&lt;strong&gt;从github education pack详情页点入name网站来激活student pack权限。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;缺点是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;后缀比较抽象，一般后缀是.game;.software;.video等。但是对于实现nas ddns的域名解析也是相当够用的了&lt;/li&gt;
&lt;li&gt;需要google pay/visa绑定&lt;/li&gt;
&lt;li&gt;续费贼贵（大概十几美刀一年），等一年过去我可能就要换其他域名了。但是无所谓，我统一用cloudflare进行托管&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/69597a66.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Name.com配合实现CloudFlare域名托管&lt;/h1&gt;
&lt;p&gt;对于阿里云亦或是腾讯云，肯定是可以实现自托管的；因为这两家服务提供商都有很广泛的业务；但我是白嫖的，肯定就得请出另一个大善人——cloudflare了！其中的Free套餐也是相当可用的。&lt;/p&gt;
&lt;p&gt;这个指南会分为三个主要部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare 的初始设置&lt;/strong&gt;：将您的域名添加到 Cloudflare 并获取 Cloudflare 的域名服务器（NS）地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name.com 的配置&lt;/strong&gt;：将 Name.com 的域名服务器（NS）更改为 Cloudflare 的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极空间 NAS 的 DDNS 设置&lt;/strong&gt;：在极空间里配置 DDNS，使其能够自动更新您域名的 IPv6 地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;核心概念&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在开始操作前，我们先简单理解一下各个角色的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name.com&lt;/strong&gt;：您域名的“房产证”颁发机构。我们在这里只需要做一件事：告诉它以后域名的地址解析（DNS）工作不要自己管了，全权交给 Cloudflare 处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;：一个强大的网络服务平台，我们将使用它免费的 DNS 托管和 DDNS 功能。它会接收来自您家 NAS 的指令，将您的域名指向您家宽带动态分配的 IPv6 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极空间 NAS&lt;/strong&gt;：您家里的数据中心。它会监测自己获取到的公网 IPv6 地址，一旦地址发生变化，就会自动通知 Cloudflare 更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个流程是：&lt;strong&gt;极空间 NAS → 通知 Cloudflare → Cloudflare 更新 DNS 记录 → 您通过域名访问 NAS&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第一部分：Cloudflare 初始设置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在这一步，我们将在 Cloudflare 添加您的域名，为后续步骤做准备。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;注册 Cloudflare 账户&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;访问 Cloudflare 官网 (&lt;code&gt;cloudflare.com&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;点击 &quot;Sign Up&quot;（注册），使用您的邮箱和密码创建一个免费账户。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加您的域名&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;登录后，点击仪表板上的 &quot;+ Add a domain&quot;（添加域）。 &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/manage-domains/add-site/&quot;&gt;[1]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;输入您在 Name.com 购买的域名（例如 &lt;code&gt;yourdomain.com&lt;/code&gt;），然后点击 &quot;Add domain&quot;。 &lt;a href=&quot;https://developers.cloudflare.com/learning-paths/clientless-access/initial-setup/add-site/&quot;&gt;[2]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择套餐&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Cloudflare 会展示不同的套餐计划。对于我们的需求，最下面的 &lt;strong&gt;Free（免费）&lt;/strong&gt; 套餐完全足够。 &lt;a href=&quot;https://www.youtube.com/watch?v=PR8iU-L8WcA&quot;&gt;[3]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;选中 Free 套餐，然后点击 &quot;Continue&quot;（继续）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查 DNS 记录&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;Cloudflare 会尝试扫描您域名现有的 DNS 记录。因为是新域名，这里很可能是空的，或者只有几条 Name.com 的默认记录。&lt;/li&gt;
&lt;li&gt;暂时不用管这些记录，直接点击 &quot;Continue&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取 Cloudflare 的域名服务器（NS）地址&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;这是关键一步。Cloudflare 会提示您，需要将您域名的 NS 记录更改为 Cloudflare 提供的地址。 &lt;a href=&quot;https://support.rocketspark.com/hc/en-us/articles/115010441968-How-do-I-set-up-my-website-on-Cloudflare&quot;&gt;[4]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;您会看到两条 NS 地址，通常是类似 &lt;code&gt;xxx.ns.cloudflare.com&lt;/code&gt; 和 &lt;code&gt;yyy.ns.cloudflare.com&lt;/code&gt; 的格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请将这两个地址完整地复制下来&lt;/strong&gt;，下一步马上会用到。先不要关闭这个页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第二部分：Name.com 配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;现在，我们需要回到 Name.com，将域名的管理权“委托”给 Cloudflare。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;登录 Name.com 账户&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;打开 Name.com 官网并登录。&lt;/li&gt;
&lt;li&gt;进入您的 &quot;My Domains&quot;（我的域名）列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;找到域名服务器设置&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击您需要设置的那个域名，进入管理页面。&lt;/li&gt;
&lt;li&gt;寻找名为 &quot;Nameservers&quot;（域名服务器）的选项卡或链接。通常它位于 DNS 设置区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改域名服务器&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;系统默认会使用 Name.com 自己的 Nameservers。您需要选择“自定义”或“使用自己的域名服务器”之类的选项。&lt;/li&gt;
&lt;li&gt;删除掉原有的 Name.com 的 NS 地址。&lt;/li&gt;
&lt;li&gt;将上一步从 Cloudflare 复制的两条 NS 地址，分别粘贴到输入框中。&lt;/li&gt;
&lt;li&gt;保存更改。Name.com 可能会有安全提示，确认即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待 DNS 生效&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;域名服务器的更改在全球范围内生效需要一些时间，这个过程称为“DNS 传播”。通常几分钟到几小时即可完成，但官方说法是最长可能需要 24-48 小时。&lt;/li&gt;
&lt;li&gt;您可以回到刚才的 Cloudflare 页面，点击 &quot;Done, check nameservers&quot;（完成，检查域名服务器）按钮。Cloudflare 会开始定期检查 NS 是否已指向它。当它检测到更改成功后，会给您发送一封确认邮件，并且您的域名在 Cloudflare 仪表板上会显示为“Active”（有效）状态。&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/3cc16802.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;第三部分：极空间 NAS 的 DDNS 设置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;当您的域名在 Cloudflare 上激活后，我们就可以配置极空间了。极空间需要一个“密码”（API Token）才能和 Cloudflare 通信。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;步骤 A：在 Cloudflare 创建 API Token&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;为了安全，我们不使用全局 API 密钥，而是创建一个权限受限的专用 Token。 &lt;a href=&quot;https://www.reddit.com/r/opnsense/comments/1bgdqx0/how_to_set_up_cloudflare_dynamic_dns_ddns_in/&quot;&gt;[5]&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进入 API Token 页面&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在 Cloudflare 仪表板右上角，点击您的头像，然后选择 &quot;My Profile&quot;（我的个人资料）。 &lt;a href=&quot;https://cloud-jake.medium.com/dynamic-dns-with-cloudflare-60cf4fe250be&quot;&gt;[6]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;在左侧菜单中，选择 &quot;API Tokens&quot;（API 令牌）。 &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/api/get-started/create-token/&quot;&gt;[7]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建 Token&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击 &quot;Create Token&quot;（创建令牌）。 &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/api/get-started/create-token/&quot;&gt;[7]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;在模板中找到 &quot;Edit Zone DNS&quot;（编辑区域 DNS）这一项，点击右侧的 &quot;Use template&quot;（使用模板）。 &lt;a href=&quot;https://www.reddit.com/r/opnsense/comments/1bgdqx0/how_to_set_up_cloudflare_dynamic_dns_ddns_in/&quot;&gt;[5]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置 Token 权限&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Token name&lt;/strong&gt;：给这个 Token 起一个容易识别的名字，比如 &lt;code&gt;Zspace-DDNS&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permissions&lt;/strong&gt;（权限）：模板已经帮我们选好了 &lt;code&gt;Zone - DNS - Edit&lt;/code&gt;，这正是我们需要的权限，无需改动。 &lt;a href=&quot;https://community.ui.com/questions/Dynamic-DNS-custom-service-with-Cloudflare-API-how-does-it-work/ed3706ae-8701-4f15-9327-2a52d84f4327&quot;&gt;[8]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zone Resources&lt;/strong&gt;（区域资源）：这是最重要的一步，用于限制此 Token 只能管理您指定的域名。 &lt;a href=&quot;https://www.reddit.com/r/opnsense/comments/1bgdqx0/how_to_set_up_cloudflare_dynamic_dns_ddns_in/&quot;&gt;[5]&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;选择 &lt;code&gt;Include&lt;/code&gt;（包括） -&amp;gt; &lt;code&gt;Specific zone&lt;/code&gt;（特定区域） -&amp;gt; 然后在下拉菜单中选择您刚刚添加的域名（例如 &lt;code&gt;yourdomain.com&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client IP Address Filtering / TTL&lt;/strong&gt;：这两项保持默认即可。&lt;/li&gt;
&lt;li&gt;点击 &quot;Continue to summary&quot;（继续以查看摘要）。&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/b9fa4727.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取并保存 Token&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在摘要页面确认信息无误后，点击 &quot;Create Token&quot;。&lt;/li&gt;
&lt;li&gt;Cloudflare 会生成一长串字符，这就是您的 API Token。&lt;strong&gt;请立即点击旁边的“复制”按钮，并将其保存在一个安全的地方（例如记事本）。这个 Token 只会显示这一次，关闭页面后将无法再次查看。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;步骤 B：在极空间中设置 DDNS&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打开极空间 DDNS 应用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;登录您的极空间网页端或客户端。&lt;/li&gt;
&lt;li&gt;在桌面或应用中心找到并打开 &quot;DDNS&quot; 应用。 &lt;a href=&quot;https://izspace.cn/tutorial/ddns.html&quot;&gt;[9]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加 DDNS 服务&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;点击“添加”或“创建新的 DDNS”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务商&lt;/strong&gt;：选择 &lt;code&gt;Cloudflare&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;IP地址获取地：&lt;strong&gt;请一定要选择“本地网口获取”，而不是“从极空间服务器获取”。极空间保存的IPv6地址和本地网口的不一致！&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/4b1b8ef0.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主机名&lt;/strong&gt;：填写您想使用的域名前缀，例如 &lt;code&gt;nas&lt;/code&gt;、&lt;code&gt;home&lt;/code&gt; 等。如果您想直接使用主域名，可以尝试填写 &lt;code&gt;@&lt;/code&gt;。这里我们以 &lt;code&gt;nas&lt;/code&gt; 为例，那么未来您将通过 &lt;code&gt;nas.yourdomain.com&lt;/code&gt; 访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;域名&lt;/strong&gt;：选择或填写您的主域名，例如 &lt;code&gt;yourdomain.com&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Token / 密钥&lt;/strong&gt;：将刚刚从 Cloudflare 复制并保存的 API Token 粘贴到这里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 地址类型&lt;/strong&gt;：&lt;strong&gt;务必选择 &lt;strong&gt;&lt;code&gt;IPv6&lt;/code&gt;。我选择的是关闭了IPv4，因为我想做的就是P2P。必须确保你的NAS已经被路由器和光猫放行&lt;/strong&gt;（即入站出站的端口/IP被允许放行，需要用超管权限。可以叫宽带师傅或者自行破解。我家的光猫配置在这里：&lt;/strong&gt;&lt;a href=&quot;https://blog.csdn.net/qq_40709699/article/details/118657782&quot;&gt;&lt;strong&gt;https://blog.csdn.net/qq_40709699/article/details/118657782&lt;/strong&gt;&lt;/a&gt;**&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://www.right.com.cn/forum/forum.php?mod=viewthread&amp;amp;tid=8236227&amp;amp;highlight=H2.&quot;&gt;&lt;strong&gt;https://www.right.com.cn/forum/forum.php?mod=viewthread&amp;amp;tid=8236227&amp;amp;highlight=H2.&lt;/strong&gt;&lt;/a&gt;&lt;/strong&gt;）**。如果选择IPv4大概率会无法访问（因为没有配置穿透）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理（Proxy）&lt;/strong&gt;：这里会有一个开关，对应 Cloudflare 的“小云朵”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;建议首次设置时关闭代理（灰色云朵）&lt;/strong&gt;：这会直接将域名解析到您 NAS 的公网 IPv6 地址。这种模式兼容性最好，适合各种服务（如 FTP、SMB 等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开启代理（橙色云朵）&lt;/strong&gt;：这会隐藏您的真实 IP，并由 Cloudflare 提供 CDN 加速和安全防护。这对于网页访问（HTTP/HTTPS）非常有用，但可能会影响非网页服务的直接连接。可以先在关闭状态下测试成功，再按需开启。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存并测试&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;保存设置。极空间的 DDNS 服务会立即尝试连接 Cloudflare 并更新一次 IP。&lt;/li&gt;
&lt;li&gt;查看 DDNS 列表中的状态，如果显示“成功”或“IP 地址已是最新”，则表示配置成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;最后验证&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在 Cloudflare 检查&lt;/strong&gt;：回到 Cloudflare 网站，进入您的域名管理页面，点击左侧的 &quot;DNS&quot;。您应该能看到一条新的 &lt;strong&gt;AAAA 记录&lt;/strong&gt;，名称是您设置的主机名（如 &lt;code&gt;nas&lt;/code&gt;），内容是您 NAS 当前的公网 IPv6 地址。&lt;img src=&quot;/images/posts/d08f3c28.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尝试访问&lt;/strong&gt; &lt;a href=&quot;https://developers.cloudflare.com/fundamentals/api/get-started/create-token/&quot;&gt;[7]&lt;/a&gt;：在连接了 IPv6 网络的设备上（例如您的手机使用蜂窝网络），尝试通过 &lt;code&gt;http://[您的NAS的IPv6地址]:5055&lt;/code&gt; （NAS的实际端口）看是否能访问。如果可以，再尝试通过 &lt;code&gt;http://nas.yourdomain.com:5055&lt;/code&gt; 访问。比如说我的极空间使用的端口是5055 http;如果两者都能成功，那么恭喜，全部设置完成了！&lt;img src=&quot;/images/posts/292322d5.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;/images/posts/743c1d28.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，您的极空间 NAS 已经成功配置了基于 Cloudflare 的 IPv6 DDNS。无论您家的公网 IPv6 地址如何变化，NAS 都会自动通知 Cloudflare，确保您的域名始终指向正确的位置。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/fundamentals/manage-domains/add-site/&quot;&gt;Add a site · Cloudflare Fundamentals docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/learning-paths/clientless-access/initial-setup/add-site/&quot;&gt;Add a site · Cloudflare Learning Paths&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=PR8iU-L8WcA&quot;&gt;How to Add a Website to Cloudflare (Step-by-Step for Beginners) - YouTube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.rocketspark.com/hc/en-us/articles/115010441968-How-do-I-set-up-my-website-on-Cloudflare&quot;&gt;How do I set up my website on Cloudflare? - Rocketspark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/opnsense/comments/1bgdqx0/how_to_set_up_cloudflare_dynamic_dns_ddns_in/&quot;&gt;How to set up Cloudflare Dynamic DNS (DDNS) in OPNsense - Reddit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud-jake.medium.com/dynamic-dns-with-cloudflare-60cf4fe250be&quot;&gt;Dynamic DNS with Cloudflare - Cloud Jake - Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/fundamentals/api/get-started/create-token/&quot;&gt;Create API token · Cloudflare Fundamentals docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://community.ui.com/questions/Dynamic-DNS-custom-service-with-Cloudflare-API-how-does-it-work/ed3706ae-8701-4f15-9327-2a52d84f4327&quot;&gt;Dynamic DNS (custom service) with Cloudflare API, how does it work? - Ubiquiti Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://izspace.cn/tutorial/ddns.html&quot;&gt;【教程】极空间DDNS使用方法- NAS交流社区&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/650354462&quot;&gt;https://zhuanlan.zhihu.com/p/650354462&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_40709699/article/details/118657782&quot;&gt;https://blog.csdn.net/qq_40709699/article/details/118657782&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.right.com.cn/forum/forum.php?mod=viewthread&amp;amp;tid=8236227&amp;amp;highlight=H2.&quot;&gt;https://www.right.com.cn/forum/forum.php?mod=viewthread&amp;amp;tid=8236227&amp;amp;highlight=H2.&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Blender导入场景进入Unity</title><link>https://fuwari.vercel.app/posts/blender%E5%AF%BC%E5%85%A5%E5%9C%BA%E6%99%AF%E8%BF%9B%E5%85%A5unity/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/blender%E5%AF%BC%E5%85%A5%E5%9C%BA%E6%99%AF%E8%BF%9B%E5%85%A5unity/</guid><description>Blender模型导入Unity的完整工作流程</description><pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先先搬上链接：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Ba4y1D7fQ/?spm_id_from=..search-card.all.click&amp;amp;vd_source=30f07a01a885038de48c259c5cfe892d&quot;&gt;【教程】关于blender模型导入unity的问题解答_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1384y1j7wt?spm_id_from=333.788.recommend_more_video.0&amp;amp;vd_source=30f07a01a885038de48c259c5cfe892d&quot;&gt;Blender中导出带贴图的FBX_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;因为发现太少有关于这个资料了所以我试错试了很久。。。（下面的图是Blender的大致步骤，Unity还有）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/365c3d4d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Blender处理&lt;/h2&gt;
&lt;h3&gt;解包对应的材质&lt;/h3&gt;
&lt;p&gt;Blender有“打包功能”和“解包功能”。顾名思义，由于blender的保存文件是.blend文件，所以材质贴图可以一起打包进去。Blender的材质管理有三种模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;索引模式：Blender建模默认执行的材质管理格式。在这种情况下，就算你点击“导入图像”或者是将图像/纹理拖入到Blender窗口，它的本质是&lt;strong&gt;读取这张图像的文件索引（类似于Latex未提供图床的图像添加）&lt;/strong&gt;，不生成额外文件。&lt;/li&gt;
&lt;li&gt;打包模式：左上角的“文件”——外部数据——“打包资源”/“自动打包资源”。&lt;strong&gt;就是将所有在项目里索引过的文件（图像、SDF图、糙度图）全部打包进入.blend文件中&lt;/strong&gt;。所有文件打包进入一个文件中，最适合进行文件的传输和保存。&lt;/li&gt;
&lt;li&gt;解包模式：**将所有索引过的文件（图像、SDF图、糙度图）在指定文件夹后自动分类命名。**方便其他未适配.blend文件而使用.fbx文件的导入导出（比如Unity）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;点击左上角的文件——外部数据——解包资源——“将文件写至当前目录（覆盖目前现有文件）&lt;strong&gt;&lt;strong&gt;（不是将文件写至源目录！）&lt;/strong&gt;&lt;/strong&gt;”,这会在之后的导出生成对应的纹理文件夹。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;导出设置&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/bb43c734.png&quot; alt=&quot;设置&quot; /&gt;如左图所示，一定要设置坐标轴**“-X向前”“Z向上”&lt;strong&gt;。这是由于.fbx文件和blender默认的坐标轴定义不一致导致的，要手动更改，还要点击&lt;/strong&gt;“应用变换”（曾经的Transform全部归零以免坐标鬼畜）**；&lt;strong&gt;路径模式选择“复制”，激活右边的“内嵌纹理”图标。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/74d48164.png&quot; alt=&quot;&quot; /&gt;导出完成后会有一个textures文件夹。fbx文件夹是我自己创建的不用管。&lt;/p&gt;
&lt;h2&gt;Unity处理&lt;/h2&gt;
&lt;h2&gt;直接拉入.fbx&lt;/h2&gt;
&lt;p&gt;没啥好说的直接拉进去就行了。这时候显示的还都是白模状态。&lt;/p&gt;
&lt;h2&gt;拉入先前准备的.texture文件夹&lt;/h2&gt;
&lt;p&gt;这时候Unity会自动处理并匹配对应名字的所有texture图像（因为blender已经为我们重新命名好了）。但是目前隶属于&quot;Checkin&quot;状态，材质不可更改。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/db8fcaf7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入Project的Inspector选项进行更改，选中“Materials”，&quot;extract textures &quot;和“extract materials”都进行导出，选择你想要导出的文件夹就可以了。&lt;/p&gt;
&lt;p&gt;在这之后在Hierarchy内的inspector就可以更改材质/shader了&lt;/p&gt;
&lt;h2&gt;问：&lt;/h2&gt;
&lt;h3&gt;为什么在做完一些还有一些属于白模状态？&lt;/h3&gt;
&lt;p&gt;这是由于Blender的着色器节点导致的。除了Blender默认的原理化BSDF可以被解析，其他非原理化着色器比如玻璃BSDF、透明BSDF不可被解析。需要你自己去匹配Unity对应的Shader（也就是说你要重写了，悲）。&lt;/p&gt;
&lt;h3&gt;我的灯好亮啊&lt;/h3&gt;
&lt;p&gt;那就调低点；这是由于Blender对于灯光定义的亮度和Unity自带的灯光定义亮度不一致。&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>概念解惑</title><link>https://fuwari.vercel.app/posts/%E6%A6%82%E5%BF%B5%E8%A7%A3%E6%83%91/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/%E6%A6%82%E5%BF%B5%E8%A7%A3%E6%83%91/</guid><pubDate>Sat, 21 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这里会放置一些有关概念解答，方便快速上手。&lt;/p&gt;
&lt;h2&gt;渲染基础流程图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_00.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;标准光照模型的各个区域详解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_01.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_02.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当使用#include &quot;UnityCG.cginc&quot;.时，库之间的关系式（其中unityCG就已经包含了BRDF标准光照函数）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_03.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;What is pragma?like #pragma vertex&lt;/h3&gt;
&lt;p&gt;pragma源于希腊语，指的是一个动作，或者接下来需要做的事情。在许多语言中用于表示编译器发出的特殊指令。&lt;/p&gt;
&lt;h2&gt;ShadowMap（阴影Map）&lt;/h2&gt;
&lt;p&gt;Shadow Map技术是一种用于生成实时阴影的常用方法，它通过预先计算并存储光源视角下的深度信息，然后在运行时比较这些信息来决定像素是否处于阴影中。本文将详细解析Shadow Map的原理、实现过程以及优缺点，并提供一些实践经验和建议。
一、Shadow Map的基本原理
Shadow Map的基本原理基于一个简单的观察：如果光源和目标点之间的连线没有任何物体遮挡，则目标点没有在阴影中；反之，如果这条连线有物体遮挡，则目标点处在阴影中。因此，我们可以通过计算并比较每个像素与光源之间的距离来判断它是否在阴影中。
Shadow Map的实现主要包括以下步骤：
二、Shadow Map的实践与优化
虽然Shadow Map的原理相对简单，但在实际应用中却需要注意许多细节。以下是一些实践经验和建议：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_04.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Mipmap&lt;/h2&gt;
&lt;p&gt;mipmap 一词是 MIP map 的缩写。字母 MIP 代表拉丁短语 multum in parvo，翻译过来就是小空间里的众多。它是 Lance Williams 在首次描述 mipmapping 技术时创造的。&lt;/p&gt;
&lt;h2&gt;类分类&lt;/h2&gt;
&lt;h3&gt;_MainTex_ST和_MainTex的区别&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;屏幕后处理就是把整个屏幕看成是一个四边形，然后整个屏幕的内容都已经渲染在一张纹理贴图上面了（左图代码中的_MianTex），然后再传入这张图片的每一个像素点的uv坐标（左图代码中的input.uv），就能够对屏幕四边形进行采样并渲染在场景里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The _ST suffix stands for Scale and Translation, or something like that. Why isn&apos;t _TO used, referring to Tiling and Offset? Because Unity has always used _ST, and backwards compatibility mandates it stays that way, even if the terminology might have changed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;扩展：使用TRANSFORM_TEX进行简化：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);//等价于o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顶点的UV快速使用TRANSFORM_TEX进行重新对齐。指定纹理组进行变换缩放，使纹理能正确在物体的表面进行显示。&lt;/p&gt;
&lt;h3&gt;sampler 2D（获取2D坐标采样器）&lt;/h3&gt;
&lt;p&gt;一般用于MainTex的获取，sampler2D 属性是一种在图形着色语言（如 OpenGL ES Shading Language, GLSL）中使用的类型，它代表了一个纹理采样器对象。这个对象用于在片元着色器（Fragment Shader）中对一个二维纹理进行采样。具体来说，sampler2D 可以用来读取存储在纹理中的颜色信息，并且可以在渲染过程中根据片段的位置或其他属性来获取相应的纹理颜色。
sampler2D 变量通常会与纹理坐标（通常是 vec2 类型）一起使用，这些坐标指定了纹理图像上的位置。当着色器执行时，它可以根据这些坐标从纹理中查找并插值颜色值。
在 Unity 的 ShaderLab 或其他图形API中，sampler2D 可能会被用来定义如何以及从哪里采样纹理数据，包括纹理过滤方式、地址模式（如重复、夹紧等）以及其他纹理状态&lt;/p&gt;
&lt;h3&gt;SV_POSITION和POSITION的区别&lt;/h3&gt;
&lt;p&gt;SV_POSTION前的SV代表System Value,代指系统确定值。输出裁剪空间下的顶点坐标数据，给光栅化使用，必须要写的数据。
一旦被作为vertex shader的输出语义，那么这个最终的顶点位置就被固定了(不能tensellate，不能再被后续改变它的空间位置？)，已经成为了转换裁剪世界的坐标，是可以直接用来进入光栅化处理的坐标，如果作为fragment shader的输入语义那么和POSITION是一样的，代表着每个像素点在屏幕上的位置（这个说法其实并不准确，事实是fragment 在 view space空间中的位置，但直观的感受是如括号之前所述一般）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;we&apos;re trying to output the position of the vertex. We have to indicate this by attaching the SV_POSITION semantic to our method. SV stands for system value, and POSITION for the final vertex position.The fragment program requires semantics as well. In this case, we have to indicate where the final color should be written to. We use SV_TARGET, which is the default shader target. This is the frame buffer, which contains the image that we are generating.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;“SV_TARGET”与“SV_POSTION” - 知乎 (zhihu.com)&lt;/p&gt;
&lt;h3&gt;Unity中如何定义_WorldSpaceLightPos0&lt;/h3&gt;
&lt;p&gt;这是一个来自于UnityShaderVariables的函数，含有四个分量，同时也是齐次坐标系。
在Unity Shader中，_WorldSpaceLightPos0是一个定义灯光位置的变量。这个变量包含了当前光源的位置或光线的方向（如果是定向光）。它有四个分量，这些是齐次坐标。对于定向光，第四个分量是0。&lt;/p&gt;
&lt;h4&gt;1. _WorldSpaceLightPos0的定义&lt;/h4&gt;
&lt;h4&gt;2. 齐次坐标解释&lt;/h4&gt;
&lt;h4&gt;3. 应用场景&lt;/h4&gt;
&lt;p&gt;通过这种方式，Unity能够灵活地处理不同类型的光照模型，并允许开发者通过Shader编程实现复杂的视觉效果。&lt;/p&gt;
&lt;h2&gt;前向渲染是什么？&lt;/h2&gt;
&lt;p&gt;前向渲染（Forward Rendering）
它的实现最贴合我们的思维逻辑，当我们渲染模型时，只需要关心画模型然后直接处理光照，让它自己去做深度测试，最后深度测试过的都显示在屏幕上。
1、对要渲染的物体进行遍历渲染出shadowmap
2、再遍历一遍上面要渲染的物体，根据shadowmap对每一个物体的像素进行光照计算
优点：
很明显，就是简单，并且可以针对每个物体指定它的材质，因为每个物体都是独立渲染的。
缺点
1、由于依赖深度测试，如果物体是乱序的，大量的物体深度测试和光照计算判断，可能会出现大量的像素光照计算都是浪费的。（大量的drawcall）
2、不能支持光源数量较多的情况。所以我们一般有两种做法来处理多光源的情况，一种是一遍渲染多个光源，所有光照运算都在一个着色器中进行。另一种是多遍渲染多个光源，意思就是每多一盏灯，就多渲染一次模型，所以性能消耗也比较大。
延迟渲染（Deferred Rendering）
它的做法是在第一遍渲染模型的时候，不进行光照计算，直接将位置、法线深度、颜色等存到G-Buffer（Geometry-buffer,专门用于进行法线，顶点坐标等物体数据储存的缓冲区。过高的buffer会占用大量的显存和带宽。）。很多人第一次接触G-Buffer这个名词都是一头雾水，其实就是创建多张和屏幕一样大的纹理，然后每张纹理的像素值分别用来存上面提到的这些数据。也就是说一次渲染，需要输出多张纹理，这跟前向渲染是不同的， 前向渲染只渲染到一张纹理上，这张纹理最终会渲染在屏幕上。而延迟渲染这多张纹理都不是最终结果，可以理解为只是用几张贴图存储一堆中间数据。
第二遍再根据G-Buffer的数据，进行光照计算，写入帧缓冲区。
优点
处理完G-Buffer之后，其实每盏灯光就可以通过一个Drawcall的消耗去执行光照计算。第一遍处理完的G-Buffer是深度测试过的，不像前向渲染一样，有那么多光照计算的浪费。
缺点
1、最终的光照计算方式只能是一种，也就是其他文章提到的只允许一个材质，因为最终计算的时候只剩下一堆数据，不知道它们分别是谁的，所以只能无差别对待。而前向渲染可以每个模型一种计算方式。
2、不允许使用透明物件，因为最终G-Buffer只剩一个像素了，无法进行混合。不过可以在第二遍渲染完之后，用前向渲染的方式渲染透明物体。
3、不支持抗锯齿，意味着不能用MSAA（多重采样抗锯齿），不过可以用FXAA（快速近似抗锯齿）进行后期处理
4、有些硬件不支持MRT(Multiple Render Targets 多重纹理目标)，也就是输出到多张纹理上实现G-Buffer的功能
5、G-Buffer需要比较大的带宽，有些硬件没不具备这个能力
6、由于光照计算本身性能消耗也不低，延迟渲染的光照计算其实等同于在做一次全屏的后处理，后处理其实对手机来说过于昂贵。而如果只有一盏灯的话，其实前向渲染省了这次额外的渲染。所以移动设备上延迟渲染的性能会比前向渲染的性能要差一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;forwardbase和一些光照如全局渲染环境光的实现：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_08.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Zwrite&lt;/h2&gt;
&lt;p&gt;这是一个出现在subshader中的全局声明。负责声明在渲染中是否需要更新深度缓冲区。ZWrite有两种状态：On（启用）和Off（禁用）。当ZWrite被设置为On时，片段的深度值将被写入深度缓冲区；当设置为Off时，则不会写入。
对于不透明对象，通常需要启用ZWrite以确保正确的深度排序和遮挡关系。对于半透明对象或某些特殊效果，可能需要禁用ZWrite以避免深度写入错误导致的渲染问题。
对于渲染管线中的解释：当ZWrite为On时，渲染管线会在光栅化阶段将片段的深度值写入深度缓冲区，这有助于实现正确的深度测试和排序。如果ZWrite设置为Off，则渲染管线不会更新深度缓冲区内容。这可能导致不正确的深度排序和潜在的渲染问题，如物体之间的遮挡关系出错。&lt;/p&gt;
&lt;h2&gt;Texcoord&lt;/h2&gt;
&lt;p&gt;Texcoord==texture coordinate 纹理坐标系的简写。这里指代的是物体储存的纹理系。可供shader使用的纹理坐标系有0-7，共有8个。&lt;/p&gt;
&lt;h2&gt;Shader LOD&lt;/h2&gt;
&lt;p&gt;指的是Shader Level of detail的缩写。作为unity的一个自带控件属性，合理根据玩家距离调整LOD的大小可以有利于性能优化。&lt;/p&gt;
&lt;h2&gt;改变天空盒操作&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;shader直接光照计算概念&lt;/h2&gt;
&lt;p&gt;直接光照计算中的DFG——DFG指的是Cook-Torrance BRDF模型中的三项：法线分布项（D）、几何项（G）和菲涅尔项（F）。
法线分布项（D）描述了由于物体表面微观结构的粗糙度导致的光线散射情况。它是根据微表面理论来模拟不同粗糙度的表面上，光线如何被散射到各个方向。几何项（G）考虑了遮挡效应和多次反射的影响，即光线在到达观察者之前可能被表面其他部分遮挡的程度。菲涅尔项（F）则描述了光线在不同角度入射时反射率的变化，这是由法国物理学家奥古斯丁·让·菲涅尔提出的理论。结合了这三个概念后，可以让PBR渲染中具有更真实的效果。&lt;/p&gt;
&lt;h2&gt;RayTracing&lt;/h2&gt;
&lt;h3&gt;DXR&lt;/h3&gt;
&lt;p&gt;DXR 是 DirectX Raytracing 的缩写，是 Microsoft 在 DirectX 12 中引入的一种光线追踪技术扩展。它允许开发者在游戏和实时渲染中使用硬件加速的光线追踪技术，从而生成更加真实的光影效果。DXR 是现代实时光线追踪的核心技术之一，它得益于图形硬件（如 NVIDIA RTX 系列显卡）的专用光线追踪单元（RT Cores）进行加速。
https://dev.epicgames.com/documentation/zh-cn/unreal-engine/refraction-using-pixel-normal-offset-in-unreal-engine&lt;/p&gt;
</content:encoded></item><item><title>自定义效果的大杀器——RenderFeature</title><link>https://fuwari.vercel.app/posts/custom-effect-renderfeature/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/custom-effect-renderfeature/</guid><description>自定义效果的大杀器——RenderFeature的解决方案和处理方法</description><pubDate>Sat, 14 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;因为在有些时候一般的Shader做不到一些东西，比如说提取深度图或者法线图计算，当然你可以写C#脚本来在每次的Update中获取，但是每次要进行修改的时候就只能在C#和Shader之间来回切换，但是邮电不美观（因为每次都要进行声明-传递-接收），更恼火的是用**C#脚本不能指定在某个渲染顺序进行渲染，也就是说你要每次在最后的工序进行渲染，而某些效果是不能在最后的时候才渲染 （如体积光和光线步进）。  **&lt;/p&gt;
&lt;p&gt;主播主播，你的C#脚本还是太吃操作了，有没有更简单（大嘘）的方法来实现这些高级功能呢？&lt;/p&gt;
&lt;p&gt;有的兄弟有的，RenderFeature可以获取渲染中的深度图、法线图和ShadowMap，甚至可以指定插入到某个渲染顺序进行处理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在Unity使用Scriptable Render Pipeline时，渲染顺序通常是从&lt;strong&gt;开始帧的资源准备和初始化工作&lt;/strong&gt;，接着进行&lt;strong&gt;环境预计算&lt;/strong&gt;如阴影贴图和光照探针的更新，&lt;strong&gt;随后渲染不透明物体&lt;/strong&gt;并根据深度排序优化性能，之后&lt;strong&gt;渲染天空盒&lt;/strong&gt;，再接着&lt;strong&gt;按距离从后往前渲染透明物体&lt;/strong&gt;以确保正确的透明效果，然后&lt;strong&gt;应用包括色调映射、景深等后期处理&lt;/strong&gt;效果增强画面质量，&lt;strong&gt;最后渲染用户界面&lt;/strong&gt;确保其位于最上层并在完成最终帧输出前进行结束帧处理以清理资源准备下一帧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以插入到上面的任意一个顺序并还能进行进一步的细分，其实就是将SRP进行抽象化并集成为了一个api&lt;/p&gt;
&lt;h2&gt;RenderFeature的主要结构&lt;/h2&gt;
&lt;p&gt;一个完整的RenderFeature可以由下面的部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RenderFeature&lt;/li&gt;
&lt;li&gt;RenderPass&lt;/li&gt;
&lt;li&gt;ShaderPass（可选，这里就是正常的Shader）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;补充：Unity&lt;strong&gt;自带的Add RenderFeature会给你一些可用的框架&lt;/strong&gt;，比如说全局屏幕效果。这是你可以将一般的Shader拖进去都行，采用的数据块是_Blit。&lt;/p&gt;
&lt;h3&gt;RenderFeature主要框架&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RenderFeature : ScriptableRendererFeature
{
    RenderPass renderpass;  //声明RenderPass
    Create();   //实例化renderpass,设置渲染顺序等操作
    AddRenderPasses();   //将上一步实例化的renderpass添加进渲染管道使用
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;继承 &lt;code&gt;ScriptableRendererFeature&lt;/code&gt; 并实现 &lt;code&gt;Create()&lt;/code&gt; 和 &lt;code&gt;AddRenderPasses()&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;Create()&lt;/code&gt; 中初始化需要的资源和渲染逻辑。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;AddRenderPasses()&lt;/code&gt; 中将自定义的 &lt;code&gt;ScriptableRenderPass&lt;/code&gt; 注册到渲染器队列中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;RenderPass主要框架&lt;/h3&gt;
&lt;p&gt;RenderPass后面有一个Pass，可以联想到Shader里面也有Pass。RenderPass就是连接Shader的一个东西。可以理解为具体操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RenderPass : ScriptableRenderPass
{

        RenderPassEvent(...)  //指定渲染的顺序
        Config(...) //可选，指定配置渲染目标（Render Target）和深度缓冲区
        Execute(...)    //使用上一个函数准备的参数进行具体的操作
        FrameCleanup(...) //释放临时渲染目标
            //下面还可以写一些自定义函数，用于对应的调用情况
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;** **&lt;code&gt;**RenderPassEvent**&lt;/code&gt;：指定这个 Render Pass 应该插入到渲染流程的哪个阶段。&lt;/li&gt;
&lt;li&gt;config如果你需要渲染到一张自定义纹理而不是直接到屏幕，就可以在这里设置渲染目标。&lt;/li&gt;
&lt;li&gt;Execute：核心的操作，对于计算部分都在这里开始&lt;/li&gt;
&lt;li&gt;Frame CleanUp：清理内存以防内存泄漏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/e973efe2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里来个形象点的比喻，就拿蟹堡王的运作方式来举例，可以把我们当做是顾客，前台就是章鱼哥，后台（RenderPass，Shader）就是RenderPass ------ 我们在前台点餐（设置参数），章鱼哥（RenderFeature）就告诉海绵宝宝（RenderPass，Shader）做几个汉堡，该放什么酱等。可以这样理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;示例代码（这里用的是RenderFeature和Shader的结合）&lt;/h2&gt;
&lt;p&gt;这里以我自己做的模仿openAI发布会上的像素化LED广告牌Shader来示例。目的是抓取camera的渲染并输出到纹理，转交给Shader后再将Shader处理过的纹理返回到相机渲染中。&lt;/p&gt;
&lt;p&gt;RenderFeature部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class PixelizeRenderFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class Settings
    {
        [Header(&quot;General Settings&quot;)]
        [Range(0, 1)]
        public float mixAmount = 0.5f;
        
        [Header(&quot;Image Settings&quot;)]
        [Range(2, 512)]
        public float pixelResolutionX = 16f;
        [Range(2, 512)]
        public float pixelResolutionY = 16f;
        public bool customresolution = false;       
        [Header(&quot;Circle Settings&quot;)]
        [Range(0.01f, 0.5f)]
        public float circleRadius = 0.4f;
        [Range(0.001f, 0.1f)]
        public float edgeSharpness = 0.01f;
        
        // Circle UV缩放
        [Range(2,512)]
        public float uvScaleX = 2.0f;
        [Range(2,512)]
        public float uvScaleY = 2.0f;
        public bool linkUVScales = false;
        public Vector2 circleOffset = new Vector2(0.5f, 0.5f);
        public Color circleColor = Color.white;
        public Color backgroundColor;
        
        [Header(&quot;GrayScale Settings&quot;)]
        public bool enableGrayscale = false;
        public Color shadowColor = new Color(0.2f, 0.3f, 0.5f, 1f);
        public Color highlightColor = new Color(0.9f, 0.7f, 0.4f, 1f);
        [Range(0.1f, 5f)]
        public float contrast = 1f;
        
        [Header(&quot;Sync Settings&quot;)]
        public bool adaptToScreenRatio = false;
    }

    // 在Inspector中公开设置
    public Settings settings = new Settings();
    
    class SDFCircleRenderPass : ScriptableRenderPass
    {
        private Material _material;
        private RenderTargetHandle tempTexture;
        private Settings settings;

        public SDFCircleRenderPass(Settings settings)
        {
            this.settings = settings;
            tempTexture.Init(&quot;_TempSDFCircleRT&quot;);
        }

        // 只接受材质
        public void Setup(Material material)
        {
            this._material = material;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (_material == null)
                return;

            // 从renderingData获取相机颜色目标
            var cameraColorTarget = renderingData.cameraData.renderer.cameraColorTarget;

            CommandBuffer cmd = CommandBufferPool.Get(&quot;Custom/TiledSDFCirclesAdvanced&quot;);
            
            _material.SetFloat(&quot;_MixAmount&quot;, settings.mixAmount);
            _material.SetFloat(&quot;_PixelResolutionX&quot;, settings.pixelResolutionX);
            _material.SetFloat(&quot;_PixelResolutionY&quot;, settings.pixelResolutionY);
            _material.SetFloat(&quot;_LinkResolutions&quot;, settings.customresolution ? 1.0f : 0.0f);
            _material.SetFloat(&quot;_CircleRadius&quot;, settings.circleRadius);
            _material.SetFloat(&quot;_EdgeSharpness&quot;, settings.edgeSharpness);
            _material.SetFloat(&quot;_UVScaleX&quot;, settings.uvScaleX);
            _material.SetFloat(&quot;_UVScaleY&quot;, settings.uvScaleY);
            _material.SetFloat(&quot;_LinkUVScales&quot;, settings.linkUVScales ? 1.0f : 0.0f);
            _material.SetVector(&quot;_CircleOffset&quot;, settings.circleOffset);
            _material.SetColor(&quot;_CircleColor&quot;, settings.circleColor);
            _material.SetColor(&quot;_BackgroundColor&quot;, settings.backgroundColor);
            _material.SetInt(&quot;_EnableGrayscale&quot;, settings.enableGrayscale ? 1 : 0);
            _material.SetColor(&quot;_ShadowColor&quot;, settings.shadowColor);
            _material.SetColor(&quot;_HighlightColor&quot;, settings.highlightColor);
            _material.SetFloat(&quot;_Contrast&quot;, settings.contrast);
            // byd字符串查找怎么你了（生气）
            

            RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
            cmd.GetTemporaryRT(tempTexture.id, descriptor);

            // 使用cameraColorTarget替代_source
            cmd.Blit(cameraColorTarget, tempTexture.Identifier(), _material);
            cmd.Blit(tempTexture.Identifier(), cameraColorTarget);
        
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
            
            //Here is Debug
            Debug.Log($&quot;Setting parameters: (Tip in l108)circleRadius={settings.circleRadius}&quot;);
        }


        public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(tempTexture.id);
        }
    }

    private SDFCircleRenderPass _renderPass;
    private Material _material;

    public override void Create()
    {
        // 加载着色器并创建材质
        Shader shader = Shader.Find(&quot;Custom/TiledSDFCirclesAdvanced&quot;);
        if (shader == null)
        {
            Debug.LogError(&quot;无法找到SDF圆形后处理着色器!&quot;);
            return;
        }
        
        _material = new Material(shader);
        
        // 创建渲染通道
        _renderPass = new SDFCircleRenderPass(settings);
        
        // 设置渲染事件时机 - 在后处理之前
        _renderPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_material == null)
            return;
        
        // 只传递材质，不传递相机颜色目标
        _renderPass.Setup(_material);
        renderer.EnqueuePass(_renderPass);
    }
    
    protected override void Dispose(bool disposing)
    {
        if (disposing &amp;amp;&amp;amp; _material != null)
        {
            CoreUtils.Destroy(_material);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shader部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shader &quot;Custom/TiledSDFCirclesAdvanced&quot;
{
    Properties
    {
        [Header(GeneralSettings)]
        _MixAmount (&quot;MixAmount(Image&amp;amp;Circle)&quot;, Range(0, 1)) = 0.5//混合量
        
        [Header(ImageSettings)]
        _MainTex (&quot;Texture&quot;, 2D) = &quot;white&quot; {}
        [Toggle] _LinkResolutions (&quot;Open Custom X/Y Resolutions&quot;, Float) = 0 // 链接XY分辨率的开关
        _PixelResolutionX (&quot;Pixel Resolution (X)&quot;, Range(2,512)) = 16
        _PixelResolutionY (&quot;Pixel Resolution Y&quot;, Range(2,512)) = 16
        
        
        [Header((I recommend Pixel Resolution is same with UV Scale))]
        
        [Header(CircleSettings)]
        _CircleRadius (&quot;Circle Radius&quot;, Range(0.01, 0.5)) = 0.4
        _EdgeSharpness (&quot;Edge Sharpness&quot;, Range(0.001, 0.1)) = 0.01
        _UVScaleX (&quot;UV Scale X&quot;, Range(1, 512)) = 2.0
        _UVScaleY (&quot;UV Scale Y&quot;, Range(1, 512)) = 2.0
        [Toggle] _LinkUVScales (&quot;Link UV X/Y&quot;, Float) = 1
        _CircleOffset (&quot;Circle Offset&quot;, Vector) = (0.5, 0.5, 0, 0)
        _CircleColor (&quot;Circle Color&quot;, Color) = (1,1,1,1)
        _BackgroundColor (&quot;Background Color&quot;, Color) = (0,0,0,1)
        
        [Header(GrayScaleSettings)]
        [Toggle] _EnableGrayscale (&quot;Enable Grayscale&quot;, Float) = 0//灰度开关控制
        _ShadowColor (&quot;Shadow Color&quot;, Color) = (0.2, 0.3, 0.5, 1.0)//阴影色
        _HighlightColor (&quot;Highlight Color&quot;, Color) = (0.9, 0.7, 0.4, 1.0)//高光色
        _Contrast (&quot;Contrast&quot;, Range(0.1, 5)) = 1.0//对比度
    }
    SubShader
    {
        Tags { &quot;RenderType&quot;=&quot;Opaque&quot; &quot;Cull&quot;=&quot;Off&quot; &quot;ZWrite&quot;=&quot;Off&quot; &quot;ZTest&quot;=&quot;Always&quot; }
        LOD 100

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include &quot;UnityCG.cginc&quot;
            #include &quot;GrayScale.hlsl&quot;

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

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

            sampler2D _MainTex;
            float _PixelResolution;
            float _CircleRadius;
            float _EdgeSharpness;
            float _UVScaleX;
            float _UVScaleY;
            float2 _CircleOffset;
            float4 _CircleColor;
            float4 _BackgroundColor;
            float _MixAmount;
            float _EnableGrayscale;
            float4 _ShadowColor;
            float4 _HighlightColor;
            float _Contrast;
            float _LinkResolutions;
            float _PixelResolutionX;
            float _PixelResolutionY;
            float _LinkUVScales;


            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.uv2 = v.uv2;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //像素化部分
               // 原始 UV 坐标
                float2 uv = i.uv;
                // 计算像素化UV - 使用独立的X和Y分辨率
                float2 pixelResolution = float2(_PixelResolutionX, _PixelResolutionY);
                // 如果链接分辨率开关打开，则使用X分辨率
                if (_LinkResolutions &amp;lt; 0.5) {
                    pixelResolution.y = pixelResolution.x;
                }
                float2 pixelatedUV = floor(uv * pixelResolution) / pixelResolution;
                // 使用修改后的 UV 坐标从纹理中采样
                fixed4 col0 = tex2D(_MainTex, pixelatedUV);
                if(_EnableGrayscale &amp;gt; 0.5)
                {
                    float grayscale = GrayscaleStandard(col0.rgb);
                    float4 tintedGrayscale = GrayscaleDuotone(
                    grayscale,
                    _ShadowColor,  // 阴影色(冷色调)
                    _HighlightColor,  // 高光色(暖色调)
                    0.5                          // 中点
                    );
                    tintedGrayscale.rgb = pow(tintedGrayscale.rgb,_Contrast);
                    col0 = tintedGrayscale;
                }

                 //Circles部分
                 // 计算Circle的UV缩放
                float2 uvScale = float2(_UVScaleX, _UVScaleY);
                if (_LinkUVScales &amp;lt; 0.5)
                {
                    uvScale.y = uvScale.x;
                }
                
                // 应用UV缩放和偏移
                float2 scaledUV = i.uv * uvScale;
                float2 tiledUV = frac(scaledUV);
                // 以指定偏移为中心
                float2 centeredUV = tiledUV - _CircleOffset;
                // 计算到圆心的距离
                float distance = length(centeredUV);
                // 平滑边缘的SDF圆
                float circle = smoothstep(_CircleRadius + _EdgeSharpness, _CircleRadius - _EdgeSharpness, distance);
                // 混合颜色
                fixed4 col1 = lerp(_BackgroundColor, _CircleColor, circle);
                fixed4 finalColor = lerp(col0, col1, _MixAmount);
                return finalColor;
            }
            ENDHLSL
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shader在RenderFeature的联动引用原理是从缓存区Blit中获取这个Shader的名字，我还以为有多高大上呢原来就是找索引啊（难绷）&lt;/p&gt;
&lt;p&gt;注：Rider在使用在缓存池进行字符串索引的时候会提示“字符串搜索的方式低效”，如果有更高效的办法就是从缓存区里获取ID，替换掉字符串。&lt;/p&gt;
</content:encoded></item><item><title>Code Review</title><link>https://fuwari.vercel.app/posts/code-review/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/code-review/</guid><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;在这个文档里我会放各种代码的解析思路，还有一些项目解析。因为看懂渲染的代码确实有些难以解释...也可能是我自己的水平不足吧(&lt;/h2&gt;
&lt;h3&gt;标准库BSDF：&lt;/h3&gt;
&lt;p&gt;这里说的是C++。实际上是HLSL（没有这个语言选项，将就着看吧）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shader &quot;Unlit/Sample Shader&quot;{Properties//来自于被渲染物体的数据选择属性。可以引用或者指定{_MainTex (&quot;Texture&quot;, 2D) = &quot;white&quot; {}}SubShader    //来自shader组的可选shader。根据设备性能的不同，可以选择其中一个subshader。    {    Tags { &quot;RenderType&quot;=&quot;Opaque&quot; }//Tags可以指定渲染的时候的rendertype。比如这里指定为不透明渲染    //光线不会穿过物体    LOD 100//Level of detail，指定纹理材质压缩率，1-100.指定了原生渲染    Pass    //passing渲染队列组    {    CGPROGRAM    #pragma vertex vert//vertex顶点组    #pragma fragment frag//fragment片元组    // make fog work    #pragma multi_compile_fog    #include &quot;UnityCG.cginc&quot;//来自unity内置的库。内含有物体的多个properties，减少编写的复杂度    struct appdata//元数据    {        float4 vertex : POSITION;        float2 uv : TEXCOORD0;    };    struct v2f    //vertex shader to fragment shader,在顶点着色器和片元着色器之间传递信息。    //其中还有一个常见的命名称为a2v    //a2v全程称为applicate to vertex shader。将数据从应用阶段传递给顶点着色器中    {    float2 uv : TEXCOORD0;    UNITY_FOG_COORDS(1)    float4 vertex : SV_POSITION;};    sampler2D _MainTex;    float4 _MainTex_ST;    v2f vert (appdata v)    //vert函数顶点着色器是用于处理3D模型中的每个顶点的程序。    //它们的主要任务是将顶点从对象空间转换到裁剪空间，并可能执行一些额外的计算，如光照、纹理坐标变换等。    //vert()函数就是用来定义这些转换和计算过程的。    {        v2f o;        o.vertex = UnityObjectToClipPos(v.vertex);        o.uv = TRANSFORM_TEX(v.uv, _MainTex);        UNITY_TRANSFER_FOG(o,o.vertex);        return o;    }    fixed4 frag (v2f i) : SV_Target//来自裁剪空间的坐标值        //像素着色器是用于处理屏幕上每个像素的程序。它们的主要任务是根据顶点着色器传递的数据和纹理信息，计算最终的颜色值。        //SV_TARGET变量就是用来存储这些颜色值的。        //SV_TARGET变量通常与特定的渲染目标相关联，例如颜色缓冲区、深度缓冲区或模板缓冲区。        //在实际应用中，像素着色器会根据不同的渲染目标进行相应的操作，以生成正确的输出。        {        // sample the texture        fixed4 col = tex2D(_MainTex, i.uv);    // apply fog    UNITY_APPLY_FOG(i.fogCoord, col);    return col;}ENDCG}}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个最简单的片元BSDF shader。它通过获取目标物体的textcord（被设置为白色），赋予目标2D UV。&lt;/p&gt;
&lt;h2&gt;透明度圆环实例&lt;/h2&gt;
&lt;p&gt;圆环内的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shader &quot;CS02/Blending&quot; //Shader的真正名字  可以是路径式的格式{    /*材质球参数及UI面板    https://docs.unity3d.com/cn/current/Manual/SL-Properties.html    https://docs.unity3d.com/cn/current/ScriptReference/MaterialPropertyDrawer.html    */    Properties         {        _MainTex (&quot;Texture&quot;, 2D) = &quot;&quot; {}    _MainColor(&quot;Main Color&quot;,Color) = (1,1,1,1)        _Emiss(&quot;Emiss&quot;, Float) = 1.0        _Speed(&quot;Speed&quot;, Vector) = (.34, .85, .92, 1)        }/*    这是为了让你可以在一个Shader文件中写多种版本的Shader，但只有一个会被使用。    提供多个版本的SubShader，Unity可以根据对应平台选择最合适的Shader    或者配合LOD机制一起使用。    一般写一个即可    */SubShader    {    /*        标签属性，有两种：一种是SubShader层级，一种在Pass层级        https://docs.unity3d.com/cn/current/Manual/SL-SubShaderTags.html        https://docs.unity3d.com/cn/current/Manual/SL-PassTags.html        */    Tags { &quot;Queue&quot; = &quot;Transparent&quot; }    /*        Pass里面的内容Shader代码真正起作用的地方，        一个Pass对应一个真正意义上运行在GPU上的完整着色器(Vertex-Fragment Shader)        一个SubShader里面可以包含多个Pass，每个Pass会被按顺序执行        */    Pass     {    //Blending:https://docs.unity3d.com/Manual/SL-Blend.html    ZWrite Off    //Blend SrcAlpha OneMinusSrcAlpha     Blend One OneMinusSrcAlpha    //Blend SrcAlpha One    //Blend DstColor Zero    CGPROGRAM  // Shader代码从这里开始     #pragma vertex vert //指定一个名为&quot;vert&quot;的函数为顶点Shader    #pragma fragment frag //指定一个名为&quot;frag&quot;函数为片元Shader    #include &quot;UnityCG.cginc&quot;  //引用Unity内置的文件，很方便，有很多现成的函数提供使用    //https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html    struct appdata  //CPU向顶点Shader提供的模型数据    {    //冒号后面的是特定语义词，告诉CPU需要哪些类似的数据    float4 vertex : POSITION; //模型空间顶点坐标    half2 texcoord0 : TEXCOORD0; //第一套UV    half2 texcoord1 : TEXCOORD1; //第二套UV    half2 texcoord2 : TEXCOORD2; //第二套UV    half2 texcoord4 : TEXCOORD3;  //模型最多只能有4套UV    half4 color : COLOR; //顶点颜色    half3 normal : NORMAL; //顶点法线    half4 tangent : TANGENT; //顶点切线(模型导入Unity后自动计算得到)};    struct v2f  //自定义数据结构体，顶点着色器输出的数据，也是片元着色器输入数据    {    float4 pos : SV_POSITION; //输出裁剪空间下的顶点坐标数据，给光栅化使用，必须要写的数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个很有意思的alpha通道测试shader。在vert顶点转换为片元着色器的时候还是正常的，转换为裁剪坐标空间讲裁剪位置坐标和纹理坐标/缩放大小重新对齐至裁剪空间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;sampler2D与texture2D函数结合使用时，可以实现从纹理图像中提取像素值的功能。例如，在片元着色器中，可以通过调用texture2D(sampler, coord)来获取指定坐标上的纹理颜色，其中sampler是一个sampler2D类型的变量，而coord是一个vec2类型的变量，表示纹理坐标.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在片元着色器输出的时候，tex2D()函数负责进行渲染的纹理映射和重新采样。tex2D (HLSL 参考) - Win32 apps | Microsoft Learn。伪代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sampler2D _MainTex;float2 speed;tex2D(_MainTex, speed);float4 ret;ret = tex2D;return ret;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tex2D中的重采样步骤选择使用时间进行偏移重采样；通过将纹理坐标与这个偏移量相加，可以得到一个新的纹理坐标，然后使用tex2D()函数来获取该坐标处的颜色值。这样可以实现纹理的动态变化，例如移动、闪烁或扭曲等效果。
最后是乘以主色彩和emiss透明度。这些在开头有了声明，因此finalcolor可以有主色彩，并可以有时间/速度偏移量。&lt;/p&gt;
&lt;h2&gt;URP外描边处理（边缘检测描边）&lt;/h2&gt;
&lt;p&gt;https://www.bilibili.com/opus/743259329311801348&lt;/p&gt;
&lt;h2&gt;对SRP渲染管线进行添加的功能处理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/image_00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;URP | 后处理-自定义后处理 - 哔哩哔哩&lt;/p&gt;
</content:encoded></item><item><title>Markdown Extended Features</title><link>https://fuwari.vercel.app/posts/markdown-extended/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/markdown-extended/</guid><description>Read more about Markdown features in Fuwari</description><pubDate>Wed, 01 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub Repository Cards&lt;/h2&gt;
&lt;p&gt;You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;Fabrizz/MMM-OnSpotify&quot;}&lt;/p&gt;
&lt;p&gt;Create a GitHub repository card with the code &lt;code&gt;::github{repo=&quot;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;&quot;}&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;saicaca/fuwari&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Admonitions&lt;/h2&gt;
&lt;p&gt;Following types of admonitions are supported: &lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
Highlights information that users should take into account, even when skimming.
:::&lt;/p&gt;
&lt;p&gt;:::tip
Optional information to help a user be more successful.
:::&lt;/p&gt;
&lt;p&gt;:::important
Crucial information necessary for users to succeed.
:::&lt;/p&gt;
&lt;p&gt;:::warning
Critical content demanding immediate user attention due to potential risks.
:::&lt;/p&gt;
&lt;p&gt;:::caution
Negative potential consequences of an action.
:::&lt;/p&gt;
&lt;h3&gt;Basic Syntax&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
Highlights information that users should take into account, even when skimming.
:::

:::tip
Optional information to help a user be more successful.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Titles&lt;/h3&gt;
&lt;p&gt;The title of the admonition can be customized.&lt;/p&gt;
&lt;p&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub Syntax&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;The GitHub syntax&lt;/a&gt; is also supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; The GitHub syntax is also supported.

&amp;gt; [!TIP]
&amp;gt; The GitHub syntax is also supported.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Spoiler&lt;/h3&gt;
&lt;p&gt;You can add spoilers to your text. The text also supports &lt;strong&gt;Markdown&lt;/strong&gt; syntax.&lt;/p&gt;
&lt;p&gt;The content :spoiler[is hidden &lt;strong&gt;ayyy&lt;/strong&gt;]!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The content :spoiler[is hidden **ayyy**]!

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Expressive Code Example</title><link>https://fuwari.vercel.app/posts/expressive-code/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/expressive-code/</guid><description>How code blocks look in Markdown using Expressive Code.</description><pubDate>Wed, 10 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Here, we&apos;ll explore how code blocks look using &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt;. The provided examples are based on the official documentation, which you can refer to for further details.&lt;/p&gt;
&lt;h2&gt;Expressive Code&lt;/h2&gt;
&lt;h3&gt;Syntax Highlighting&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;Syntax Highlighting&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Regular syntax highlighting&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;This code is syntax highlighted!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Rendering ANSI escape sequences&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Editor &amp;amp; Terminal Frames&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;Editor &amp;amp; Terminal Frames&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Code editor frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Title attribute example&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;File name comment example&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Terminal frames&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;This terminal frame has no title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;This one has a title!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Overriding frame types&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Look ma, no frame!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;# Without overriding, this would be a terminal frame
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Text &amp;amp; Line Markers&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;Text &amp;amp; Line Markers&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Marking full lines &amp;amp; line ranges&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range &quot;7-8&quot;
// Line 8 - targeted by range &quot;7-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting line marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;this line is marked as deleted&apos;)
  // This line and the next one are marked as inserted
  console.log(&apos;this is the second inserted line&apos;)

  return &apos;this line uses the neutral default marker type&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding labels to line markers&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Adding long labels on their own lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Using diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Combining syntax highlighting with diff-like syntax&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // This entire block gets highlighted as JavaScript,
    // and we can still add diff markers to it!
-   console.log(&apos;Old code to be removed&apos;)
+   console.log(&apos;New and shiny code!&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Marking individual text inside lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // Mark any given text inside lines
  return &apos;Multiple matches of the given text are supported&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Regular expressions&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;The words yes and yep will be marked.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Escaping forward slashes&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selecting inline marker types (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;These are inserted and deleted marker types&apos;);
  // The return statement uses the default marker type
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Word Wrap&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;Word Wrap&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Configuring word wrap per block&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with wrap=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Configuring indentation of wrapped lines&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent (enabled by default)
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Example with preserveIndent=false
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Collapsible Sections&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;Collapsible Sections&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// This part of the code will be visible by default
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // You can have multiple collapsed sections
  const a = 1
  const b = 2
  const c = a + b

  // This will remain visible
  console.log(`Calculation result: ${a} + ${b} = ${c}`)
  return c
}

// All this code until the end of the block will be collapsed again
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: &apos;End of example boilerplate code&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Line Numbers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;Line Numbers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Displaying line numbers per block&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// This code block will show line numbers
console.log(&apos;Greetings from line 2!&apos;)
console.log(&apos;I am on line 3&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// Line numbers are disabled for this block
console.log(&apos;Hello?&apos;)
console.log(&apos;Sorry, do you know what line I am on?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Changing the starting line number&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Greetings from line 5!&apos;)
console.log(&apos;I am on line 6&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Simple Guides for Fuwari</title><link>https://fuwari.vercel.app/posts/guide/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/guide/</guid><description>How to use this blog template.</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This blog template is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. For the things that are not mentioned in this guide, you may find the answers in the &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Front-matter of Posts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The title of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The date the post was published.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short description of the post. Displayed on index page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cover image path of the post.&amp;lt;br/&amp;gt;1. Start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;: Use web image&amp;lt;br/&amp;gt;2. Start with &lt;code&gt;/&lt;/code&gt;: For image in &lt;code&gt;public&lt;/code&gt; dir&amp;lt;br/&amp;gt;3. With none of the prefixes: Relative to the markdown file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tags of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The category of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If this post is still a draft, which won&apos;t be displayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Where to Place the Post Files&lt;/h2&gt;
&lt;p&gt;Your post files should be placed in &lt;code&gt;src/content/posts/&lt;/code&gt; directory. You can also create sub-directories to better organize your posts and assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Markdown Example</title><link>https://fuwari.vercel.app/posts/markdown/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/markdown/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Sun, 01 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;An h1 header&lt;/h1&gt;
&lt;p&gt;Paragraphs are separated by a blank line.&lt;/p&gt;
&lt;p&gt;2nd paragraph. &lt;em&gt;Italic&lt;/em&gt;, &lt;strong&gt;bold&lt;/strong&gt;, and &lt;code&gt;monospace&lt;/code&gt;. Itemized lists
look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;this one&lt;/li&gt;
&lt;li&gt;that one&lt;/li&gt;
&lt;li&gt;the other one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Block quotes are
written like so.&lt;/p&gt;
&lt;p&gt;They can span multiple paragraphs,
if you like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., &quot;it&apos;s all
in chapters 12--14&quot;). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺&lt;/p&gt;
&lt;h2&gt;An h2 header&lt;/h2&gt;
&lt;p&gt;Here&apos;s a numbered list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;first item&lt;/li&gt;
&lt;li&gt;second item&lt;/li&gt;
&lt;li&gt;third item&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here&apos;s a code sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;define foobar() {
    print &quot;Welcome to flavor country!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(which makes copying &amp;amp; pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time
# Quick, count to ten!
for i in range(10):
    # (but not *too* quick)
    time.sleep(0.5)
    print i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;An h3 header&lt;/h3&gt;
&lt;p&gt;Now a nested list:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First, get these ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;carrots&lt;/li&gt;
&lt;li&gt;celery&lt;/li&gt;
&lt;li&gt;lentils&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boil some water.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dump everything in the pot and follow
this algorithm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; find wooden spoon
 uncover pot
 stir
 cover pot
 balance wooden spoon precariously on pot handle
 wait 10 minutes
 goto first step (or shut off burner when done)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not bump wooden spoon or it will fall.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).&lt;/p&gt;
&lt;p&gt;Here&apos;s a link to &lt;a href=&quot;http://foo.bar&quot;&gt;a website&lt;/a&gt;, to a &lt;a href=&quot;local-doc.html&quot;&gt;local
doc&lt;/a&gt;, and to a &lt;a href=&quot;#an-h2-header&quot;&gt;section heading in the current
doc&lt;/a&gt;. Here&apos;s a footnote [^1].&lt;/p&gt;
&lt;p&gt;[^1]: Footnote text goes here.&lt;/p&gt;
&lt;p&gt;Tables can look like this:&lt;/p&gt;
&lt;p&gt;size material color&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;9 leather brown
10 hemp canvas natural
11 glass transparent&lt;/p&gt;
&lt;p&gt;Table: Shoes, their sizes, and what they&apos;re made of&lt;/p&gt;
&lt;p&gt;(The above is the caption for the table.) Pandoc also supports
multi-line tables:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;keyword text&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;red Sunsets, apples, and
other red or reddish
things.&lt;/p&gt;
&lt;p&gt;green Leaves, grass, frogs
and other things it&apos;s
not easy being.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A horizontal rule follows.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s a definition list:&lt;/p&gt;
&lt;p&gt;apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There&apos;s no &quot;e&quot; in tomatoe.&lt;/p&gt;
&lt;p&gt;Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a &quot;line block&quot;:&lt;/p&gt;
&lt;p&gt;| Line one
| Line too
| Line tree&lt;/p&gt;
&lt;p&gt;and images can be specified like so:&lt;/p&gt;
&lt;p&gt;Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:&lt;/p&gt;
&lt;p&gt;$$I = \int \rho R^{2} dV$$&lt;/p&gt;
&lt;p&gt;$$
\begin{equation*}
\pi
=3.1415926535
;8979323846;2643383279;5028841971;6939937510;5820974944
;5923078164;0628620899;8628034825;3421170679;\ldots
\end{equation*}
$$&lt;/p&gt;
&lt;p&gt;And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: `foo`, *bar*, etc.&lt;/p&gt;
</content:encoded></item><item><title>Include Video in the Posts</title><link>https://fuwari.vercel.app/posts/video/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/video/</guid><description>This post demonstrates how to include embedded video in a blog post.</description><pubDate>Tue, 01 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: Include Video in the Post
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>