<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>拾星集 · Nebula Notes</title>
    <link>https://blog.yeluoxiaoxiao.top</link>
    <atom:link href="https://blog.yeluoxiaoxiao.top/rss.xml" rel="self" type="application/rss+xml" />
    <description>拾星集 · Nebula Notes —— 一个漂浮在星云里的个人博客，记录 WebGL、创意编程、前端工程与星光。</description>
    <language>zh-cn</language>
    <lastBuildDate>Thu, 09 Jul 2026 20:42:35 GMT</lastBuildDate>
    <item>
      <title>把静态博客变成订阅制产品：Stripe + Cloudflare Worker 完整实践</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/monetize-static-blog-stripe</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/monetize-static-blog-stripe</guid>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <description>不租服务器、不写后端框架，用一个 Cloudflare Worker + D1 + Stripe Checkout，把纯静态博客升级成带 Google 登录和会员订阅的变现站点。这篇付费长文给出全部架构决策、踩坑与可复制的代码骨架。</description>
    </item>
    <item>
      <title>海外独立站冷启动 SEO 手册：从 0 到被 Google 与 AI 搜索收录</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/overseas-seo-playbook</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/overseas-seo-playbook</guid>
      <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
      <description>新域名上线后的前 30 天决定了它未来一年的搜索命运。这篇付费文章给出一份可照抄的冷启动清单：技术 SEO 地基、Search Console 正确姿势、结构化数据、以及 2026 年绕不开的 AI 爬虫适配。</description>
    </item>
    <item>
      <title>用一段顶点着色器，捏出一片会旋转的星系</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/galaxy-vertex-shader</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/galaxy-vertex-shader</guid>
      <pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate>
      <description>不用任何贴图和模型，只靠 BufferGeometry 和 60 行 GLSL，在浏览器里点亮一万颗粒子组成的螺旋星系。这篇文章记录整个推导过程：从极坐标分布到差速旋转。</description>
      <content:encoded><![CDATA[<p>这个博客首页的那片星云，最初只是一次失眠的产物。我盯着 Bruno Simon 课程里的 galaxy generator 看了很久，突然意识到：<strong>星系的美感来自数学，而不是资源量</strong>。不需要贴图，不需要模型，一个 <code>Points</code> 加一段顶点着色器就够了。</p>
<h2>先把粒子撒成螺旋</h2>
<p>星系的骨架是「分支 + 旋转偏移」。把每个粒子分配到某一条旋臂上，再让半径越大的粒子偏转越多，螺旋就出现了：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> BRANCHES</span><span style="color:#89DDFF"> =</span><span style="color:#FF9E64"> 4</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> SPIN</span><span style="color:#89DDFF"> =</span><span style="color:#FF9E64"> 1.15</span></span>
<span class="line"></span>
<span class="line"><span style="color:#BB9AF7">for</span><span style="color:#9ABDF5"> (</span><span style="color:#9D7CD8;font-style:italic">let</span><span style="color:#BB9AF7"> i</span><span style="color:#89DDFF"> =</span><span style="color:#FF9E64"> 0</span><span style="color:#89DDFF">;</span><span style="color:#C0CAF5"> i</span><span style="color:#BB9AF7"> &#x3C;</span><span style="color:#C0CAF5"> count</span><span style="color:#89DDFF">;</span><span style="color:#C0CAF5"> i</span><span style="color:#89DDFF">++</span><span style="color:#9ABDF5">)</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#51597D;font-style:italic">  // 幂次让粒子向核心聚集，比均匀分布自然得多</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">  const</span><span style="color:#BB9AF7"> r</span><span style="color:#89DDFF"> =</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">pow</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">random</span><span style="color:#9ABDF5">()</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 0.8</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#FF9E64"> RADIUS</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">  const</span><span style="color:#BB9AF7"> branchAngle</span><span style="color:#89DDFF"> =</span><span style="color:#9ABDF5"> ((</span><span style="color:#C0CAF5">i</span><span style="color:#89DDFF"> %</span><span style="color:#FF9E64"> BRANCHES</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">/</span><span style="color:#FF9E64"> BRANCHES</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#FF9E64">PI</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 2</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">  const</span><span style="color:#BB9AF7"> spinAngle</span><span style="color:#89DDFF"> =</span><span style="color:#C0CAF5"> r</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> SPIN</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C0CAF5">  positions</span><span style="color:#9ABDF5">[</span><span style="color:#7DCFFF">i</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 3</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 0</span><span style="color:#9ABDF5">] </span><span style="color:#89DDFF">=</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">cos</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">branchAngle</span><span style="color:#89DDFF"> +</span><span style="color:#C0CAF5"> spinAngle</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#C0CAF5"> r</span></span>
<span class="line"><span style="color:#C0CAF5">  positions</span><span style="color:#9ABDF5">[</span><span style="color:#7DCFFF">i</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 3</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 1</span><span style="color:#9ABDF5">] </span><span style="color:#89DDFF">=</span><span style="color:#FF9E64"> 0</span></span>
<span class="line"><span style="color:#C0CAF5">  positions</span><span style="color:#9ABDF5">[</span><span style="color:#7DCFFF">i</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 3</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 2</span><span style="color:#9ABDF5">] </span><span style="color:#89DDFF">=</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">sin</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">branchAngle</span><span style="color:#89DDFF"> +</span><span style="color:#C0CAF5"> spinAngle</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#C0CAF5"> r</span></span>
<span class="line"><span style="color:#9ABDF5">}</span></span></code></pre>
<p>这时候的星系整齐得像个风扇。真实的星云是「毛茸茸」的 —— 给每个轴叠加一个幂律随机偏移，幂次越高，粒子越贴近旋臂：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#7AA2F7"> rand</span><span style="color:#89DDFF"> =</span><span style="color:#9ABDF5"> ()</span><span style="color:#BB9AF7"> =></span></span>
<span class="line"><span style="color:#C0CAF5">  Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">pow</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">random</span><span style="color:#9ABDF5">()</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 2.9</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF"> *</span><span style="color:#51597D;font-style:italic">          // 幂律：大偏移很罕见</span></span>
<span class="line"><span style="color:#9ABDF5">  (</span><span style="color:#C0CAF5">Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">random</span><span style="color:#9ABDF5">()</span><span style="color:#BB9AF7"> &#x3C;</span><span style="color:#FF9E64"> 0.5</span><span style="color:#BB9AF7"> ?</span><span style="color:#FF9E64"> 1</span><span style="color:#BB9AF7"> :</span><span style="color:#89DDFF"> -</span><span style="color:#FF9E64">1</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF"> *</span><span style="color:#51597D;font-style:italic">        // 正负对称</span></span>
<span class="line"><span style="color:#FF9E64">  0.44</span><span style="color:#89DDFF"> *</span><span style="color:#9ABDF5"> (</span><span style="color:#C0CAF5">r</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 0.6</span><span style="color:#9ABDF5">)</span><span style="color:#51597D;font-style:italic">                        // 半径越大越松散</span></span></code></pre>
<h2>颜色是三段插值</h2>
<p>从核心到边缘，我用了三段颜色：核心是恒星密集的暖金，中段过渡到品红，外围随机混入极光青或紫。诀窍是<strong>在 JS 里算好颜色塞进 attribute</strong>，着色器里只做透明度：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> inside</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">Color</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">#ffd27d</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> mid</span><span style="color:#89DDFF">    =</span><span style="color:#89DDFF"> new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">Color</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">#f973c0</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> outer</span><span style="color:#89DDFF">  =</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">random</span><span style="color:#9ABDF5">()</span><span style="color:#BB9AF7"> &#x3C;</span><span style="color:#FF9E64"> 0.5</span></span>
<span class="line"><span style="color:#BB9AF7">  ?</span><span style="color:#89DDFF"> new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">Color</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">#6ee7f9</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#BB9AF7">  :</span><span style="color:#89DDFF"> new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">Color</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">#a88bfa</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> t</span><span style="color:#89DDFF"> =</span><span style="color:#C0CAF5"> r</span><span style="color:#89DDFF"> /</span><span style="color:#FF9E64"> RADIUS</span></span>
<span class="line"><span style="color:#C0CAF5">tmp</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">copy</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">inside</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#C0CAF5">t</span><span style="color:#BB9AF7"> &#x3C;</span><span style="color:#FF9E64"> 0.45</span></span>
<span class="line"><span style="color:#BB9AF7">  ?</span><span style="color:#C0CAF5"> tmp</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">lerp</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">mid</span><span style="color:#89DDFF">,</span><span style="color:#C0CAF5"> t</span><span style="color:#89DDFF"> /</span><span style="color:#FF9E64"> 0.45</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#BB9AF7">  :</span><span style="color:#C0CAF5"> tmp</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">copy</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">mid</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">lerp</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">outer</span><span style="color:#89DDFF">,</span><span style="color:#9ABDF5"> (</span><span style="color:#C0CAF5">t</span><span style="color:#89DDFF"> -</span><span style="color:#FF9E64"> 0.45</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF"> /</span><span style="color:#FF9E64"> 0.55</span><span style="color:#9ABDF5">)</span></span></code></pre>
<h2>差速旋转：星系活起来的关键</h2>
<p>如果整个星系用同一个角速度旋转，看起来就是一张贴图在转盘上打转。真实星系是<strong>差速旋转</strong>的 —— 越靠近核心转得越快。把旋转搬进顶点着色器，每颗粒子按自己的半径算角度：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">uniform</span><span style="color:#BB9AF7"> float</span><span style="color:#A9B1D6"> uTime</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#BB9AF7">void</span><span style="color:#7AA2F7"> main</span><span style="color:#9ABDF5">()</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#9ABDF5">  vec3 p </span><span style="color:#89DDFF">=</span><span style="color:#9ABDF5"> position</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#BB9AF7">  float</span><span style="color:#9ABDF5"> dist  </span><span style="color:#89DDFF">=</span><span style="color:#7AA2F7"> length</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">p</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">xz</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#BB9AF7">  float</span><span style="color:#9ABDF5"> angle </span><span style="color:#89DDFF">=</span><span style="color:#7AA2F7"> atan</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">p</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">x</span><span style="color:#89DDFF">,</span><span style="color:#C0CAF5"> p</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">z</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#89DDFF">              +</span><span style="color:#9ABDF5"> uTime </span><span style="color:#89DDFF">*</span><span style="color:#FF9E64"> 0.24</span><span style="color:#89DDFF"> /</span><span style="color:#9ABDF5"> (dist </span><span style="color:#89DDFF">*</span><span style="color:#FF9E64"> 0.55</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 0.35</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span><span style="color:#51597D;font-style:italic"> // 核心快、边缘慢</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C0CAF5">  p</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">x</span><span style="color:#89DDFF"> =</span><span style="color:#7AA2F7"> sin</span><span style="color:#9ABDF5">(</span><span style="color:#7AA2F7">angle</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#9ABDF5"> dist</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#C0CAF5">  p</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">z</span><span style="color:#89DDFF"> =</span><span style="color:#7AA2F7"> cos</span><span style="color:#9ABDF5">(</span><span style="color:#7AA2F7">angle</span><span style="color:#9ABDF5">) </span><span style="color:#89DDFF">*</span><span style="color:#9ABDF5"> dist</span><span style="color:#89DDFF">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9ABDF5">  vec4 mv </span><span style="color:#89DDFF">=</span><span style="color:#9ABDF5"> modelViewMatrix </span><span style="color:#89DDFF">*</span><span style="color:#7AA2F7"> vec4</span><span style="color:#9ABDF5">(</span><span style="color:#7AA2F7">p</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 1.0</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#9ABDF5">  gl_Position  </span><span style="color:#89DDFF">=</span><span style="color:#9ABDF5"> projectionMatrix </span><span style="color:#89DDFF">*</span><span style="color:#9ABDF5"> mv</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#9ABDF5">  gl_PointSize </span><span style="color:#89DDFF">=</span><span style="color:#9ABDF5"> uSize </span><span style="color:#89DDFF">*</span><span style="color:#9ABDF5"> aScale </span><span style="color:#89DDFF">*</span><span style="color:#9ABDF5"> (</span><span style="color:#FF9E64">42.0</span><span style="color:#89DDFF"> /</span><span style="color:#89DDFF"> -</span><span style="color:#C0CAF5">mv</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">z</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span><span style="color:#51597D;font-style:italic">   // 近大远小</span></span>
<span class="line"><span style="color:#9ABDF5">}</span></span></code></pre>
<p>片元着色器负责把方形点精灵变成柔光球，<code>pow</code> 的指数决定光晕的「硬度」：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#BB9AF7">void</span><span style="color:#7AA2F7"> main</span><span style="color:#9ABDF5">()</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#BB9AF7">  float</span><span style="color:#9ABDF5"> d </span><span style="color:#89DDFF">=</span><span style="color:#7AA2F7"> distance</span><span style="color:#9ABDF5">(</span><span style="color:#7AA2F7">gl_PointCoord</span><span style="color:#89DDFF">,</span><span style="color:#7AA2F7"> vec2</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">0.5</span><span style="color:#9ABDF5">))</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#BB9AF7">  float</span><span style="color:#9ABDF5"> strength </span><span style="color:#89DDFF">=</span><span style="color:#7AA2F7"> pow</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">1.0</span><span style="color:#89DDFF"> -</span><span style="color:#7AA2F7"> smoothstep</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">0.0</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 0.5</span><span style="color:#89DDFF">,</span><span style="color:#7AA2F7"> d</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 3.0</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#9ABDF5">  gl_FragColor </span><span style="color:#89DDFF">=</span><span style="color:#7AA2F7"> vec4</span><span style="color:#9ABDF5">(</span><span style="color:#7AA2F7">vColor</span><span style="color:#89DDFF">,</span><span style="color:#7AA2F7"> strength</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#9ABDF5">}</span></span></code></pre>
<p>最后开加法混合，粒子重叠处自动过曝成辉光，核心不点灯自己就会发光：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#89DDFF">new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">ShaderMaterial</span><span style="color:#9ABDF5">({</span></span>
<span class="line"><span style="color:#73DACA">  transparent</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> true</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  depthWrite</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> false</span><span style="color:#89DDFF">,</span><span style="color:#51597D;font-style:italic">               // 关键：否则粒子会互相裁剪</span></span>
<span class="line"><span style="color:#73DACA">  blending</span><span style="color:#89DDFF">:</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">AdditiveBlending</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  vertexColors</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> true</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<h2>踩过的坑</h2>
<blockquote>
<p><code>depthWrite: false</code> 忘了关，粒子边缘出现黑色方块，查了一个小时。加法混合 + 深度写入是互斥的，切记。</p>
</blockquote>
<ul>
<li><code>gl_PointSize</code> 要乘 <code>devicePixelRatio</code>，否则高分屏上粒子小得像灰尘</li>
<li>粒子数量不是越多越好：一万颗以后提升有限，移动端直接砍半</li>
<li>旋转速度放在 uniform 里而不是每帧改 rotation，GPU 转比 CPU 转便宜得多</li>
</ul>
<p>一万颗粒子，一次 draw call，六十行着色器 —— 这大概就是我爱上 WebGL 的原因：<strong>用最少的资源，换最大的浪漫</strong>。</p>]]></content:encoded>
    </item>
    <item>
      <title>滚动叙事的手感工程：Lenis 与 ScrollTrigger 的正确打开方式</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/scroll-storytelling</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/scroll-storytelling</guid>
      <pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
      <description>为什么有的网站滚起来像丝绸，有的像生锈的齿轮？拆解平滑滚动背后的工程细节：单一 rAF 时钟、数字型 scrub、固定舞台模式，以及那些没人告诉你的调参玄学。</description>
      <content:encoded><![CDATA[<p>判断一个交互型网站的完成度，我有个私人标准：<strong>闭上眼睛滚三下</strong>。手感对了，其他都不会太差。这篇文章总结我做滚动叙事页面时沉淀下来的工程套路。</p>
<h2>为什么原生滚动「不够用」</h2>
<p>原生滚动是阶梯式的：滚轮每格跳 100px 左右，动画跟着一顿一顿。Lenis 做的事情很简单 —— 拦截输入，把目标位置用缓动函数「追」出来：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#7DCFFF">import</span><span style="color:#0DB9D7"> Lenis</span><span style="color:#7DCFFF"> from</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">lenis</span><span style="color:#89DDFF">'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> lenis</span><span style="color:#89DDFF"> =</span><span style="color:#89DDFF"> new</span><span style="color:#7AA2F7"> Lenis</span><span style="color:#9ABDF5">({</span></span>
<span class="line"><span style="color:#73DACA">  duration</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 1.15</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#7AA2F7">  easing</span><span style="color:#89DDFF">:</span><span style="color:#9ABDF5"> (</span><span style="color:#E0AF68">t</span><span style="color:#9ABDF5">)</span><span style="color:#BB9AF7"> =></span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">min</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">1</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 1.001</span><span style="color:#89DDFF"> -</span><span style="color:#C0CAF5"> Math</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">pow</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">2</span><span style="color:#89DDFF">,</span><span style="color:#89DDFF"> -</span><span style="color:#FF9E64">10</span><span style="color:#89DDFF"> *</span><span style="color:#C0CAF5"> t</span><span style="color:#9ABDF5">))</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  smoothWheel</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> true</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<p>但装上 Lenis 只是开始。真正的坑在于：<strong>页面里现在有几个时钟？</strong></p>
<h2>一个页面只允许一个 rAF 循环</h2>
<p>Lenis 要 <code>raf</code>，GSAP 有自己的 ticker，Three.js 还有渲染循环。三个 <code>requestAnimationFrame</code> 各跑各的，帧与帧之间互相踩踏，这就是「莫名卡一下」的元凶。解法是全部挂到 GSAP 的单一 ticker 上：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#C0CAF5">lenis</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">on</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">scroll</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span><span style="color:#C0CAF5"> ScrollTrigger</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">update</span><span style="color:#9ABDF5">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C0CAF5">gsap</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">ticker</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">add</span><span style="color:#9ABDF5">((</span><span style="color:#E0AF68">time</span><span style="color:#9ABDF5">)</span><span style="color:#BB9AF7"> =></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">  lenis</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">raf</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">time</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 1000</span><span style="color:#9ABDF5">) </span><span style="color:#51597D;font-style:italic">// GSAP 用秒，Lenis 用毫秒</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span>
<span class="line"><span style="color:#C0CAF5">gsap</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">ticker</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">lagSmoothing</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">0</span><span style="color:#9ABDF5">)</span></span></code></pre>
<p>从此滚动、补间、渲染共享一个心跳。</p>
<h2>scrub 永远写数字</h2>
<p><code>scrub: true</code> 是 1:1 硬绑定，快速甩动滚轮时动画会「抽搐」。写一个数字，它就变成了追赶延迟（秒）：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#C0CAF5">gsap</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">to</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">camera</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">position</span><span style="color:#89DDFF">,</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#73DACA">  z</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 4</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  ease</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">none</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  scrollTrigger</span><span style="color:#89DDFF">:</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#73DACA">    trigger</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">#journey</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">    start</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">top top</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">    end</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">bottom bottom</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">    scrub</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 0.8</span><span style="color:#89DDFF">,</span><span style="color:#51597D;font-style:italic"> // 0.8 秒的“惯性”，抖动全部被抹平</span></span>
<span class="line"><span style="color:#9ABDF5">  }</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<p>0.6 ~ 1.0 之间的值手感最好。再大就开始「晕船」了。</p>
<h2>固定舞台，而不是 pin</h2>
<p>滚动驱动 3D 场景时，教程都爱用 <code>pin: true</code>。但 pin 会克隆 DOM、改写布局，跟 Lenis、跟别的动画库打架是常事。更稳的做法是<strong>舞台固定 + 幽灵滚动条</strong>：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#E0AF68">.</span><span style="color:#9ECE6A">stage</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#7AA2F7">  position</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> fixed</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#7AA2F7">  inset</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 0</span><span style="color:#89DDFF">;</span></span>
<span class="line"><span style="color:#7AA2F7">  z-index</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 0</span><span style="color:#89DDFF">;</span><span style="color:#51597D;font-style:italic"> /* 3D 永远铺在底下 */</span></span>
<span class="line"><span style="color:#9ABDF5">}</span></span>
<span class="line"><span style="color:#E0AF68">.</span><span style="color:#9ECE6A">scroll-driver</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#7AA2F7">  height</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 500</span><span style="color:#F7768E">vh</span><span style="color:#89DDFF">;</span><span style="color:#51597D;font-style:italic"> /* 一个透明的高个子，只负责产生滚动距离 */</span></span>
<span class="line"><span style="color:#9ABDF5">}</span></span></code></pre>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#C0CAF5">ScrollTrigger</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">create</span><span style="color:#9ABDF5">({</span></span>
<span class="line"><span style="color:#73DACA">  trigger</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">.scroll-driver</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  start</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">top top</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  end</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">bottom bottom</span><span style="color:#89DDFF">'</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  scrub</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 0.8</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#7AA2F7">  onUpdate</span><span style="color:#89DDFF">:</span><span style="color:#9ABDF5"> (</span><span style="color:#E0AF68">self</span><span style="color:#9ABDF5">)</span><span style="color:#BB9AF7"> =></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">    world</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">progress</span><span style="color:#89DDFF"> =</span><span style="color:#C0CAF5"> self</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">progress</span><span style="color:#51597D;font-style:italic"> // 写进共享状态，useFrame 里读</span></span>
<span class="line"><span style="color:#9ABDF5">  }</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<p>DOM 层滚 DOM 的，3D 层读进度做自己的事，两边解耦，谁也不干扰谁。</p>
<h2>数值写进 ref，不写进 state</h2>
<p>滚动进度每秒变化上百次。放进 React state，等于每秒上百次重渲染：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#51597D;font-style:italic">// ❌ 整棵组件树给你表演原地爆炸</span></span>
<span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#89DDFF"> [</span><span style="color:#BB9AF7">progress</span><span style="color:#89DDFF">,</span><span style="color:#BB9AF7"> setProgress</span><span style="color:#89DDFF">]</span><span style="color:#89DDFF"> =</span><span style="color:#7AA2F7"> useState</span><span style="color:#9ABDF5">(</span><span style="color:#FF9E64">0</span><span style="color:#9ABDF5">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#51597D;font-style:italic">// ✅ 可变单例 + useFrame 直读，零重渲染</span></span>
<span class="line"><span style="color:#7DCFFF">export</span><span style="color:#9D7CD8;font-style:italic"> const</span><span style="color:#BB9AF7"> world</span><span style="color:#89DDFF"> =</span><span style="color:#9ABDF5"> {</span><span style="color:#73DACA"> progress</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> 0</span><span style="color:#9ABDF5"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7AA2F7">useFrame</span><span style="color:#9ABDF5">(()</span><span style="color:#BB9AF7"> =></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">  camera</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">position</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">z</span><span style="color:#89DDFF"> =</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">MathUtils</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">damp</span><span style="color:#9ABDF5">(</span></span>
<span class="line"><span style="color:#C0CAF5">    camera</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">position</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">z</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#FF9E64">    6.5</span><span style="color:#89DDFF"> -</span><span style="color:#C0CAF5"> world</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">progress</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 2.6</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#FF9E64">    2.2</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#C0CAF5">    delta</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">  )</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<h2>别忘了那 5% 的用户</h2>
<p>平滑滚动是甜点，不是主食。<code>prefers-reduced-motion</code> 开启时，Lenis 干脆不要初始化：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> reduced</span><span style="color:#89DDFF"> =</span><span style="color:#7AA2F7"> matchMedia</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">(prefers-reduced-motion: reduce)</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">matches</span></span>
<span class="line"><span style="color:#BB9AF7">if</span><span style="color:#9ABDF5"> (</span><span style="color:#BB9AF7">!</span><span style="color:#C0CAF5">reduced</span><span style="color:#9ABDF5">)</span><span style="color:#7AA2F7"> initLenis</span><span style="color:#9ABDF5">()</span></span></code></pre>
<p>滚动叙事的尽头是克制 —— 用户感觉不到你调了三天参数，只觉得「这网站滚起来真舒服」，那你就成功了。</p>]]></content:encoded>
    </item>
    <item>
      <title>R3F 性能优化清单：把一个 24fps 的场景救回 120fps</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/r3f-performance-checklist</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/r3f-performance-checklist</guid>
      <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
      <description>一次真实的性能急救记录。六个步骤，把一个粒子 + 后期 + 十几个模型的 React Three Fiber 场景，从掉帧掉到肉眼可见，优化到高刷屏满帧运行。</description>
      <content:encoded><![CDATA[<p>上个月接手了一个营销页：R3F 写的，创意很棒，但在我的 M1 上只有 24fps，风扇转得像要起飞。最终它跑到了 120fps。这是完整的急救清单，按性价比排序。</p>
<h2>1. 先测量，再动手</h2>
<p>猜是没用的。装上 <code>r3f-perf</code>，让数据说话：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#7DCFFF">import</span><span style="color:#9ABDF5"> { </span><span style="color:#0DB9D7">Perf</span><span style="color:#9ABDF5"> }</span><span style="color:#7DCFFF"> from</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">r3f-perf</span><span style="color:#89DDFF">'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#BA3C97">&#x3C;</span><span style="color:#DE5971">Canvas</span><span style="color:#BA3C97">></span></span>
<span class="line"><span style="color:#BA3C97">  &#x3C;</span><span style="color:#DE5971">Perf </span><span style="color:#BB9AF7">position</span><span style="color:#89DDFF">=</span><span style="color:#89DDFF">"</span><span style="color:#9ECE6A">top-left</span><span style="color:#89DDFF">"</span><span style="color:#BA3C97"> /></span></span>
<span class="line"><span style="color:#BA3C97">  &#x3C;</span><span style="color:#DE5971">Scene </span><span style="color:#BA3C97">/></span></span>
<span class="line"><span style="color:#BA3C97">&#x3C;/</span><span style="color:#DE5971">Canvas</span><span style="color:#BA3C97">></span></span></code></pre>
<p>看三个数字：<strong>draw calls、三角形数量、GPU 耗时</strong>。那个页面的初始状态：draw calls 217，三角形 340 万。目标：draw calls 50 以内，三角形 50 万以内。</p>
<h2>2. setState 逐出 useFrame</h2>
<p>搜一遍代码库，凡是 <code>useFrame</code> 里出现 <code>setXxx</code> 的，全部改成 ref 直改：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#51597D;font-style:italic">// ❌ 每帧一次 setState = 每帧一次协调，React 表示很累</span></span>
<span class="line"><span style="color:#7AA2F7">useFrame</span><span style="color:#9ABDF5">(()</span><span style="color:#BB9AF7"> =></span><span style="color:#7AA2F7"> setRotation</span><span style="color:#9ABDF5">((</span><span style="color:#E0AF68">r</span><span style="color:#9ABDF5">)</span><span style="color:#BB9AF7"> =></span><span style="color:#C0CAF5"> r</span><span style="color:#89DDFF"> +</span><span style="color:#FF9E64"> 0.01</span><span style="color:#9ABDF5">))</span></span>
<span class="line"></span>
<span class="line"><span style="color:#51597D;font-style:italic">// ✅ 直接改 Three 对象，React 完全不知情</span></span>
<span class="line"><span style="color:#7AA2F7">useFrame</span><span style="color:#9ABDF5">((</span><span style="color:#E0AF68">_</span><span style="color:#89DDFF">,</span><span style="color:#E0AF68"> delta</span><span style="color:#9ABDF5">)</span><span style="color:#BB9AF7"> =></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">  meshRef</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">current</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">rotation</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">y</span><span style="color:#89DDFF"> +=</span><span style="color:#C0CAF5"> delta</span><span style="color:#89DDFF"> *</span><span style="color:#FF9E64"> 0.6</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span></code></pre>
<p>这一步就拿回了 20fps。React 的协调器很快，但每秒 60 次全树 diff 谁也扛不住。</p>
<h2>3. 重复物体全部实例化</h2>
<p>场景里 90 个小行星是 90 次 draw call。<code>InstancedMesh</code> 合并成 1 次：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> dummy</span><span style="color:#89DDFF"> =</span><span style="color:#7AA2F7"> useMemo</span><span style="color:#9ABDF5">(()</span><span style="color:#BB9AF7"> =></span><span style="color:#89DDFF"> new</span><span style="color:#0DB9D7"> THREE</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">Object3D</span><span style="color:#9ABDF5">()</span><span style="color:#89DDFF">,</span><span style="color:#9ABDF5"> [])</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7AA2F7">useFrame</span><span style="color:#9ABDF5">(()</span><span style="color:#BB9AF7"> =></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">  asteroids</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">forEach</span><span style="color:#9ABDF5">((</span><span style="color:#E0AF68">a</span><span style="color:#89DDFF">,</span><span style="color:#E0AF68"> i</span><span style="color:#9ABDF5">) </span><span style="color:#BB9AF7">=></span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#C0CAF5">    dummy</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">position</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">copy</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">a</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">position</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#C0CAF5">    dummy</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">rotation</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">y</span><span style="color:#89DDFF"> =</span><span style="color:#C0CAF5"> a</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">spin</span></span>
<span class="line"><span style="color:#C0CAF5">    dummy</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">updateMatrix</span><span style="color:#9ABDF5">()</span></span>
<span class="line"><span style="color:#C0CAF5">    meshRef</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">current</span><span style="color:#89DDFF">.</span><span style="color:#7AA2F7">setMatrixAt</span><span style="color:#9ABDF5">(</span><span style="color:#C0CAF5">i</span><span style="color:#89DDFF">,</span><span style="color:#C0CAF5"> dummy</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">matrix</span><span style="color:#9ABDF5">)</span></span>
<span class="line"><span style="color:#9ABDF5">  })</span></span>
<span class="line"><span style="color:#C0CAF5">  meshRef</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">current</span><span style="color:#89DDFF">.</span><span style="color:#C0CAF5">instanceMatrix</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">needsUpdate</span><span style="color:#89DDFF"> =</span><span style="color:#FF9E64"> true</span></span>
<span class="line"><span style="color:#9ABDF5">})</span></span>
<span class="line"></span>
<span class="line"><span style="color:#BA3C97">&#x3C;</span><span style="color:#DE5971">instancedMesh </span><span style="color:#BB9AF7">ref</span><span style="color:#89DDFF">=</span><span style="color:#7DCFFF">{</span><span style="color:#C0CAF5">meshRef</span><span style="color:#7DCFFF">}</span><span style="color:#BB9AF7"> args</span><span style="color:#89DDFF">=</span><span style="color:#7DCFFF">{</span><span style="color:#9ABDF5">[</span><span style="color:#FF9E64">undefined</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> undefined</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 90</span><span style="color:#9ABDF5">]</span><span style="color:#7DCFFF">}</span><span style="color:#BA3C97">></span></span>
<span class="line"><span style="color:#BA3C97">  &#x3C;</span><span style="color:#DE5971">icosahedronGeometry </span><span style="color:#BB9AF7">args</span><span style="color:#89DDFF">=</span><span style="color:#7DCFFF">{</span><span style="color:#9ABDF5">[</span><span style="color:#FF9E64">0.4</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 0</span><span style="color:#9ABDF5">]</span><span style="color:#7DCFFF">}</span><span style="color:#BA3C97"> /></span></span>
<span class="line"><span style="color:#BA3C97">  &#x3C;</span><span style="color:#DE5971">meshStandardMaterial </span><span style="color:#BA3C97">/></span></span>
<span class="line"><span style="color:#BA3C97">&#x3C;/</span><span style="color:#DE5971">instancedMesh</span><span style="color:#BA3C97">></span></span></code></pre>
<p>217 个 draw call 降到 41 个。</p>
<h2>4. dpr 封顶，抗锯齿关掉</h2>
<p>4K 屏 + <code>devicePixelRatio = 3</code> 意味着每帧要画 2400 万像素。封顶到 2，视觉上几乎无损：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#BA3C97">&#x3C;</span><span style="color:#DE5971">Canvas</span></span>
<span class="line"><span style="color:#BB9AF7">  dpr</span><span style="color:#89DDFF">=</span><span style="color:#7DCFFF">{</span><span style="color:#9ABDF5">[</span><span style="color:#FF9E64">1</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 2</span><span style="color:#9ABDF5">]</span><span style="color:#7DCFFF">}</span></span>
<span class="line"><span style="color:#BB9AF7">  gl</span><span style="color:#89DDFF">=</span><span style="color:#7DCFFF">{</span><span style="color:#9ABDF5">{</span><span style="color:#73DACA"> antialias</span><span style="color:#89DDFF">:</span><span style="color:#FF9E64"> false</span><span style="color:#89DDFF">,</span><span style="color:#73DACA"> powerPreference</span><span style="color:#89DDFF">:</span><span style="color:#89DDFF"> '</span><span style="color:#9ECE6A">high-performance</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5"> }</span><span style="color:#7DCFFF">}</span></span>
<span class="line"><span style="color:#BA3C97">></span></span></code></pre>
<p>粒子和辉光场景里 MSAA 基本看不出来，关掉换 15% 的 GPU 余量，值。</p>
<h2>5. 灯光是奢侈品</h2>
<p>每盏灯都是一次全场景着色计算，阴影贴图更是每帧一次额外渲染。那个页面里有 7 盏 PointLight，其中 5 盏「只是想加点氛围」：</p>
<ul>
<li>氛围光交给 <code>ambientLight</code> + 材质的 <code>emissive</code></li>
<li>真实阴影换成 drei 的 <code>ContactShadows</code>（烘焙一次，不逐帧算）</li>
<li>保留的动态光不超过 2 盏</li>
</ul>
<h2>6. 设备分级，移动端主动降载</h2>
<p>最后一步是承认现实：手机 GPU 就是画不动桌面级的粒子量。入口处做一次分级：</p>
<pre class="shiki tokyo-night" style="background-color:#1a1b26;color:#a9b1d6" tabindex="0"><code><span class="line"><span style="color:#9D7CD8;font-style:italic">const</span><span style="color:#BB9AF7"> lowTier</span><span style="color:#89DDFF"> =</span></span>
<span class="line"><span style="color:#7AA2F7">  matchMedia</span><span style="color:#9ABDF5">(</span><span style="color:#89DDFF">'</span><span style="color:#9ECE6A">(pointer: coarse)</span><span style="color:#89DDFF">'</span><span style="color:#9ABDF5">)</span><span style="color:#89DDFF">.</span><span style="color:#7DCFFF">matches</span><span style="color:#BB9AF7"> ||</span><span style="color:#C0CAF5"> innerWidth</span><span style="color:#BB9AF7"> &#x3C;</span><span style="color:#FF9E64"> 768</span></span>
<span class="line"></span>
<span class="line"><span style="color:#7DCFFF">export</span><span style="color:#9D7CD8;font-style:italic"> const</span><span style="color:#BB9AF7"> quality</span><span style="color:#89DDFF"> =</span><span style="color:#9ABDF5"> {</span></span>
<span class="line"><span style="color:#73DACA">  particles</span><span style="color:#89DDFF">:</span><span style="color:#C0CAF5"> lowTier</span><span style="color:#BB9AF7"> ?</span><span style="color:#FF9E64"> 4000</span><span style="color:#BB9AF7"> :</span><span style="color:#FF9E64"> 10000</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  dpr</span><span style="color:#89DDFF">:</span><span style="color:#C0CAF5"> lowTier</span><span style="color:#BB9AF7"> ?</span><span style="color:#9ABDF5"> [</span><span style="color:#FF9E64">1</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 1.5</span><span style="color:#9ABDF5">]</span><span style="color:#BB9AF7"> :</span><span style="color:#9ABDF5"> [</span><span style="color:#FF9E64">1</span><span style="color:#89DDFF">,</span><span style="color:#FF9E64"> 2</span><span style="color:#9ABDF5">]</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#73DACA">  crystals</span><span style="color:#89DDFF">:</span><span style="color:#C0CAF5"> lowTier</span><span style="color:#BB9AF7"> ?</span><span style="color:#FF9E64"> 4</span><span style="color:#BB9AF7"> :</span><span style="color:#FF9E64"> 7</span><span style="color:#89DDFF">,</span></span>
<span class="line"><span style="color:#9ABDF5">}</span><span style="color:#89DDFF"> as</span><span style="color:#9D7CD8;font-style:italic"> const</span></span></code></pre>
<p>低端机少画一半粒子，用户只会觉得流畅，不会觉得寒酸。<strong>流畅本身就是高级感的一部分。</strong></p>
<h2>急救结果</h2>






























<table><thead><tr><th>指标</th><th>优化前</th><th>优化后</th></tr></thead><tbody><tr><td>FPS (M1)</td><td>24</td><td>120</td></tr><tr><td>Draw calls</td><td>217</td><td>41</td></tr><tr><td>三角形</td><td>340 万</td><td>38 万</td></tr><tr><td>GPU 帧耗时</td><td>38ms</td><td>6.2ms</td></tr></tbody></table>
<p>性能优化最反直觉的一点：<strong>它几乎从不牺牲画面</strong>。上面六步做完，页面看起来和原来一模一样 —— 只是终于对得起那个创意了。</p>]]></content:encoded>
    </item>
    <item>
      <title>为什么我把博客做成了数字花园，而不是时间线</title>
      <link>https://blog.yeluoxiaoxiao.top/blog/digital-garden</link>
      <guid isPermaLink="true">https://blog.yeluoxiaoxiao.top/blog/digital-garden</guid>
      <pubDate>Wed, 18 Feb 2026 00:00:00 GMT</pubDate>
      <description>关于「为什么还要写博客」这个老问题的一次自我回答：写作是索引思想的方式，花园是比时间线更诚实的隐喻，以及为什么这个站点长成了一片星云的样子。</description>
      <content:encoded><![CDATA[<p>每隔几年，「个人博客已死」的论调就会流行一轮。但我今年还是重写了自己的博客 —— 第四次。这篇文章想认真回答一个问题：<strong>在 2026 年，个人博客对我来说到底是什么？</strong></p>
<h2>时间线是新闻的隐喻，不是思想的</h2>
<p>传统博客是倒序时间线：最新的在最上面，旧文章一路沉底，仿佛写作的价值会随时间衰减。但我翻自己笔记的体验恰恰相反 —— 三年前的某段想法，常常在今天突然接上了电。</p>
<p>「数字花园」这个比喻我第一次听到就被击中了：</p>
<blockquote>
<p>花园里没有「过期」的植物，只有长势不同的植物。有的刚发芽，有的已经开花，有的需要修剪，有的正在和别的枝条缠绕生长。</p>
</blockquote>
<p>所以这个站点里，文章按主题聚集而不是按日期流放。旧文会被反复修订 —— 那不是历史记录，是还活着的想法。</p>
<h2>写作是思考的调试器</h2>
<p>我真正开始规律写作，是发现了一个副作用：<strong>写不清楚的地方，恰好就是没想清楚的地方</strong>。</p>
<p>写代码时我们有类型检查、有单元测试、有 linter。思考没有这些工具 —— 大脑非常擅长用「感觉懂了」糊弄自己。而写作会强迫所有隐式假设显式化，就像给思维过程加了一层静态检查。文章写卡住的位置，几乎精确对应理解的断层。</p>
<p>从这个角度说，博客发布出去有没有人看，反而是次要的。写的过程本身已经完成了大部分价值。</p>
<h2>为什么是星云</h2>
<p>决定重做博客时，我给自己提了个要求：这一版要「像我自己」，而不是像某个主题模板。</p>
<p>我最后选了星云作为整个站点的视觉隐喻，因为它和数字花园惊人地同构：</p>
<ul>
<li><strong>恒星在星云中诞生</strong> —— 想法在笔记的尘埃里凝聚成形</li>
<li><strong>引力让物质彼此吸引</strong> —— 主题相近的文章自然聚集成簇</li>
<li><strong>光要走很多年才被看见</strong> —— 今天写下的东西，可能几年后才照到某个人</li>
</ul>
<p>于是首页那一万颗粒子不只是炫技（好吧，也有一点），它是这个博客的自我介绍。</p>
<h2>给同样想写的你</h2>
<p>如果你也在犹豫要不要开始写博客，我的建议浓缩成三条：</p>
<ol>
<li><strong>给自己写，不给算法写。</strong> 流量是副产品，别让它变成目标函数</li>
<li><strong>发布粗糙的版本。</strong> 花园里允许有幼苗，完美主义是写作的头号杀手</li>
<li><strong>拥有自己的域名和数据。</strong> 平台会倒闭、会改推荐策略，而 Markdown 文件永远是你的</li>
</ol>
<p>这片星云会一直长下去。欢迎常来看看 —— 说不定哪颗星星，正好是你缺的那一颗。</p>]]></content:encoded>
    </item>
  </channel>
</rss>
