<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>游戏与图形 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/shelves/%E6%B8%B8%E6%88%8F%E4%B8%8E%E5%9B%BE%E5%BD%A2/</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/shelves/%E6%B8%B8%E6%88%8F%E4%B8%8E%E5%9B%BE%E5%BD%A2/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>
    <item>
      <title>《游戏开发物理学》学习笔记：从运动学到刚体碰撞的新手实战指南</title>
      <link>https://ftmao.com/blog/bookstore/game-physics-bourg-notes/</link>
      <pubDate>Thu, 16 Jan 2020 11:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/game-physics-bourg-notes/</guid>
      <description>&lt;h1 id=&#34;游戏开发物理学学习笔记从运动学到刚体碰撞的新手实战指南&#34;&gt;《游戏开发物理学》学习笔记：从运动学到刚体碰撞的新手实战指南&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;参考书籍：《Physics for Game Developers》（David M. Bourg，O&amp;rsquo;Reilly，中译名《游戏开发物理学》）
目标读者：想自己做物理效果（而不是只会调引擎参数）的游戏开发者、图形学/仿真方向的新手。
阅读建议：这篇文章帮你建立&lt;strong&gt;主干知识框架&lt;/strong&gt;：先数学，再运动学 → 动力学 → 载具模拟 → 实时积分 → 碰撞 → 粒子，最后给出避坑清单和主流引擎对照。公式只保留实战中真正会用的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一这本书到底讲什么&#34;&gt;一、这本书到底讲什么？&lt;/h2&gt;
&lt;p&gt;很多新手做游戏的物理效果，路径是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 Unity/UE 拖一个 Rigidbody，调调质量、摩擦力，能跑就行；&lt;/li&gt;
&lt;li&gt;遇到「球穿过墙了」「车开起来像肥皂盒」「碰撞后能量爆炸越弹越高」就抓瞎；&lt;/li&gt;
&lt;li&gt;回头发现：不是引擎不行，是自己不懂底层的物理模型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bourg 这本书的价值就在于：&lt;strong&gt;它把物理引擎内部在干什么，用程序员能实现的粒度讲了一遍&lt;/strong&gt;。它不追求物理学的严谨完备，而是追求「在 60 FPS 的游戏循环里，用可承受的计算量，得到看起来正确、玩起来稳定的结果」。&lt;/p&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((游戏开发物理学))
    数学基础
      向量与运算
      矩阵与坐标变换
      四元数
    运动学 Kinematics
      位移 速度 加速度
      抛体运动
    动力学 Dynamics
      牛顿定律
      力与力矩
      流体阻力
    载具模拟
      飞机
      舰船
      气垫船
      汽车
    实时模拟
      数值积分
      游戏循环
      时间步长
    碰撞
      碰撞检测
      碰撞反应 冲量
    粒子系统&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&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;物理引擎 90% 的代码是向量、矩阵和四元数运算。这一关不过，后面全看不懂。&lt;/p&gt;
&lt;h3 id=&#34;21-向量物理量的通用语言&#34;&gt;2.1 向量：物理量的通用语言&lt;/h3&gt;
&lt;p&gt;位置、速度、力、动量，全部是向量。必须熟练的运算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加减&lt;/strong&gt;：力的合成（$F = F_1 + F_2$）、相对速度；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;点积&lt;/strong&gt;（结果是一个数）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}||\mathbf{b}|\cos\theta
$$&lt;/p&gt;
&lt;p&gt;实战用途：求投影（比如速度沿碰撞法线方向的分量）、判断两个方向夹角是锐角还是钝角（点积大于 0 还是小于 0）。碰撞反应里「物体是否在相互靠近」就是靠点积判断的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;叉积&lt;/strong&gt;（结果是一个垂直向量，仅 3D；2D 中退化为一个标量）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
|\mathbf{a} \times \mathbf{b}| = |\mathbf{a}||\mathbf{b}|\sin\theta
$$&lt;/p&gt;
&lt;p&gt;实战用途：&lt;strong&gt;力矩&lt;/strong&gt; $\tau = \mathbf{r} \times \mathbf{F}$（力作用点偏离质心会产生转动）、2D 碰撞冲量里的旋转项。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;归一化&lt;/strong&gt;：$\hat{\mathbf{v}} = \mathbf{v} / |\mathbf{v}|$，只要方向不要大小（碰撞法线必须归一化，忘记这步是经典 bug）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：2D 里叉积 $\mathbf{r} \times \mathbf{F}$ 是标量 $r_x F_y - r_y F_x$，正负号代表顺时针/逆时针，和引擎的坐标系朝向约定有关，写之前先定好「逆时针为正」并全程统一。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;22-矩阵与坐标变换&#34;&gt;2.2 矩阵与坐标变换&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;strong&gt;世界坐标系&lt;/strong&gt;（所有物体一起碰撞、渲染的地方）。物理计算每天的工作就是在这两个系之间来回变换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-四元数3d-姿态的正确打开方式&#34;&gt;2.3 四元数：3D 姿态的正确打开方式&lt;/h3&gt;
&lt;p&gt;3D 中表示朝向有三种方式：&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;欧拉角（roll/pitch/yaw）&lt;/td&gt;
					&lt;td&gt;直观，3 个数&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;万向锁&lt;/strong&gt;（gimbal lock），插值不平滑&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;旋转矩阵&lt;/td&gt;
					&lt;td&gt;直接用于向量变换&lt;/td&gt;
					&lt;td&gt;9 个数冗余，数值误差积累会「变形」&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;四元数&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;4 个数紧凑，插值平滑，无万向锁&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;h3 id=&#34;24-微积分只需要两个直觉&#34;&gt;2.4 微积分：只需要两个直觉&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; = 累积。已知加速度，累加得到速度；累加速度得到位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游戏里你永远不会真的去解符号积分——而是用数值积分一步步逼近（见第六章）。理解「导数是斜率、积分是累加」就够了。&lt;/p&gt;
&lt;h3 id=&#34;25-单位制与坐标系开工前先立规矩&#34;&gt;2.5 单位制与坐标系：开工前先立规矩&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（这是新手第一大坑）：开工前必须白纸黑字定下来——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长度单位是&lt;strong&gt;米&lt;/strong&gt;还是&lt;strong&gt;像素&lt;/strong&gt;？重力加速度是 9.81 m/s² 还是 981 px/s²？混用必出灵异现象。&lt;/li&gt;
&lt;li&gt;2D 屏幕坐标 &lt;strong&gt;y 轴向下&lt;/strong&gt;，而物理教科书 y 轴向上。直接照抄公式会把「下落」写成「飞天」。&lt;/li&gt;
&lt;li&gt;角度用&lt;strong&gt;弧度&lt;/strong&gt;，所有三角函数接口都是弧度，传角度进去结果毫无明显错误提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三运动学kinematics只描述运动不问原因&#34;&gt;三、运动学（Kinematics）：只描述运动，不问原因&lt;/h2&gt;
&lt;p&gt;运动学关心「物体怎么动」，不关心「为什么动」（那是动力学的事）。&lt;/p&gt;
&lt;h3 id=&#34;31-三个基本量&#34;&gt;3.1 三个基本量&lt;/h3&gt;
&lt;p&gt;$$
\text{速度 } \mathbf{v} = \frac{d\mathbf{x}}{dt}, \qquad \text{加速度 } \mathbf{a} = \frac{d\mathbf{v}}{dt}
$$&lt;/p&gt;
&lt;p&gt;匀加速运动（加速度恒定）下的三个实用公式：&lt;/p&gt;
&lt;p&gt;$$
v = v_0 + at
$$&lt;/p&gt;
&lt;p&gt;$$
x = x_0 + v_0 t + \frac{1}{2} a t^2
$$&lt;/p&gt;
&lt;p&gt;$$
v^2 = v_0^2 + 2a(x - x_0)
$$&lt;/p&gt;
&lt;h3 id=&#34;32-抛体运动游戏里最值钱的运动学&#34;&gt;3.2 抛体运动：游戏里最值钱的运动学&lt;/h3&gt;
&lt;p&gt;抛体（炮弹、篮球、跳跃的角色）的核心思想一句话：&lt;strong&gt;水平方向和竖直方向互相独立，分开算&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平：无加速度，匀速 $x = v_0 \cos\theta \cdot t$&lt;/li&gt;
&lt;li&gt;竖直：受重力，$y = v_0 \sin\theta \cdot t - \frac{1}{2} g t^2$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由这两个式子可以推出（落回同一高度时）：&lt;/p&gt;
&lt;p&gt;$$
\text{射程 } R = \frac{v_0^2 \sin 2\theta}{g}, \qquad \text{最高点 } H = \frac{v_0^2 \sin^2\theta}{2g}
$$&lt;/p&gt;
&lt;p&gt;两个直接能用在游戏里的结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;45 度抛射射程最远&lt;/strong&gt;（不计空气阻力时）；&lt;/li&gt;
&lt;li&gt;已知初速度和目标距离，可以反解出抛射角——这就是「抛物线命中目标」的数学基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;典型问题：炮弹打移动目标的提前量。&lt;/strong&gt; 设目标当前位置 $\mathbf{p}$、速度 $\mathbf{v}_t$，炮弹速度 $s$，求解命中时间 $t$：&lt;/p&gt;
&lt;p&gt;$$
|\mathbf{p} + \mathbf{v}_t t| = s t
$$&lt;/p&gt;
&lt;p&gt;两边平方得到一个关于 $t$ 的一元二次方程，取最小正根，瞄准点就取 $\mathbf{p} + \mathbf{v}_t t$。AI 炮塔、技能预判全是这个套路。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：二次方程可能无解（目标跑得太快炮弹追不上），代码里必须处理判别式小于 0 的情况，否则 AI 会对着永远打不中的目标开火。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四作用力与动力学dynamics运动的原因&#34;&gt;四、作用力与动力学（Dynamics）：运动的原因&lt;/h2&gt;
&lt;h3 id=&#34;41-牛顿三定律的程序员版本&#34;&gt;4.1 牛顿三定律的程序员版本&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; $\mathbf{F} = m\mathbf{a}$：所有物理引擎的心脏。&lt;strong&gt;先把作用在物体上的所有力加起来（合力），除以质量得到加速度，然后交给积分器&lt;/strong&gt;；&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;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;质量 $m$&lt;/td&gt;
					&lt;td&gt;转动惯量 $I$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;力 $\mathbf{F}$&lt;/td&gt;
					&lt;td&gt;力矩 $\tau = \mathbf{r} \times \mathbf{F}$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;加速度 $\mathbf{a} = \mathbf{F}/m$&lt;/td&gt;
					&lt;td&gt;角加速度 $\alpha = \tau / I$&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;速度 $\mathbf{v}$&lt;/td&gt;
					&lt;td&gt;角速度 $\omega$&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;转动惯量的直觉：质量分布离转轴越远越难转（花样滑冰收手加速）。2D 里 $I$ 就是一个标量（如均匀圆盘 $I = \frac{1}{2}mr^2$，细杆绕中心 $I = \frac{1}{12}mL^2$）；3D 里是一个 3×3 张量矩阵，通常直接在&lt;strong&gt;局部坐标系&lt;/strong&gt;下预计算并求逆，避免每帧重复。&lt;/p&gt;
&lt;h3 id=&#34;42-游戏里最常见的几种力&#34;&gt;4.2 游戏里最常见的几种力&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重力&lt;/strong&gt;：$F = mg$，恒定向下，人人有份；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弹簧力&lt;/strong&gt;：胡克定律 $F = -kx$（$k$ 是劲度系数），悬挂、软绳、镜头跟随弹性都靠它；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;摩擦力&lt;/strong&gt;：静摩擦（小于阈值时完全抵消切向力）与动摩擦 $F = \mu N$（$\mu$ 摩擦系数，$N$ 法向压力）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流体阻力&lt;/strong&gt;（空气/水）：低速近似线性 $F = -k_1 v$，高速用二次项：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
F_d = -\frac{1}{2} \rho v^2 C_d A , \hat{\mathbf{v}}
$$&lt;/p&gt;
&lt;p&gt;$\rho$ 是流体密度，$C_d$ 阻力系数，$A$ 是迎流面积。飞机、舰船、弹道下坠感全靠它。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：二次阻力在速度很大时力也爆炸式增大，直接积分容易数值不稳定（物体来回震荡甚至飞出屏幕）。实战技巧是给阻力加速度设上限，或改用半隐式积分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五载具物理本书的招牌四章&#34;&gt;五、载具物理：本书的招牌四章&lt;/h2&gt;
&lt;p&gt;Bourg 用飞机、舰船、气垫船、汽车四个例子，展示同一套框架（合力 → 加速度 → 积分）如何套出完全不同的手感。掌握套路比记住每个参数重要。&lt;/p&gt;
&lt;h3 id=&#34;51-飞机四力平衡&#34;&gt;5.1 飞机：四力平衡&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 LR
    T[推力 Thrust&amp;lt;br/&amp;gt;发动机] --&amp;gt;|向前| P((飞机))
    D[阻力 Drag&amp;lt;br/&amp;gt;空气摩擦与压差] --&amp;gt;|向后| P
    L[升力 Lift&amp;lt;br/&amp;gt;机翼上下压差] --&amp;gt;|向上| P
    W[重力 Weight&amp;lt;br/&amp;gt;mg] --&amp;gt;|向下| P&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;核心公式是升力和阻力，共享同一个「动压」结构：&lt;/p&gt;
&lt;p&gt;$$
L = \frac{1}{2} \rho v^2 S C_L, \qquad D = \frac{1}{2} \rho v^2 S C_D
$$&lt;/p&gt;
&lt;p&gt;其中 $S$ 是机翼面积，$C_L$ 升力系数。关键知识点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$C_L$ 随&lt;strong&gt;迎角&lt;/strong&gt;（机翼与气流的夹角）增大而增大，但超过临界迎角后骤减——这就是&lt;strong&gt;失速&lt;/strong&gt;，飞机升力瞬间消失往下掉。模拟飞行手感，失速是必须实现的；&lt;/li&gt;
&lt;li&gt;滚转、俯仰、偏航三个通道各自独立输入力矩，组合出飞行姿态；&lt;/li&gt;
&lt;li&gt;升力正比于 $v^2$：速度不够就拉不起来，这是「起飞需要跑道」的物理原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：完整的空气动力学模型极复杂，游戏里几乎没人真做 CFD。标准做法是&lt;strong&gt;查表/分段线性&lt;/strong&gt;：把 $C_L$ 和 $C_D$ 做成关于迎角的查找表，手感调好就是胜利。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;52-舰船浮力与流体阻力的天下&#34;&gt;5.2 舰船：浮力与流体阻力的天下&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;：浮心与重心的相对位置决定船会不会翻。重心太高、浮心太低 = 头重脚轻，一晃就翻；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻力&lt;/strong&gt;：水的密度约为空气的 800 多倍，所以舰船阻力以&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;h3 id=&#34;53-气垫船低摩擦的特殊手感&#34;&gt;5.3 气垫船：低摩擦的特殊手感&lt;/h3&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;strong&gt;转弯时船身会侧滑&lt;/strong&gt;，直到横向水/气阻力把侧向速度消耗掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现上就是在普通刚体模型里把接触摩擦调得极小，再加分方向差异很大的阻力系数（横向阻力大、纵向阻力小）。「阻力系数各向异性」是气垫船和所有水上载具手感的灵魂。&lt;/p&gt;
&lt;h3 id=&#34;54-汽车一切手感都在轮胎上&#34;&gt;5.4 汽车：一切手感都在轮胎上&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;/p&gt;
&lt;ol&gt;
&lt;li&gt;驱动力 $F = \text{engineForce}$，随速度衰减（极速的来源：驱动力 = 阻力时不再加速）；&lt;/li&gt;
&lt;li&gt;阻力 = 滚动阻力 + 空气阻力（二次项主导高速）；&lt;/li&gt;
&lt;li&gt;转向时按当前速度把车头方向「往速度方向拉」，拉伸强度代表抓地力——强度低就是漂移手感。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：很多新手给汽车直接用「速度 = 车头方向 × 油门」，车像轨道车一样瞬间转向，毫无惯性。正确做法是&lt;strong&gt;转向改的是车头朝向，速度方向靠轮胎力逐渐跟上&lt;/strong&gt;，这个「跟不上」就是车感的来源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六实时模拟数值积分与时间步长&#34;&gt;六、实时模拟：数值积分与时间步长&lt;/h2&gt;
&lt;p&gt;前面所有公式给出的是「这一瞬间的加速度」。游戏是一帧一帧跑的，核心问题是：&lt;strong&gt;怎么从加速度一步步推出位置和姿态？&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;61-游戏物理主循环&#34;&gt;6.1 游戏物理主循环&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 TD
    A[读取输入] --&amp;gt; B[计算所有作用力与力矩&amp;lt;br/&amp;gt;重力/推力/阻力/弹簧...]
    B --&amp;gt; C[求合力 F 与合力矩 τ&amp;lt;br/&amp;gt;a = F/m, α = τ/I]
    C --&amp;gt; D[数值积分&amp;lt;br/&amp;gt;更新速度 v、角速度 ω]
    D --&amp;gt; E[再积分&amp;lt;br/&amp;gt;更新位置 x、姿态 q]
    E --&amp;gt; F[碰撞检测]
    F --&amp;gt; G{有碰撞?}
    G --&amp;gt;|是| H[碰撞反应&amp;lt;br/&amp;gt;施加冲量修正速度]
    G --&amp;gt;|否| I[渲染]
    H --&amp;gt; I
    I --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-三种积分方法对比&#34;&gt;6.2 三种积分方法对比&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;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;$x(t+h) = x(t) + v(t),h$&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;strong&gt;半隐式欧拉&lt;/strong&gt;（Symplectic Euler）&lt;/td&gt;
					&lt;td&gt;先 $v(t+h) = v(t) + a,h$，再 $x(t+h) = x(t) + v(t+h),h$&lt;/td&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;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;RK4&lt;/td&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;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;半隐式欧拉只比显式欧拉改了一个顺序（&lt;strong&gt;用新速度更新位置&lt;/strong&gt;），稳定性却有质的飞跃——这是书里最划算的「一行代码的智慧」。&lt;/p&gt;
&lt;h3 id=&#34;63-时间步长实时模拟的头号工程问题&#34;&gt;6.3 时间步长：实时模拟的头号工程问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定时间步&lt;/strong&gt;：物理每 1/60 秒（或 1/120 秒）推进一步，渲染帧率高时插值、低时多补几步。优点：结果可复现（回放、联机同步必须用它）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可变时间步&lt;/strong&gt;：直接用上一帧的耗时当 $h$。优点简单；缺点致命——帧率波动直接改变物理行为，同一台机器跑两遍结果都不一样。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（ spiral of death，死亡螺旋）：帧率突然下降 → 单帧物理步数暴增 → 物理计算更慢 → 帧率进一步下降。解法：给每帧物理步数设上限（如最多 5 步），宁可游戏慢动作也不让模拟爆炸。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑（隧道效应 tunneling）：步长太大 + 速度太快 → 子弹一帧飞过了整面墙，两帧采样点都在墙外，碰撞检测永远发现不了。解法：小步长、射线扫掠检测（swept collision / CCD），或对高速物体单独细分步长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七刚体旋转从粒子到真正的物体&#34;&gt;七、刚体旋转：从粒子到真正的「物体」&lt;/h2&gt;
&lt;p&gt;粒子只有位置；刚体多了&lt;strong&gt;姿态&lt;/strong&gt;（orientation），一切都变复杂了。&lt;/p&gt;
&lt;p&gt;3D 姿态更新的标准流程（四元数版）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;存姿态四元数 $q$ 和角速度向量 $\omega$；&lt;/li&gt;
&lt;li&gt;每步积分：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
q(t+h) = q(t) + \frac{h}{2}, \tilde{\omega} , q(t)
$$&lt;/p&gt;
&lt;p&gt;其中 $\tilde{\omega} = (0, \omega_x, \omega_y, \omega_z)$ 是纯四元数形式的角速度（注意乘法顺序，别写反）；
3. &lt;strong&gt;归一化 $q$&lt;/strong&gt;（不做的后果：物体慢慢变大/缩小）；
4. 需要转动向量时，用 $q$ 把向量从局部系旋转到世界系。&lt;/p&gt;
&lt;p&gt;力矩链路：外力作用点相对质心的偏移 $\mathbf{r}$ 产生力矩 $\tau = \mathbf{r} \times \mathbf{F}$，角加速度 $\alpha = I^{-1}\tau$（3D 中 $I^{-1}$ 要用当前姿态把局部系的逆惯量张量变换到世界系），积分得 $\omega$。这就是为什么打在边缘的一拳会让物体边飞边转。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：3D 转动惯量张量一定要在&lt;strong&gt;局部坐标系&lt;/strong&gt;预计算并求逆；每帧变换到世界系用 $I^{-1}_{world} = R I^{-1}_{local} R^T$。直接在世界系里积分惯量是错误的经典写法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八碰撞检测先粗后精&#34;&gt;八、碰撞检测：先粗后精&lt;/h2&gt;
&lt;h3 id=&#34;81-两阶段流水线&#34;&gt;8.1 两阶段流水线&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 LR
    subgraph 粗阶段 Broad Phase
        A1[遍历所有物体对] --&amp;gt; A2[包围球 / AABB 快速排除]
        A2 --&amp;gt; A3[空间划分&amp;lt;br/&amp;gt;网格 / 四叉树 / BVH]
    end
    subgraph 窄阶段 Narrow Phase
        B1[候选物体对] --&amp;gt; B2[精确求交&amp;lt;br/&amp;gt;SAT / 顶点-面 / 边-边]
        B2 --&amp;gt; B3[输出碰撞信息&amp;lt;br/&amp;gt;接触点 / 法线 / 穿透深度]
    end
    A3 --&amp;gt; B1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键思想：$n$ 个物体两两检测是 $O(n^2)$，先用粗阶段把 99% 不可能相碰的对子排除掉，窄阶段只处理剩下的少数。&lt;/p&gt;
&lt;h3 id=&#34;82-常用包围体&#34;&gt;8.2 常用包围体&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;/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;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AABB（轴对齐包围盒）&lt;/td&gt;
					&lt;td&gt;各轴区间重叠判断，简单快速&lt;/td&gt;
					&lt;td&gt;静态场景、粗阶段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;OBB（有向包围盒）&lt;/td&gt;
					&lt;td&gt;贴合物体旋转，测试较贵&lt;/td&gt;
					&lt;td&gt;车辆、细长物体&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;凸多边形/凸包 + SAT&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;分离轴定理&lt;/strong&gt;：存在一条轴使两形体的投影不重叠 ⟺ 不相交&lt;/td&gt;
					&lt;td&gt;2D 精确检测、3D 凸体&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;避坑：SAT 的轴候选是「两个形体所有面的法线」（3D 还要加边叉积），漏掉一组轴就会在特定角度漏检。另外 SAT 只适用于&lt;strong&gt;凸体&lt;/strong&gt;，凹体要先拆成多个凸体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;83-碰撞检测必须输出的三样东西&#34;&gt;8.3 碰撞检测必须输出的三样东西&lt;/h3&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;h2 id=&#34;九碰撞反应冲量法&#34;&gt;九、碰撞反应：冲量法&lt;/h2&gt;
&lt;p&gt;碰撞瞬间持续时间极短、力极大，直接模拟力不现实。游戏物理的标准做法是&lt;strong&gt;冲量法&lt;/strong&gt;：把碰撞看成瞬间改变速度的一次「踢」。&lt;/p&gt;
&lt;h3 id=&#34;91-核心推导思路&#34;&gt;9.1 核心推导思路&lt;/h3&gt;
&lt;p&gt;设两物体碰撞前的&lt;strong&gt;相对速度沿法线分量&lt;/strong&gt;为 $v_r \cdot \mathbf{n}$，碰撞后变为 $-e$ 倍（$e$ 是恢复系数：$e=1$ 完全弹性，$e=0$ 完全非弹性）——这就是牛顿碰撞定律。把「冲量同时改变两物体的线速度和角速度」代入，解出法向冲量大小：&lt;/p&gt;
&lt;p&gt;$$
j = \frac{-(1+e), \mathbf{v}_{rel} \cdot \mathbf{n}}
{\dfrac{1}{m_1} + \dfrac{1}{m_2} +
\dfrac{(\mathbf{r}_1 \times \mathbf{n})^2}{I_1} +
\dfrac{(\mathbf{r}_2 \times \mathbf{n})^2}{I_2}}
$$&lt;/p&gt;
&lt;p&gt;（这是 2D 版本，$\mathbf{r}_1$、$\mathbf{r}_2$ 是接触点相对两质心的偏移；3D 版本把平方项换成矩阵形式 $\mathbf{n} \cdot [(\mathbf{r} \times \mathbf{n}) I^{-1} \times \mathbf{r}]$。）&lt;/p&gt;
&lt;p&gt;然后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线速度：$\mathbf{v}_1 \mathrel{-}= \frac{j}{m_1}\mathbf{n}$，$\mathbf{v}_2 \mathrel{+}= \frac{j}{m_2}\mathbf{n}$&lt;/li&gt;
&lt;li&gt;角速度：$\omega_1 \mathrel{-}= \frac{(\mathbf{r}_1 \times j\mathbf{n})}{I_1}$，$\omega_2 \mathrel{+}= \frac{(\mathbf{r}_2 \times j\mathbf{n})}{I_2}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打偏的一撞让物体旋转起来，就是这两个 $\mathbf{r} \times$ 项在起作用。&lt;/p&gt;
&lt;h3 id=&#34;92-摩擦冲量&#34;&gt;9.2 摩擦冲量&lt;/h3&gt;
&lt;p&gt;法向冲量解决「弹开」，摩擦冲量解决「蹭」。算出切向相对速度后施加切向冲量 $j_t$，并钳制在库仑摩擦锥内：&lt;/p&gt;
&lt;p&gt;$$
|j_t| \le \mu , j
$$&lt;/p&gt;
&lt;p&gt;不做这个钳制，摩擦会反过来给物体加速——又一个「能量爆炸」来源。&lt;/p&gt;
&lt;h3 id=&#34;93-实战避坑清单碰撞反应篇&#34;&gt;9.3 实战避坑清单（碰撞反应篇）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;穿透与抖动&lt;/strong&gt;：数值误差让物体每帧陷进去一点又弹出来。解法：允许一个微小穿透量（slop），只对超出部分做位置修正（Baumgarte 稳定化），修正量再打个 0.2 左右的折扣；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能量爆炸&lt;/strong&gt;：单次冲量解多接触点会互相打架，恢复系数别用满 1，摩擦记得钳制，必要时加阻尼；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代求解&lt;/strong&gt;：多个接触点/约束时，对速度约束循环迭代 4-10 次（顺序冲量法），直到收敛——Box2D 就是这么做的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静止接触&lt;/strong&gt;：物体叠放静止时，冲量趋近于零但不等于零，抖动主要来自这里。slop + 位置修正 + 休眠（速度长期低于阈值就把物体标记为睡眠，跳过模拟）三件套缺一不可；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;衝量只改速度&lt;/strong&gt;：碰完物体还插在里面是正常的，位置交给下一帧的位置修正或积分自然分离，别试图一帧内又改速度又硬掰位置（除非做的是专门的位置投影）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十粒子系统刚体的降维打击&#34;&gt;十、粒子系统：刚体的「降维打击」&lt;/h2&gt;
&lt;p&gt;粒子 = 没有旋转、没有形状（通常渲染成点或公告板）、数量极大的迷你刚体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模拟上只做：合力（重力、风、阻力）→ 半隐式欧拉积分 → 简单碰撞（对地面反弹或直接死亡）；&lt;/li&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;blockquote&gt;
&lt;p&gt;避坑：粒子系统的瓶颈几乎从不在物理，而在&lt;strong&gt;渲染和内存&lt;/strong&gt;。对象池复用粒子、避免每帧 new/delete，CPU 模拟 + GPU 渲染分工明确，是入门就该养成的习惯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一2d-刚体模拟器把全书串起来的最小实战&#34;&gt;十一、2D 刚体模拟器：把全书串起来的最小实战&lt;/h2&gt;
&lt;p&gt;书中后半部分的 2D 刚体模拟示例，是最适合新手的毕业项目。最小可用版本只需要这些模块：&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
    subgraph 数据结构
        D1[RigidBody2D&amp;lt;br/&amp;gt;位置/角度/线速度/角速度&amp;lt;br/&amp;gt;质量/转动惯量]
        D2[Contact&amp;lt;br/&amp;gt;接触点/法线/穿透深度]
    end
    subgraph 每帧流程
        S1[施加重力等外力] --&amp;gt; S2[半隐式欧拉积分]
        S2 --&amp;gt; S3[粗阶段: AABB 排除]
        S3 --&amp;gt; S4[窄阶段: SAT 求接触]
        S4 --&amp;gt; S5[冲量求解 迭代若干次]
        S5 --&amp;gt; S6[位置修正 slop + Baumgarte]
        S6 --&amp;gt; S7[休眠检测]
    end
    D1 --&amp;gt; S1
    S4 --&amp;gt; D2
    D2 --&amp;gt; S5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;动手顺序建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先做单刚体自由落体 + 地面反弹（恢复系数可调），验证积分正确；&lt;/li&gt;
&lt;li&gt;加旋转：给一个方块偏心施加力，看是否边飞边转；&lt;/li&gt;
&lt;li&gt;加碰撞：圆 vs 圆（最简单）→ 凸多边形 SAT；&lt;/li&gt;
&lt;li&gt;加摩擦冲量和迭代求解；&lt;/li&gt;
&lt;li&gt;加位置修正和休眠，一堆方块能稳稳叠起来——恭喜，你写出了一个迷你 Box2D。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十二典型问题-faq&#34;&gt;十二、典型问题 FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：物体速度越来越快最后飞出世界（数值发散）？&lt;/strong&gt;
检查三件事：是否用了显式欧拉（换半隐式）；步长是否过大（弹簧系统等刚度大的力要求更小的步）；阻力/弹簧是否缺上限或阻尼。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：子弹穿墙？&lt;/strong&gt;
隧道效应。高速物体用扫掠检测（上一帧位置到这一帧位置打一条射线/扫掠体），或对其使用更小的子步长（CCD）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：箱子叠在一起不停抖动、慢慢沉进地里？&lt;/strong&gt;
缺 slop 容差和位置修正折扣；恢复系数设成了接近 1（静止接触应该用 0 或极小）；缺少休眠机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：联机/回放时物理结果不一致？&lt;/strong&gt;
使用了可变时间步或浮点运算顺序不一致。固定时间步 + 固定迭代次数 + 固定遍历顺序是确定性模拟的前提；跨平台完全一致还要锁定浮点模式或用定点数（如《王者荣耀》等帧同步游戏的做法）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：斜坡上的箱子自己往下滑 / 不会滑？&lt;/strong&gt;
检查摩擦实现：切向冲量有没有做库仑钳制 $\mu j$；法线是否归一化；静摩擦与动摩擦有没有分开处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q6：四元数积分后物体悄悄变形/缩放？&lt;/strong&gt;
忘记每步归一化四元数。这是四元数模拟里出现率最高的 bug。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q7：2D 里物体往天上「掉」？&lt;/strong&gt;
屏幕 y 轴向下而公式按 y 轴向上写的，重力符号反了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十三避坑总清单速查表&#34;&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;单位混用（米/像素）&lt;/td&gt;
					&lt;td&gt;一切数值都「差不多但不对」&lt;/td&gt;
					&lt;td&gt;开工前定死单位制，重力常数跟着改&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;y 轴朝向&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;/tr&gt;
			&lt;tr&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;扫掠检测 / CCD / 子步长&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;法线没归一化&lt;/td&gt;
					&lt;td&gt;碰撞反应大小诡异&lt;/td&gt;
					&lt;td&gt;所有法线使用前 normalize&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;/tr&gt;
			&lt;tr&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;slop + Baumgarte 折扣修正&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;/tr&gt;
			&lt;tr&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;h2 id=&#34;十四主流游戏框架与物理引擎&#34;&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;strong&gt;Unity&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PhysX（3D）、Box2D（2D）&lt;/td&gt;
					&lt;td&gt;Rigidbody / Rigidbody2D，FixedUpdate 即固定时间步&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Unreal Engine&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Chaos（新版）、PhysX（旧版）&lt;/td&gt;
					&lt;td&gt;Chaos 支持破坏、布料等大型模拟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Godot&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;自研 Godot Physics，可换 Jolt&lt;/td&gt;
					&lt;td&gt;开源，2D/3D 分离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Box2D&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;2D 物理库（C++）&lt;/td&gt;
					&lt;td&gt;本书思想的工业化实现，愤怒的小鸟、无数 2D 游戏在用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Bullet&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库&lt;/td&gt;
					&lt;td&gt;开源老牌，影视与游戏双用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Jolt&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库&lt;/td&gt;
					&lt;td&gt;《地平线》系列使用，近年性能口碑极好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;PhysX&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;3D 物理库（NVIDIA）&lt;/td&gt;
					&lt;td&gt;Unity/UE 底层，支持 GPU 加速&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Cocos Creator&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内置 2D（Box2D/builtin）、3D 可选 PhysX/cannon.js&lt;/td&gt;
					&lt;td&gt;国内小游戏主流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;LayaAir&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;集成 Bullet（编译为 wasm）&lt;/td&gt;
					&lt;td&gt;国内 3D H5 主流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Rapier&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Rust 编写，可编译 wasm&lt;/td&gt;
					&lt;td&gt;Web/独立游戏新势力，确定性模拟支持好&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;MuJoCo / PyBullet&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;选型直觉：2D 游戏默认 Box2D 系；3D 商用项目跟着引擎走（Unity 用 PhysX、UE 用 Chaos）；网页端想要确定性（帧同步）看 Rapier；做机器人/RL 仿真看 MuJoCo。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十五学习路径建议&#34;&gt;十五、学习路径建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一周&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;第三周&lt;/strong&gt;：实现 2D 刚体（加旋转和转动惯量），偏心受力看旋转；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第四周&lt;/strong&gt;：SAT 碰撞检测 + 冲量法碰撞反应 + 摩擦，让方块能撞、能停、能叠；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;之后&lt;/strong&gt;：挑一个载具（汽车最容易出成就感）做手感调优；有 3D 需求再啃四元数和惯量张量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贯穿始终&lt;/strong&gt;：每实现一个特性，先想「这一步在哪个引擎里对应什么参数」，原理和工具互相印证。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;
&lt;p&gt;《游戏开发物理学》最大的价值不是那些公式，而是一个思维框架：&lt;strong&gt;任何炫酷的物理效果，拆到最后都是「合力 → 积分 → 碰撞 → 冲量」这条流水线的变体&lt;/strong&gt;。飞机和汽车的差别只在于往「合力」里塞了什么力；刚体和粒子的差别只在于有没有旋转那一半。&lt;/p&gt;
&lt;p&gt;先把这条流水线在小项目里跑通，再去用引擎——你会发现所有参数突然都认识了。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
