<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>WebGL on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/webgl/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sun, 26 Jan 2020 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/webgl/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《WebGL 编程指南》学习总结：渲染基础、核心数学与游戏引擎落地（新人友好版）</title>
      <link>https://ftmao.com/blog/bookstore/webgl-programming-guide-notes/</link>
      <pubDate>Sun, 26 Jan 2020 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/webgl-programming-guide-notes/</guid>
      <description>&lt;h1 id=&#34;webgl-编程指南学习总结&#34;&gt;《WebGL 编程指南》学习总结&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：会一点 JavaScript、想搞懂&amp;quot;3D 画面到底是怎么画出来的&amp;quot;的新人，或从 Cocos Creator 上层 API 往下挖原理的引擎使用者。
参考书籍：Kouichi Matsuda / Rodger Lea《WebGL 编程指南》（WebGL Programming Guide）
关键词：渲染管线、顶点/片元着色器、MVP 矩阵、纹理、深度测试&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;写在前面这本书在讲什么&#34;&gt;写在前面：这本书在讲什么？&lt;/h2&gt;
&lt;p&gt;《WebGL 编程指南》是 WebGL 入门最经典的一本书。它不依赖任何三方库（不用 Three.js），从 &lt;code&gt;canvas&lt;/code&gt; 上一个三角形开始，一步步讲到多物体场景、光照、纹理、雾化。全书的知识结构可以概括成一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((WebGL 编程指南))
    基础准备
      Canvas 与 WebGL 上下文
      WebGL 是状态机
      清空颜色缓冲区
    着色器
      GLSL ES 语言
      顶点着色器
      片元着色器
      attribute / uniform / varying
    几何绘制
      缓冲区对象 Buffer
      三角形与图元装配
      索引绘制 IBO
    数学核心
      向量运算
      矩阵变换
      模型/视图/投影矩阵
    进阶效果
      颜色与纹理
      光照模型
      深度测试与混合
      雾化与三维交互&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;学这本书最大的收获不是&amp;quot;会用 WebGL API&amp;quot;，而是理解&lt;strong&gt;所有 3D 引擎（包括 Cocos Creator、Unity）底层都在做的同一件事&lt;/strong&gt;：把一堆顶点数据，经过数学变换和逐像素计算，画到屏幕上。理解了这套流程，引擎里的材质、Shader、渲染设置对你来说就再也不是黑盒。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一渲染基础知识画面是怎么画出来的&#34;&gt;一、渲染基础知识：画面是怎么画出来的？&lt;/h2&gt;
&lt;h3 id=&#34;11-一句话理解渲染管线&#34;&gt;1.1 一句话理解渲染管线&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;渲染（Rendering）就是把&amp;quot;三维场景的数学描述&amp;quot;转换成&amp;quot;屏幕上一个个像素颜色&amp;quot;的过程。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;WebGL 渲染管线是一条固定流水线，数据从 CPU 流向 GPU，每个阶段各司其职：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[CPU: JavaScript&amp;lt;br/&amp;gt;准备顶点数据] --&amp;gt;|gl.bufferData| B[GPU 缓冲区&amp;lt;br/&amp;gt;顶点坐标/颜色/UV]
    B --&amp;gt; C[顶点着色器&amp;lt;br/&amp;gt;Vertex Shader]
    C --&amp;gt; D[图元装配&amp;lt;br/&amp;gt;Primitive Assembly]
    D --&amp;gt; E[光栅化&amp;lt;br/&amp;gt;Rasterization]
    E --&amp;gt; F[片元着色器&amp;lt;br/&amp;gt;Fragment Shader]
    F --&amp;gt; G[逐片元操作&amp;lt;br/&amp;gt;深度测试/混合]
    G --&amp;gt; H[帧缓冲区&amp;lt;br/&amp;gt;Framebuffer]
    H --&amp;gt; I[显示器画面]

    style C fill:#e1f5ff
    style F fill:#e1f5ff&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;逐个阶段用大白话解释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;准备数据（CPU 侧）&lt;/strong&gt;：JavaScript 把顶点坐标、颜色、纹理坐标（UV）打包成一块&amp;quot;数组&amp;quot;（缓冲区对象 Buffer），传给 GPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顶点着色器&lt;/strong&gt;：对&lt;strong&gt;每一个顶点&lt;/strong&gt;执行一次你写的 GLSL 程序，核心任务是把顶点坐标变换到最终位置（乘 MVP 矩阵），输出到内置变量 &lt;code&gt;gl_Position&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图元装配&lt;/strong&gt;：GPU 按照 &lt;code&gt;gl.drawArrays&lt;/code&gt; 指定的模式（&lt;code&gt;TRIANGLES&lt;/code&gt; 等），把顶点三个三个地拼成三角形。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;光栅化&lt;/strong&gt;：把三角形这个&amp;quot;几何图形&amp;quot;打碎成屏幕上实际的一个个&lt;strong&gt;片元&lt;/strong&gt;（可以近似理解为像素候选）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;片元着色器&lt;/strong&gt;：对&lt;strong&gt;每一个片元&lt;/strong&gt;执行一次 GLSL 程序，算出这个像素最终的颜色，输出到 &lt;code&gt;gl_FragColor&lt;/code&gt;。纹理采样、光照计算都在这里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐片元操作&lt;/strong&gt;：深度测试（谁挡谁）、模板测试、颜色混合（半透明），最后写进帧缓冲区显示出来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;关键点：&lt;strong&gt;顶点着色器和片元着色器是你唯一能编程的两个环节&lt;/strong&gt;，其余都是 GPU 固定管线。WebGL 开发的本质就是&amp;quot;喂数据 + 写这两段 Shader&amp;quot;。&lt;/p&gt;
&lt;h3 id=&#34;12-webgl-是一台状态机&#34;&gt;1.2 WebGL 是一台&amp;quot;状态机&amp;quot;&lt;/h3&gt;
&lt;p&gt;WebGL API 没有&amp;quot;对象&amp;quot;概念，它是一台巨大的状态机。你做的所有事都是：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    S1[创建着色器程序&amp;lt;br/&amp;gt;gl.createProgram] --&amp;gt; S2[绑定到当前状态&amp;lt;br/&amp;gt;gl.useProgram]
    S2 --&amp;gt; S3[创建并绑定缓冲区&amp;lt;br/&amp;gt;gl.bindBuffer]
    S3 --&amp;gt; S4[把缓冲区接到 attribute 上&amp;lt;br/&amp;gt;gl.vertexAttribPointer]
    S4 --&amp;gt; S5[设置 uniform 值&amp;lt;br/&amp;gt;gl.uniformMatrix4fv]
    S5 --&amp;gt; S6[发出绘制命令&amp;lt;br/&amp;gt;gl.drawArrays / drawElements]
    S6 --&amp;gt; S7{还要画别的物体?}
    S7 --&amp;gt;|是| S2
    S7 --&amp;gt;|否| Done[本帧结束]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;新人最容易犯的错就是&lt;strong&gt;忘记&amp;quot;绑定&amp;quot;&lt;/strong&gt;：改了缓冲区 A 的数据，结果绘制时绑着的还是缓冲区 B。记住口诀：&lt;strong&gt;先绑定，再设置，后绘制&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;13-第一个三角形的最小代码骨架&#34;&gt;1.3 第一个三角形的最小代码骨架&lt;/h3&gt;
&lt;p&gt;书里第 2、3 章的核心内容，浓缩起来就是这几步：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 1. 获取 WebGL 上下文
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;canvas&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getContext&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;webgl&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 2. 编译着色器、链接成程序（顶点 + 片元着色器）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;program&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;createProgram&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;VSHADER_SOURCE&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;FSHADER_SOURCE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;useProgram&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;program&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 3. 准备顶点数据并写入缓冲区
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;([&lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;,  &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;,  &lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0.5&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;createBuffer&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;bindBuffer&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ARRAY_BUFFER&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;bufferData&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ARRAY_BUFFER&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;STATIC_DRAW&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 4. 把缓冲区数据&amp;#34;接&amp;#34;到着色器的 attribute 变量上
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;a_Position&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getAttribLocation&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;program&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;a_Position&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;vertexAttribPointer&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;a_Position&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FLOAT&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;enableVertexAttribArray&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;a_Position&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 5. 清空 + 绘制
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;clearColor&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;COLOR_BUFFER_BIT&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;drawArrays&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TRIANGLES&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对应的着色器（GLSL ES）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 顶点着色器：接收每个顶点的位置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attribute&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; a_Position;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gl_Position &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a_Position;   &lt;span style=&#34;color:#75715e&#34;&gt;// 必须写的内置输出&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 片元着色器：给每个片元上色&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;precision&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;mediump&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gl_FragColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);  &lt;span style=&#34;color:#75715e&#34;&gt;// 红色&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;14-三种变量数据怎么流进着色器&#34;&gt;1.4 三种变量：数据怎么流进着色器&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;变量类型&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;数据流向&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;粒度&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;典型用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;attribute&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;JS → 顶点着色器&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;每个顶点不同&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顶点坐标、颜色、UV、法线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;uniform&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;JS → 两个着色器&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;一次绘制内不变&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;MVP 矩阵、光照参数、时间&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;varying&lt;/code&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顶点着色器 → 片元着色器&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;被光栅化插值&lt;/strong&gt;&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;把顶点颜色渐变到三角形内部&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;varying&lt;/code&gt; 特别值得理解：顶点着色器给三个顶点各设了一个颜色，片元着色器拿到的却是&lt;strong&gt;插值后的颜色&lt;/strong&gt;——三角形内部的颜色渐变（渐变三角形）就是这么来的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二核心数学知识向量矩阵与坐标变换&#34;&gt;二、核心数学知识：向量、矩阵与坐标变换&lt;/h2&gt;
&lt;p&gt;这是全书最重要、也是新人最容易劝退的部分。其实只要抓住一条主线：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[局部坐标&amp;lt;br/&amp;gt;模型自己的坐标系] --&amp;gt;|模型矩阵 M| B[世界坐标&amp;lt;br/&amp;gt;摆放在场景里]
    B --&amp;gt;|视图矩阵 V| C[观察坐标&amp;lt;br/&amp;gt;以相机为原点]
    C --&amp;gt;|投影矩阵 P| D[裁剪坐标&amp;lt;br/&amp;gt;齐次坐标]
    D --&amp;gt;|透视除法| E[标准化设备坐标 NDC&amp;lt;br/&amp;gt;-1 到 1]
    E --&amp;gt;|视口变换| F[屏幕像素坐标]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;著名的 &lt;strong&gt;MVP 矩阵&lt;/strong&gt;就是上面三个矩阵的乘积。顶点着色器里一行代码搞定：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gl_Position &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; u_ProjMatrix &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; u_ViewMatrix &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; u_ModelMatrix &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; a_Position;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;21-向量方向与位置&#34;&gt;2.1 向量：方向与位置&lt;/h3&gt;
&lt;p&gt;向量就是一个有方向的箭头，如 $\vec{v} = (x, y, z)$。渲染里有两个运算用最多：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;点积（Dot Product）&lt;/strong&gt;：衡量两个向量&amp;quot;有多同向&amp;quot;。&lt;/p&gt;
&lt;p&gt;$$
\vec{a} \cdot \vec{b} = a_x b_x + a_y b_y + a_z b_z = |\vec{a}||\vec{b}|\cos\theta
$$&lt;/p&gt;
&lt;p&gt;光照计算全靠它：光线方向和表面法线的夹角越接近 $0$，$\cos\theta$ 越大，表面越亮。这就是漫反射光照的核心公式：&lt;/p&gt;
&lt;p&gt;$$
\text{漫反射颜色} = \text{光源颜色} \times \text{表面基底色} \times \max(\vec{n} \cdot \vec{l},\ 0)
$$&lt;/p&gt;
&lt;p&gt;其中 $\vec{n}$ 是表面法线，$\vec{l}$ 是指向光源的方向，$\max(\cdot, 0)$ 保证背光面不会出现&amp;quot;负亮度&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;叉积（Cross Product）&lt;/strong&gt;：求垂直于两个向量的法线，长度为平行四边形面积。&lt;/p&gt;
&lt;p&gt;$$
\vec{a} \times \vec{b} = (a_y b_z - a_z b_y,\ a_z b_x - a_x b_z,\ a_x b_y - a_y b_x)
$$&lt;/p&gt;
&lt;p&gt;知道三角形三个顶点，用两条边叉积就能求出面的法线方向；判断三角形朝向（正面/背面剔除）也靠它。&lt;/p&gt;
&lt;h3 id=&#34;22-齐次坐标为什么坐标是-4-维的&#34;&gt;2.2 齐次坐标：为什么坐标是 4 维的？&lt;/h3&gt;
&lt;p&gt;三维点为什么要写成 $(x, y, z, w)$？因为&lt;strong&gt;平移&lt;/strong&gt;没法用 $3 \times 3$ 矩阵乘法表示，升到 4 维后，平移、旋转、缩放、投影&lt;strong&gt;全部统一成一次矩阵乘法&lt;/strong&gt;。约定：点的 $w = 1$，方向的 $w = 0$（方向不受平移影响，这个约定天然正确）。&lt;/p&gt;
&lt;h3 id=&#34;23-三种基本变换矩阵&#34;&gt;2.3 三种基本变换矩阵&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;平移矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
T = \begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; T_x \\
0 &amp;amp; 1 &amp;amp; 0 &amp;amp; T_y \\
0 &amp;amp; 0 &amp;amp; 1 &amp;amp; T_z \\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;绕 z 轴旋转矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
R_z(\theta) = \begin{bmatrix}
\cos\theta &amp;amp; -\sin\theta &amp;amp; 0 &amp;amp; 0 \\
\sin\theta &amp;amp; \cos\theta &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; 1 &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缩放矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
S = \begin{bmatrix}
S_x &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; S_y &amp;amp; 0 &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; S_z &amp;amp; 0 \\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;矩阵相乘的顺序&lt;strong&gt;非常重要&lt;/strong&gt;：矩阵从右往左作用于顶点。$M = T \cdot R$ 表示&amp;quot;先旋转再平移&amp;quot;，和&amp;quot;先平移再旋转&amp;quot;结果完全不同。书里的 &lt;code&gt;cuon-matrix.js&lt;/code&gt; 提供了 &lt;code&gt;setTranslate / rotate / scale&lt;/code&gt; 等方法，实际项目中 Cocos Creator、gl-matrix 也提供同样的能力，不需要手写。&lt;/p&gt;
&lt;h3 id=&#34;24-视图矩阵与投影矩阵&#34;&gt;2.4 视图矩阵与投影矩阵&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;视图矩阵&lt;/strong&gt;：把整个世界&amp;quot;挪&amp;quot;到相机视角下。由三个参数决定——视点（相机位置）、观察目标点、上方向，即 lookAt：&lt;/p&gt;
&lt;p&gt;$$
V = \text{lookAt}(\text{eye},\ \text{center},\ \text{up})
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;投影矩阵&lt;/strong&gt;有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正射投影（Orthographic）&lt;/strong&gt;：物体远近一样大，像工程图纸，适合 2D 游戏、UI。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;透视投影（Perspective）&lt;/strong&gt;：近大远小，符合人眼，适合 3D 场景。由一个视锥体（near、far、fov、aspect）定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;25-法线矩阵一个容易被忽略的细节&#34;&gt;2.5 法线矩阵：一个容易被忽略的细节&lt;/h3&gt;
&lt;p&gt;对模型做了&lt;strong&gt;非等比缩放&lt;/strong&gt;（比如 x 方向压扁、y 方向拉长）后，法线不能直接乘模型矩阵，要用&lt;strong&gt;模型矩阵的逆转置矩阵&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
\vec{n}&amp;rsquo; = (M^{-1})^{T} \cdot \vec{n}
$$&lt;/p&gt;
&lt;p&gt;书里第 8 章专门讲了这个问题。等比缩放 + 旋转时法线矩阵等于模型矩阵，所以简单场景可以偷懒，但骨骼动画、非均匀缩放时漏掉这步，光照就是错的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三纹理光照与隐藏面消除&#34;&gt;三、纹理、光照与隐藏面消除&lt;/h2&gt;
&lt;h3 id=&#34;31-纹理贴图把图片贴到三角形上&#34;&gt;3.1 纹理贴图：把图片&amp;quot;贴&amp;quot;到三角形上&lt;/h3&gt;
&lt;p&gt;流程五步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JS 加载图片（&lt;code&gt;Image&lt;/code&gt; 对象）。&lt;/li&gt;
&lt;li&gt;创建并绑定纹理对象（&lt;code&gt;gl.createTexture&lt;/code&gt; + &lt;code&gt;gl.bindTexture&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;上传图片数据到 GPU（&lt;code&gt;gl.texImage2D&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;把纹理单元连接到着色器的 &lt;code&gt;uniform sampler2D&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;顶点传 UV 坐标，片元着色器里 &lt;code&gt;texture2D(u_Sampler, v_UV)&lt;/code&gt; 采样取色。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;新人第一坑：&lt;strong&gt;图片在 WebGL 里是上下颠倒的&lt;/strong&gt;，需要 &lt;code&gt;gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)&lt;/code&gt; 翻转 Y 轴。原因：图片坐标系原点在左上，纹理坐标系原点在左下。&lt;/p&gt;
&lt;h3 id=&#34;32-光照环境光--漫反射&#34;&gt;3.2 光照：环境光 + 漫反射&lt;/h3&gt;
&lt;p&gt;书里的光照模型是最经典的 Phong 简化版，新人记住两项就够：&lt;/p&gt;
&lt;p&gt;$$
\text{最终颜色} = \underbrace{\text{环境光} \times \text{基底色}}&lt;em&gt;{\text{照亮背光面，防止死黑}} + \underbrace{\text{平行光颜色} \times \text{基底色} \times \max(\vec{n}\cdot\vec{l}, 0)}&lt;/em&gt;{\text{漫反射}}
$$&lt;/p&gt;
&lt;h3 id=&#34;33-隐藏面消除深度缓冲区&#34;&gt;3.3 隐藏面消除：深度缓冲区&lt;/h3&gt;
&lt;p&gt;画多个三角形时，谁挡住谁？WebGL 用&lt;strong&gt;深度缓冲区（Z-Buffer）&lt;/strong&gt;：每个像素除了存颜色，还存一个深度值。新片元要写入时先和已有深度比较，更近的才覆盖。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;enable&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DEPTH_TEST&lt;/span&gt;);                          &lt;span style=&#34;color:#75715e&#34;&gt;// 开启深度测试
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;COLOR_BUFFER_BIT&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DEPTH_BUFFER_BIT&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// 每帧连深度一起清
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;忘记清深度缓冲区，画面会越画越乱——这是新人第二坑。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四典型问题排查踩坑指南&#34;&gt;四、典型问题排查（踩坑指南）&lt;/h2&gt;
&lt;h3 id=&#34;41-我画的东西是黑屏什么都不显示&#34;&gt;4.1 &amp;ldquo;我画的东西是黑屏/什么都不显示&amp;rdquo;&lt;/h3&gt;
&lt;p&gt;这是 WebGL 新人 90% 的问题。按这个流程排查，命中率极高：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Start[画面黑屏/空白] --&amp;gt; Q1{着色器编译链接&amp;lt;br/&amp;gt;报错了吗?}
    Q1 --&amp;gt;|是| A1[看 gl.getShaderInfoLog&amp;lt;br/&amp;gt;通常是 GLSL 语法错误&amp;lt;br/&amp;gt;或精度限定符缺失]
    Q1 --&amp;gt;|否| Q2{顶点坐标在&amp;lt;br/&amp;gt;-1 到 1 的 NDC 范围内吗?}
    Q2 --&amp;gt;|否| A2[检查 MVP 矩阵&amp;lt;br/&amp;gt;是否被设置/乘反了顺序]
    Q2 --&amp;gt;|是| Q3{三角形顶点顺序&amp;lt;br/&amp;gt;是逆时针 CCW 吗?}
    Q3 --&amp;gt;|否| A3[被背面剔除干掉了&amp;lt;br/&amp;gt;gl.disable CULL_FACE 验证]
    Q3 --&amp;gt;|是| Q4{深度测试开启且&amp;lt;br/&amp;gt;每帧清深度缓冲了吗?}
    Q4 --&amp;gt;|否| A4[gl.clear 加上&amp;lt;br/&amp;gt;DEPTH_BUFFER_BIT]
    Q4 --&amp;gt;|是| Q5{相机在物体&amp;lt;br/&amp;gt;正确的一侧吗?}
    Q5 --&amp;gt;|否| A5[物体在相机背后&amp;lt;br/&amp;gt;或在视锥体 near/far 之外]
    A1 --&amp;gt; OK[问题解决]
    A2 --&amp;gt; OK
    A3 --&amp;gt; OK
    A4 --&amp;gt; OK
    A5 --&amp;gt; OK&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-深度冲突z-fighting&#34;&gt;4.2 深度冲突（Z-Fighting）&lt;/h3&gt;
&lt;p&gt;两个面片距离极近时，深度缓冲精度不够，会出现闪烁的&amp;quot;花纹&amp;quot;（两个面片抢着显示）。&lt;/p&gt;
&lt;p&gt;解决方案（书里第 10 章的方法）：&lt;strong&gt;多边形偏移&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;enable&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;POLYGON_OFFSET_FILL&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;polygonOffset&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);  &lt;span style=&#34;color:#75715e&#34;&gt;// 让后画的面片深度&amp;#34;向后推&amp;#34;一点
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;本质原因也值得一提：透视投影的深度值不是线性的，精度集中在 near 平面附近。所以&lt;strong&gt;near 不要设得过小&lt;/strong&gt;（如 0.0001），越大精度越好。&lt;/p&gt;
&lt;h3 id=&#34;43-半透明物体显示错误&#34;&gt;4.3 半透明物体显示错误&lt;/h3&gt;
&lt;p&gt;开启混合 &lt;code&gt;gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)&lt;/code&gt; 后，半透明物体仍然错乱？因为&lt;strong&gt;混合依赖绘制顺序&lt;/strong&gt;：必须先画不透明物体，再按从远到近排序画半透明物体，且半透明物体之间往往要关闭深度写入（&lt;code&gt;gl.depthMask(false)&lt;/code&gt;）只保留深度测试。&lt;/p&gt;
&lt;h3 id=&#34;44-性能问题draw-call-太多&#34;&gt;4.4 性能问题：Draw Call 太多&lt;/h3&gt;
&lt;p&gt;每个 &lt;code&gt;gl.drawArrays/drawElements&lt;/code&gt; 都有一次 CPU→GPU 的状态切换开销。几千个小物体各画一次，帧率直接崩。思路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;合批（Batching）&lt;/strong&gt;：把使用同一材质的小物体顶点合并到一个大缓冲区，一次画完。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纹理图集（Texture Atlas）&lt;/strong&gt;：小图合成大图，减少纹理切换。&lt;/li&gt;
&lt;li&gt;状态排序：按材质/着色器排序绘制顺序，减少状态切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五在-cocos-creator-中落地这些渲染知识&#34;&gt;五、在 Cocos Creator 中落地这些渲染知识&lt;/h2&gt;
&lt;p&gt;学 WebGL 不是为了手写引擎，而是为了&lt;strong&gt;看得懂 Cocos Creator 在干什么&lt;/strong&gt;。下面对照来看。&lt;/p&gt;
&lt;h3 id=&#34;51-概念对照表&#34;&gt;5.1 概念对照表&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;WebGL 原生概念&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;Cocos Creator 对应物&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顶点/片元着色器程序&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Effect&lt;/strong&gt;（&lt;code&gt;.effect&lt;/code&gt; 文件，Cocos Effect 语法，本质还是 GLSL）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;着色器 + uniform 参数集合&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Material（材质）&lt;/strong&gt;：一个 Effect 实例 + 具体参数值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;attribute 顶点数据&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;Mesh&lt;/code&gt; / &lt;code&gt;MeshRenderer&lt;/code&gt; 的顶点缓冲区&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;uniform 矩阵（MVP）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;引擎自动填充：模型矩阵来自节点 Transform，视图/投影矩阵来自 Camera&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;纹理 + sampler&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Material 上的 &lt;code&gt;mainTexture&lt;/code&gt; 等贴图槽&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;深度测试/混合状态&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;Effect 中 Pass 的 &lt;code&gt;depthStencilState&lt;/code&gt; / &lt;code&gt;blendState&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;gl.drawElements&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;引擎渲染管线（Forward Pipeline）自动按材质合批后提交&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;52-引擎帮你做的事&#34;&gt;5.2 引擎帮你做的事&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 你写的部分
        N1[节点树 + Transform]
        N2[自定义 Effect / Shader]
        N3[材质参数]
    end
    subgraph 引擎自动处理
        E1[遍历场景收集可渲染对象]
        E2[计算模型矩阵&amp;lt;br/&amp;gt;节点世界变换]
        E3[相机视图/投影矩阵]
        E4[按材质排序 + 合批&amp;lt;br/&amp;gt;Draw Call 优化]
        E5[设置 uniform 常量缓冲]
        E6[调用底层图形 API]
    end
    N1 --&amp;gt; E1
    N2 --&amp;gt; E5
    N3 --&amp;gt; E5
    E1 --&amp;gt; E2 --&amp;gt; E3 --&amp;gt; E4 --&amp;gt; E5 --&amp;gt; E6
    E6 --&amp;gt; API[WebGL / WebGPU / Vulkan / Metal]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;你 WebGL 手写时代要做的&amp;quot;传 MVP 矩阵、绑缓冲区、开深度测试、手动合批&amp;quot;，引擎全部代劳了。但&lt;strong&gt;底层语义完全没变&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点的 &lt;code&gt;position/rotation/scale&lt;/code&gt; 就是在拼第 2 节的 $T \cdot R \cdot S$ 模型矩阵；&lt;/li&gt;
&lt;li&gt;Camera 的 &lt;code&gt;fov、near、far&lt;/code&gt; 就是第 2.4 节的透视投影参数（所以 Cocos 里 near 设太小同样会 Z-Fighting）；&lt;/li&gt;
&lt;li&gt;2D 游戏的 &lt;code&gt;Sprite&lt;/code&gt; 本质是&amp;quot;两张三角形拼的矩形 + 纹理采样&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpriteFrame&lt;/code&gt; 图集就是第 4.4 节的纹理合批优化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-自定义-shader知识直接迁移&#34;&gt;5.3 自定义 Shader：知识直接迁移&lt;/h3&gt;
&lt;p&gt;Cocos Creator 里写一个自定义 Effect，核心片段和手写 WebGL 几乎一一对应：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Cocos Effect 的顶点着色器片段&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CCProgram vs &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; vert() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// CC_MAT_MV_PROJ 就是引擎帮你算好的 MVP 矩阵&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; CC_MAT_MV_PROJ &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(In.position, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 片元着色器片段&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CCProgram fs &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; frag() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// mainTexture 就是 uniform sampler2D&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; texture(mainTexture, In.uv);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;看懂前面四章的内容后，Cocos 的 Effect 文档里那些 &lt;code&gt;CC_MAT_MV_PROJ&lt;/code&gt;、&lt;code&gt;CC_HANDLE_UNIFORM&lt;/code&gt;、&lt;code&gt;depthStencilState&lt;/code&gt; 全部都能对上号。遇到&amp;quot;自定义描边、溶解、UV 动画&amp;quot;这类需求时，你写的就是片元着色器里的逐像素计算。&lt;/p&gt;
&lt;h3 id=&#34;54-性能优化落地&#34;&gt;5.4 性能优化落地&lt;/h3&gt;
&lt;p&gt;第 4.4 节的合批思想在 Cocos 里的具体做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI 用自动图集（Auto Atlas）&lt;/strong&gt;：减少纹理切换，让 2D 合批生效；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免跨材质穿插&lt;/strong&gt;：合批会被不同材质打断，注意节点层级排序；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D 场景用 GPU 实例化（Instancing）&lt;/strong&gt;：同网格同材质的大量物体（草地、树木）一次 Draw Call 画完，相当于手动版的 &lt;code&gt;gl.drawElementsInstanced&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用引擎的渲染调试工具看 Draw Call 数&lt;/strong&gt;，和当年数 &lt;code&gt;gl.drawElements&lt;/code&gt; 调用次数是同一件事。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六经典特效-demo-与实现分析&#34;&gt;六、经典特效 Demo 与实现分析&lt;/h2&gt;
&lt;p&gt;游戏里的特效看起来五花八门，拆开看几乎都是四种基本手段的组合：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((特效的四板斧))
    顶点动画
      波浪/旗帜飘动
      草和树摇摆
      顶点挤出描边
    片元计算
      溶解/燃烧
      UV 扭曲(热浪/水波纹)
      菲涅尔边缘光
    粒子系统
      烟雾/火焰
      雨雪/魔法拖尾
    后处理
      泛光 Bloom
      模糊/暗角&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;下面挑三个最经典的特效，从原理、数学、Shader 代码到 Cocos Creator 落地完整走一遍。&lt;/p&gt;
&lt;h3 id=&#34;61-烟雾粒子系统billboard--alpha-混合&#34;&gt;6.1 烟雾：粒子系统（Billboard + Alpha 混合）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一句话原理&lt;/strong&gt;：烟雾不是&amp;quot;一个模型&amp;quot;，而是几百个&lt;strong&gt;始终面向摄像机的半透明小方片&lt;/strong&gt;（粒子），各自经历出生、上升、变大、变淡、死亡的生命周期，叠加起来就是一蓬烟。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[发射器&amp;lt;br/&amp;gt;按速率生成粒子] --&amp;gt; B[初始化属性&amp;lt;br/&amp;gt;位置/速度/大小/寿命]
    B --&amp;gt; C[每帧更新&amp;lt;br/&amp;gt;位置 += 速度·dt&amp;lt;br/&amp;gt;透明度随寿命衰减]
    C --&amp;gt; D{寿命耗尽?}
    D --&amp;gt;|否| E[渲染: Billboard 面片&amp;lt;br/&amp;gt;采样烟雾纹理 + Alpha 混合]
    D --&amp;gt;|是| F[回收粒子&amp;lt;br/&amp;gt;进入对象池]
    E --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;核心技巧一：Billboard（公告板）&lt;/strong&gt;。粒子面片必须永远正对相机，否则侧着看就露馅。做法是从视图矩阵里取出相机的右方向和上方向向量（视图矩阵第一、二列就是它们），在顶点着色器里现场拼面片：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 顶点着色器：粒子中心位置 + 相机坐标系下的二维偏移&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;uniform&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; u_CameraRight;   &lt;span style=&#34;color:#75715e&#34;&gt;// 视图矩阵第一列&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;uniform&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; u_CameraUp;      &lt;span style=&#34;color:#75715e&#34;&gt;// 视图矩阵第二列&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attribute&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; a_Center;      &lt;span style=&#34;color:#75715e&#34;&gt;// 粒子中心（每粒子数据）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attribute&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec2&lt;/span&gt; a_Corner;      &lt;span style=&#34;color:#75715e&#34;&gt;// 面片角点偏移 (-1~1)，每顶点数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;attribute&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; a_Size;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; pos &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a_Center
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; u_CameraRight &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; a_Corner.x &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; a_Size
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; u_CameraUp    &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; a_Corner.y &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; a_Size;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gl_Position &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; u_ProjMatrix &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; u_ViewMatrix &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(pos, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;核心技巧二：透明度随生命周期衰减&lt;/strong&gt;。用一个 0~1 的生命进度 $t_{\text{life}}$ 控制淡出，片元着色器：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; tex &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; texture2D(u_SmokeTex, v_UV);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 淡入淡出：两头为 0，中间最浓，一条简单的抛物线&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; fade &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4.0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; v_Life &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (&lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; v_Life);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gl_FragColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(tex.rgb, tex.a &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; fade &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; u_Opacity);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;渲染状态设置&lt;/strong&gt;（对照 4.3 节的半透明坑）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;enable&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BLEND&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;blendFunc&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;SRC_ALPHA&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ONE_MINUS_SRC_ALPHA&lt;/span&gt;); &lt;span style=&#34;color:#75715e&#34;&gt;// 烟雾用普通混合
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// gl.blendFunc(gl.SRC_ALPHA, gl.ONE);              // 火焰改用加法混合，越叠越亮
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;enable&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DEPTH_TEST&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gl&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;depthMask&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 关键：只测深度、不写深度，粒子之间不互相遮挡
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;性能坑&lt;/strong&gt;：烟雾最大的开销不是粒子数量，而是 &lt;strong&gt;Overdraw&lt;/strong&gt;——大面积半透明面片层层叠叠，同一个像素被反复着色。优化方向：控制单粒子最大尺寸、用更低分辨率渲染粒子再合成（引擎做法）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cocos Creator 落地&lt;/strong&gt;：&lt;code&gt;ParticleSystem&lt;/code&gt;（3D）/ &lt;code&gt;ParticleSystem2D&lt;/code&gt; 组件就是这套东西的完整实现——发射速率、生命周期、颜色/大小随时间曲线、Billboard 渲染模式、材质混合模式，面板上的每个参数都能在上面找到对应原理。想自定义就用 &lt;code&gt;renderMode&lt;/code&gt; 配合自定义材质。&lt;/p&gt;
&lt;h3 id=&#34;62-逼真波浪顶点动画正弦叠加--gerstner-波&#34;&gt;6.2 逼真波浪：顶点动画（正弦叠加 → Gerstner 波）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一句话原理&lt;/strong&gt;：水面网格的&lt;strong&gt;几何形状本身不动&lt;/strong&gt;，每帧在顶点着色器里用波形函数把每个顶点的 y 坐标&amp;quot;抬&amp;quot;起来，再重新算法线做光照——看到的就是流动的波浪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;入门版：正弦波叠加&lt;/strong&gt;。单个正弦波：&lt;/p&gt;
&lt;p&gt;$$
y = A \sin(\omega, x + \varphi, t)
$$&lt;/p&gt;
&lt;p&gt;其中 $A$ 是振幅（波多高），$\omega$ 是频率（波多密），$\varphi$ 控制波速，$t$ 是时间。多个不同方向、频率、振幅的正弦波相加，水面就&amp;quot;乱&amp;quot;得自然了：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; wave(&lt;span style=&#34;color:#66d9ef&#34;&gt;vec2&lt;/span&gt; p, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec2&lt;/span&gt; dir, &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; freq, &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; amp, &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; speed) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; phase &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(dir, p) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; freq &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; u_Time &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; speed;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; amp &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; sin(phase);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 顶点着色器里：y = wave(p, d1,...) + wave(p, d2,...) + ...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;进阶版：Gerstner 波&lt;/strong&gt;。正弦波的波峰是圆滚滚的，真实海浪的波峰是&lt;strong&gt;尖锐&lt;/strong&gt;的。Gerstner 波在抬升 y 的同时把顶点&lt;strong&gt;水平拉向波峰&lt;/strong&gt;，模拟出尖峰效果：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
x&amp;rsquo; &amp;amp;= x + Q A, D_x \cos!\big(\omega(\vec{D}\cdot\vec{p}) + \varphi t\big) \\
y&amp;rsquo; &amp;amp;= A \sin!\big(\omega(\vec{D}\cdot\vec{p}) + \varphi t\big) \\
z&amp;rsquo; &amp;amp;= z + Q A, D_z \cos!\big(\omega(\vec{D}\cdot\vec{p}) + \varphi t\big)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;其中 $\vec{D}$ 是波传播方向，$Q$ 是&amp;quot;尖锐度&amp;quot;参数：$Q = 0$ 退化为正弦波，$Q$ 越大波峰越尖（超过上限会自我穿插，一般取 $0 \le QA\omega \le 1$）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键细节：法线必须跟着重算&lt;/strong&gt;。顶点位移后，原来的法线就不对了，光照会露馅。严谨做法是对波形函数求偏导，用解析法得到切线 $\vec{T}$ 和副切线 $\vec{B}$，法线就是 $\vec{n} = \vec{B} \times \vec{T}$。GAMES101 等课程里有完整推导，新人知道&amp;quot;顶点位移后要重算法线&amp;quot;这个坑即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;片元着色器里的水面质感&lt;/strong&gt;：靠菲涅尔（Fresnel）效应——视线越贴近水面（掠射角），反射越强、越像镜子；垂直看下去则越透。Schlick 近似公式：&lt;/p&gt;
&lt;p&gt;$$
F(\theta) = F_0 + (1 - F_0),(1 - \cos\theta)^5
$$&lt;/p&gt;
&lt;p&gt;水的 $F_0 \approx 0.02$。用 $F$ 在&amp;quot;水体本色&amp;quot;和&amp;quot;天空/环境反射色&amp;quot;之间插值，水面瞬间就有真实感。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑提醒&lt;/strong&gt;：顶点动画的前提是&lt;strong&gt;网格顶点足够密&lt;/strong&gt;——一块只有 4 个顶点的平面怎么抬都是平的。要么建模时细分网格，要么用 tessellation。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cocos Creator 落地&lt;/strong&gt;：水面是自定义 Effect 的经典练手项目——&lt;code&gt;cc.time&lt;/code&gt;（引擎内置 uniform）提供时间，顶点着色器里写 Gerstner 位移，材质面板上把振幅、频率、波速、方向暴露成可调参数。社区商城里大量水面 Shader 都是这个套路的变体。&lt;/p&gt;
&lt;h3 id=&#34;63-溶解噪声纹理--discard&#34;&gt;6.3 溶解：噪声纹理 + discard&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一句话原理&lt;/strong&gt;：片元着色器里采样一张噪声图，噪声值低于阈值的像素直接丢弃（&lt;code&gt;discard&lt;/code&gt;），阈值随时间从 0 推到 1，物体就&amp;quot;一点点消失&amp;quot;；在阈值边缘画一条发光边，就是燃烧/消散效果。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[采样噪声纹理&amp;lt;br/&amp;gt;得到 noise 值 0~1] --&amp;gt; B{noise &amp;lt; 阈值 threshold?}
    B --&amp;gt;|是| C[discard&amp;lt;br/&amp;gt;该像素不画]
    B --&amp;gt;|否| D{noise &amp;lt; threshold + 边缘宽度?}
    D --&amp;gt;|是| E[输出边缘发光色&amp;lt;br/&amp;gt;如橙红火焰色]
    D --&amp;gt;|否| F[正常输出原纹理颜色]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心代码极简：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; noise &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; texture2D(u_NoiseTex, v_UV).r;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (noise &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; u_Threshold) &lt;span style=&#34;color:#66d9ef&#34;&gt;discard&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 边缘宽度内混入燃烧色，smoothstep 让过渡自然&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; edge &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; smoothstep(u_Threshold, u_Threshold &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; u_EdgeWidth, noise);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; color &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mix(texture2D(u_MainTex, v_UV).rgb, u_BurnColor.rgb, edge);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gl_FragColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(color, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;discard&lt;/code&gt; 就是 WebGL 里的&amp;quot;这个片元不要了&amp;quot;——对照第 1.1 节的管线，它在片元着色器阶段直接终止了后续写入。噪声图的选择决定溶解风格：Perlin 噪声 = 烟雾状消散，碎裂纹理 = 破碎消失，斜向渐变 = 扫描式消失。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cocos Creator 落地&lt;/strong&gt;：这是新手写自定义材质的最佳起点——抄一个 builtin Effect，加 &lt;code&gt;noiseTexture&lt;/code&gt;、&lt;code&gt;threshold&lt;/code&gt;、&lt;code&gt;edgeColor&lt;/code&gt; 三个材质属性，JS 里用 tween 把 threshold 从 0 推到 1，就是角色死亡消散、传送门出现等常见需求。&lt;/p&gt;
&lt;h3 id=&#34;64-更多特效的快速索引&#34;&gt;6.4 更多特效的快速索引&lt;/h3&gt;
&lt;p&gt;同样套路能覆盖的常见需求，新人可以先建立&amp;quot;这个效果属于哪一板斧&amp;quot;的直觉：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;特效&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;属于&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;核心技术点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;火焰&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;粒子系统&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;加法混合 + 颜色随生命从黄到红&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;旗帜/头发飘动&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顶点动画&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;正弦波，振幅随 UV 或骨骼权重衰减&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;模型描边&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;顶点动画&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;沿法线挤出背面 + 正面剔除&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;热浪/水下扭曲&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;片元计算&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;噪声扰动 UV 后再采样&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;全息/护盾&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;片元计算&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;菲涅尔边缘光 + 扫描线（&lt;code&gt;fract(uv.y - time)&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;径向模糊/泛光&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;后处理&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;帧缓冲区（FBO）离屏渲染 + 二次全屏绘制&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七总结与学习路线建议&#34;&gt;七、总结与学习路线建议&lt;/h2&gt;
&lt;p&gt;回顾全文，WebGL 学习的核心脉络是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;管线&lt;/strong&gt;：数据从 CPU 缓冲区 → 顶点着色器 → 光栅化 → 片元着色器 → 帧缓冲，能编程的只有两个着色器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数学&lt;/strong&gt;：向量点积（光照）、叉积（法线）、$4\times4$ 矩阵（MVP 变换）是必须过关的三块；齐次坐标是为了统一平移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态机思维&lt;/strong&gt;：先绑定、再设置、后绘制；忘了绑定或忘了清缓冲区是最高频 bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排查有套路&lt;/strong&gt;：黑屏先看着色器日志，再查 NDC 范围、背面剔除、深度缓冲、相机位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;落地到引擎&lt;/strong&gt;：Cocos Creator 的 Effect/材质/合批，全部是这套原理的封装——学透 WebGL，引擎文档读起来就是&amp;quot;老朋友换新名字&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特效有套路&lt;/strong&gt;：再炫的效果也逃不出四板斧——顶点动画、片元计算、粒子系统、后处理，看懂烟雾、波浪、溶解三个例子就能举一反三。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;建议的动手顺序（对应原书章节）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    S1[Ch2-3&amp;lt;br/&amp;gt;画三角形&amp;lt;br/&amp;gt;理解缓冲区] --&amp;gt; S2[Ch4&amp;lt;br/&amp;gt;矩阵变换&amp;lt;br/&amp;gt;让图形动起来]
    S2 --&amp;gt; S3[Ch5&amp;lt;br/&amp;gt;颜色与纹理]
    S3 --&amp;gt; S4[Ch7&amp;lt;br/&amp;gt;三维世界&amp;lt;br/&amp;gt;视图+投影矩阵]
    S4 --&amp;gt; S5[Ch8&amp;lt;br/&amp;gt;光照]
    S5 --&amp;gt; S6[Ch9-10&amp;lt;br/&amp;gt;多物体/高级技巧&amp;lt;br/&amp;gt;雾化/帧缓冲/选中物体]
    S6 --&amp;gt; S7[读 Cocos Creator&amp;lt;br/&amp;gt;Effect 源码&amp;lt;br/&amp;gt;自定义 Shader 实战]
    S7 --&amp;gt; S8[经典特效实战&amp;lt;br/&amp;gt;粒子烟雾/Gerstner 波浪&amp;lt;br/&amp;gt;噪声溶解]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;下一步推荐&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跟着原书把每个示例亲手敲一遍，改参数观察变化（比如把投影矩阵的 near 改大改小，直观感受 Z-Fighting）。&lt;/li&gt;
&lt;li&gt;在 Cocos Creator 里打开一个官方材质，逐行读懂它的 Effect 文件。&lt;/li&gt;
&lt;li&gt;有余力再看《Fundamentals of Computer Graphics》（虎书）补全光栅化、光线追踪的理论体系。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
