<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Jiahao Li - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Jiahao Li - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 16 Jun 2026 17:18:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/jiahao/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 趣谈 Jest 配置 ]]>
                </title>
                <description>
                    <![CDATA[ Jest 已经成为了大部分前端项目的标配，每次说到 Jest、Webpack、ESLint  等配置，脑瓜子都嗡嗡的。在诸多配置中，有时一个“铆钉大”的配置，就能让程序或测试的运行效率大幅下降。至于为啥要写这篇文章，就是因为目前所在的项目因一处 Jest  配置的问题，导致60多个 test case 在 --no-cache 条件下要跑足足 790s。 所以就记录分享一下 Jest 的一些常用配置。 module.exports = {   setupFiles: [     'react-app-polyfill/jsdom',     '<rootDir>/test/unit/jest.setup.js',     'core-js',   ],   testMatch: [     ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/jest-setting/</link>
                <guid isPermaLink="false">6162a27121a1350622df5186</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Sat, 09 Oct 2021 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/1_xFEOPesnxHTgZFBGzwQ6Kg.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><code>Jest</code> 已经成为了大部分前端项目的标配，每次说到 <code>Jest</code>、<code>Webpack</code>、<code>ESLint</code> 等配置，脑瓜子都嗡嗡的。在诸多配置中，有时一个“铆钉大”的配置，就能让程序或测试的运行效率大幅下降。至于为啥要写这篇文章，就是因为目前所在的项目因一处 <code>Jest</code> 配置的问题，导致60多个 <em>test case</em> 在 <code>--no-cache</code> 条件下要跑足足 <em><strong>790s</strong></em>。</p><p>所以就记录分享一下 <code>Jest</code> 的一些常用配置。</p><pre><code class="language-js">module.exports = {
  setupFiles: [
    'react-app-polyfill/jsdom',
    '&lt;rootDir&gt;/test/unit/jest.setup.js',
    'core-js',
  ],
  testMatch: [
    '&lt;rootDir&gt;/src/**/__tests__/**/*.{spec,test}.{js,jsx,ts,tsx}',
    '&lt;rootDir&gt;/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
  ],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '&lt;rootDir&gt;/node_modules/babel-jest',
    '^.+\\.(css|less)$': '&lt;rootDir&gt;/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '&lt;rootDir&gt;/config/jest/fileTransform.js',
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
  moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss|less)$': 'identity-obj-proxy',
    '\\.svg$': 'identity-obj-proxy',
    'test/(.*)': '&lt;rootDir&gt;/test/$1',
    '^src/(.*)': '&lt;rootDir&gt;/src/$1',
  },
  moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node',
  ],
  // 其它配置已省略
};
</code></pre><p>对于 <code>Jest</code> 的配置优化无外乎下面两点：</p><ul><li>更少：减少不必要的元素（比如图片、样式等）；</li><li>更精确：减少在文件系统中查找匹配的时间；</li></ul><p>在看下面如何优化之前，可以先看下这份 <code>Jest</code> 配置，看一下有没有什么可以想到的优化点</p><h3 id="setupfiles"><code>setupFiles</code></h3><p>古语云：“思则有备，有备无患”。但跑测试时一定要“万事”俱备才行吗？</p><p><code>setupFiles</code> 可谓是 <code>Jest</code> 的“内务府大臣”，位居二品！此大臣就是来准备测试所需要的一些环境或 <code>mock</code> 一些全局状态的，比如 <code>@testing-library/jest-dom</code> 就常在 <code>setupFile</code> 中用到，它允许我们可以在 <code>Jest</code> 中断言一些关于 <code>DOM</code> 的状态。而我们再回头去看上面的配置：</p><pre><code class="language-json">{
  "setupFiles": [
    "react-app-polyfill/jsdom",
    "&lt;rootDir&gt;/test/unit/jest.setup.js",
    "core-js"
  ]
}</code></pre><p><code>react-app-polyfill/jsdom</code> 做了什么：</p><pre><code class="language-js">// 真的，源码只有这三行，原来“内务府”也有“外包项目” 
if (typeof window !== 'undefined') {
  require ('whatwg-fetch');
}
</code></pre><p><code>whatwg-fetch</code> 和 <code>core-js</code> 可以简单理解为对当下的一些新标准做 <code>polyfill</code>，但我们有 <code>babel-jest</code> 呀，还要你二者何用？果断“开掉”，节省开支</p><pre><code class="language-json">{
  "setupFiles": ["&lt;rootDir&gt;/test/unit/jest.setup.js"]
}</code></pre><h3 id="modulefileextensions"><code>moduleFileExtensions</code></h3><p><code>moduleFileExtensions</code> 就是 <code>Jest</code> 中各个“国家”（模块）的“通关文牒”，有此文牒方可游历各国。游遍各国也得有个顺序不是，不然会徒增“食宿饮马等费用”，要是搁徐霞客身上，若不是他家业富足，不然大可能会饿死途中。张骞通西域朝廷会有路线规划（默认配置），当然他也能随机应变（自定义配置），默认配置大部分情况下是行得通的，只不过可能要在“路上”多花些时间：</p><pre><code class="language-js">// 默认配置
["js", "jsx", "ts", "tsx", "json", "node"]
</code></pre><p><code>moduleFileExtensions</code> 会 <strong>从左到右</strong> 查找对应的 <code>extension</code>，但如果在 <code>TypeScript + React</code> 项目中可能稍微调整一下会更好：</p><pre><code class="language-js">// 调整后
["ts", "tsx", "js", "jsx", "json", "node"]
</code></pre><p>这样就能减少些查找 <code>extension</code> 的次数，省点“油钱”。</p><h3 id="modulenamemapper"><code>moduleNameMapper</code></h3><p>一般 <code>npm</code> 依赖中的源码分为 <code>esm</code> 和 <code>cjs</code> 模块，当然像 <code>react</code> 之类的是分为 <code>cjs</code> 和 <code>umd</code>。以 <code>antd</code> 为例，其结构如下：</p><pre><code class="language-js">antd
  |- lib/
  |- es/
  |- package.json
</code></pre><p>其中，在 <code>package.json</code> 中可以制定 <code>esm</code> 和 <code>cjs</code> 打包文件的目录，以 <code>antd</code> 中使用到的 <code>rc-select</code> 组件包为例：</p><pre><code class="language-json">{
  "version": "12.1.5",
  "main": "./lib/index",
  "module": "./es/index"
}</code></pre><p>如要使用 <code>cjs</code> 规范的打包文件，工具会查询 <code>main</code> 字段对应文件路径内的入口文件，使用 <code>esm</code> 规范的打包文件，工具则会查询 <code>module</code> 字段对应的入口文件。</p><p>但在 <code>rc-util</code> 中，并没有指明 <code>main</code> 及 <code>module</code> 字段，那么其使用方式就像下面这样：</p><pre><code class="language-js">// rc-select/es/utils/legacyUtils.js
import toArray from 'rc-util/es/Children/toArray';
</code></pre><p>“聊 <code>Jest</code> 呢，叨叨上面这么多干嘛呢？”</p><p>因为 <code>Jest</code> 目前支持的是 <code>cjs</code> 规范，项目中又用到了 <code>antd</code>，所以对于其使用的 <code>rc-util</code> 这种依赖，<code>Jest</code> 无法处理，需要手动转换一下，这就需要引入一个 <code>Jest</code> 配置字段 —— <code>moduleNameMapper</code>，关于该配置字段的描述文档如下所述：</p><blockquote><em>A map from regular expressions to module names that allow to stub out resources, like images or styles with a single module.</em></blockquote><p>说白了就是用来 <code>stub</code> 一些资源文件或 <code>module</code> 使用的，可以把匹配到的内容映射为你指定的内容，哪怕是“指鹿为马”也是行得通的！在前端的单元测试中，时常有许多内容是不需要的，比如：静态资源、样式文件等。那么这个时候就可以将这些“鹿”指成“马”了。</p><p>我们常把“鹿 ”指为 <code>identity-obj-proxy</code> 这个工具，虽然 <code>identity-obj-proxy</code> 上次发布是5年前了，但确实很好用，并且源码也十分简单（2分钟你看不完源码你顺着信号 来打我）！</p><pre><code class="language-js">module.exports = {
  // ...
  moduleNameMapper: {
    '\\.svg$': 'identity-obj-proxy',
    '\\.css$': 'identity-obj-proxy',
  },
};
</code></pre><p>对于上面说到的将 <code>antd</code> 的 <code>es</code> 指到 <code>lib</code> 也就很简单了：</p><pre><code class="language-js">module.exports = {
  // ...
  moduleNameMapper: {
    'antd/es/(.*)': 'antd/lib/$1',
  },
};
</code></pre><p>通过 <code>moduleNameMapper</code> 就可以做到 <strong><em>更少</em></strong> 这个原则，当然下面要介绍的 <code>transformIgnorePatterns</code> 以及其它 <code>ignore</code> 等相关字段也可以让处理的资源或无关的资源更少。</p><h3 id="transform"><code>transform</code></h3><p>《天龙八部》中，马大元夫人康敏将一招“借刀杀人”发挥得淋漓尽致，而在 <code>Jest</code> 中 <code>transform</code> 也“借他人之手除掉异己”，至于康敏居心何在下回再讲，这次就说 <code>transform</code> 为何要“下此毒手”。所谓“异己”一般是脱离自己控制的资源，上面说到 <code>Jest</code> 支持的是 <code>cjs</code>，但在现在的前端项目中，一般都是使用 <code>import/export</code> 等 <code>esm</code> 规范来模块化开发，所以对于这种资源，我们需“借他之手”处理：</p><pre><code class="language-js">module.exports = {
    // ...
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};
</code></pre><p>但这么看来，<code>transform</code> 也可以将 <code>antd</code> 中的 <code>esm</code> 资源转为 <code>cjs</code>，但既然可以“礼仪教化”，又何必“兵刃相接”呢？</p><h3 id="transformignorepatterns"><code>transformIgnorePatterns</code></h3><p>通过名字就能看出来，此配置的内容是“康敏”理都不想理的内容，该值默认是 <code>['node_modules']</code>，也十分好理解。但我们回到文章最初的配置去看看：</p><pre><code class="language-js">module.exports = {
  // ...
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
};
</code></pre><p>项目的初衷是使用 <code>transform</code> 去处理引入的 <code>antd</code> 的资源，但这也就导致了在 <code>transform</code> 时去遍历了整个 <code>node_modules</code> 文件系统，<code>node_modules</code> 内容是非常多的，所以在扫描时耗费了大量的时间，测试跑完发现“乔峰找着他爹了”。</p><p>所以本文最开始所说的“一处配置问题”就是这儿，删掉 <code>transformIgnorePatterns</code> 转而使用 <code>moduleNameMapper</code> 会快很多，不信你试试！</p><h2 id="-">结语</h2><p>当优化、最佳实践等问题脱离具体项目场景时，那就是“耍流氓”！上学时老师经常说：“具体问题，具体分析”，对开发来说也是如此。经过优化后，测试消耗时间从 <strong><em>790s</em></strong> 缩减到了 <strong><em>40s</em></strong>，还算是一个可以接受的时间吧✌️</p><p>欢迎搜索关注微信公众号 “Refactor”，阅读我的更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ ThreeJS 源码剖析之 Renderer（二） ]]>
                </title>
                <description>
                    <![CDATA[ 在上一篇文章 [https://chinese.freecodecamp.org/news/threejs-renderer/]里，我们初探 Renderer。这篇文章将介绍常用的 Renderer 中的方法。 clear this.clear = function ( color, depth, stencil ) {   let bits = 0;   if ( color === undefined || color ) bits |= _gl.COLOR_BUFFER_BIT;   if ( depth === undefined || depth ) bits |= _gl.DEPTH_BUFFER_BIT;   if ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/threejs-renderer-2/</link>
                <guid isPermaLink="false">60a23e16724d3b04e234ef4c</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Wed, 08 Sep 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/daria-shevtsova-zbWFT4eVopE-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在<a href="https://chinese.freecodecamp.org/news/threejs-renderer/">上一篇文章</a>里，我们初探 Renderer。这篇文章将介绍常用的 <em><em>Renderer</em></em> 中的方法。</p><h3 id="clear"><strong>clear</strong></h3><pre><code class="language-js">this.clear = function ( color, depth, stencil ) {
  let bits = 0;

  if ( color === undefined || color ) bits |= _gl.COLOR_BUFFER_BIT;
  if ( depth === undefined || depth ) bits |= _gl.DEPTH_BUFFER_BIT;
  if ( stencil === undefined || stencil ) bits |= _gl.STENCIL_BUFFER_BIT;

  _gl.clear( bits );
};
</code></pre><p><code>clear</code> 方法接收三个可选参数，分别是清空 <em>颜色缓冲区、深度缓冲区和模板缓冲区</em>。而 <code>gl.clear()</code> 方法可以接受一个复合值，所以采用了 <code>bits |= value</code> 形式来清空缓冲区。同时，<code>clearColor/clearDepth/clearStencil</code> 三个方法内部也都是调用的本 <code>clear</code> 方法。</p><h3 id="setsize"><strong>setSize</strong></h3><p>当我们 <em>new</em> 了一个 <em>Renderer</em> 之后，会经常调用 <code>setSize</code> 来设置 <code>canvas</code> 的大小，那我们就看一下 <code>setSize</code> 的实现：</p><pre><code class="language-js">this.setSize = function ( width, height, updateStyle ) {
  if ( xr.isPresenting ) {
    console.warn( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' );
    return;
  }
  
  _width = width;
  _height = height;
  _canvas.width = Math.floor( width * _pixelRatio );
  _canvas.height = Math.floor( height * _pixelRatio );

  if ( updateStyle !== false ) {
    _canvas.style.width = width + 'px';
    _canvas.style.height = height + 'px';
  }

  this.setViewport( 0, 0, width, height );
};
</code></pre><p>在上面的代码中我们不仅仅设置了 <code>canvas</code> 的 <code>width</code> 和 <code>height</code>，同时还设置了 <code>style</code> 以及 <code>viewport</code> 的 <code>width</code> 和 <code>height</code>。如果我们将 <code>style</code> 的宽高和 <code>canvas</code> 的宽高设置的不同会出现什么效果呢？以之前 <em>WebGL基础</em> 系列文章中纹理贴图中超级赛人的代码为例，下图是我们正常显示的图片（<code>style</code> 和 <code>canvas</code> 宽高都是 <em>600px</em>）：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><p><code>canvas</code> 尺寸保持不变，我们将 <code>style</code> 的 <code>width</code> 设为 <em>1200px</em> 看一下有什么效果：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-6.png" class="kg-image" alt="image-6" width="600" height="400" loading="lazy"></figure><p>可见，图片发生了形变的同时也变得更模糊了，为了避免这种情况的发生，我们一般会将 <code>canvas</code>、<code>style</code> 和 <code>viewport</code> 的尺寸设为相同的大小。</p><h3 id="render"><strong>render</strong></h3><p><code>render()</code> 是我们使用最多的方法，作为 <code>renderer</code> 的核心，让我们来瞧瞧它的玄机。官方文档中 <code>render()</code> 方法的用法为：</p><pre><code class="language-js">// Methods
.render ( scene : Object3D, camera : Camera ) : null
// Render a scene or another type of object using a camera.
</code></pre><p>可以看到文档中提到“使用 <code>camera</code> 渲染的是 <code>scene</code> 或其它 <code>object</code>”，故此处 <code>scene</code> 的类型是 <code>Object3D</code> 而不是 <code>Scene</code>（<code>Object3D</code> 是 <em>ThreeJS</em> 中所有物体的基类）。同时，在文档中 <code>render</code> 方法只给出了两个参数，但我们看源码会发现其实它还能再额外接受两个参数 <code>renderTarget</code> 和 <code>forceClear</code>，这是为了兼容老版本中接受的这两个参数，所以就从文档中移除了，取而代之的是使用 <code>renderer</code> 的 <code>setRenderTarget</code> 和 <code>clear</code> 两个方法。</p><p><strong>context</strong></p><p>接着看 <code>render</code> 方法，可以看到一个很有趣的语句：</p><pre><code class="language-js">if ( _isContextLost === true ) return;
</code></pre><p>语义上来说，如果丢失了当前的 <em>context</em> 就直接 <em>return</em>，而什么会触发 <em>WebGL</em> 上下文丢失的事件呢？我们去瞅一眼 specification 便知：</p><blockquote>Occurrences such as <strong>power events on mobile devices</strong> may cause the WebGL rendering context to be lost at any time and require the application to rebuild it...</blockquote><p><a href="https://link.zhihu.com/?target=https%3A//www.khronos.org/registry/webgl/specs/latest/1.0/%235.14.13"></a><a href="https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14.13">WebGL Specification​www.khronos.org</a></p><p>比如当我们的设备休眠时就会触发该事件。而 <em>丢失上下文</em> 这一事件属于 <code>WebGLContextEvent</code>，<em>WebGL</em> 的 <code>WebGLContextEvent</code> 会响应 <em>WebGL</em> 上下文状态的重要变换而生成相应事件。事件会通过 <em>DOM</em> 事件系统发送，并被调度到 <em>WebGL</em> 渲染上下文关联的 <code>HTMLCanvasElement</code> 或 <code>OffscreenCanvas</code> 上。可以触发上下文变换的 <code>WebGLContextEvent</code> 包括上下文的丢失、恢复和无法创建上下文。</p><blockquote><code>OffscreenCanvas</code> 顾名思义是 <em>离屏 Canvas，详情请参考下文</em></blockquote><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas">OffscreenCanvas​developer.mozilla.org</a></p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/v2-72382e605ce3eba8154aa310a712c403_ipico.jpg" class="kg-image" alt="图标" width="600" height="400" loading="lazy"></figure><p>当客户端检测到图形缓冲区关联的 <em>WebGL</em> 渲染上下文丢失时，会执行以下操作：</p><ol><li>让 <em>canvas</em> 作为上下文的 <em>canvas</em>；</li><li>如果设置了 <em>WebGL</em> 上下文的 <em>ContextLost</em> 标志，则中止后续步骤；</li><li>否则设置 <em>ContextLost</em> 标志；</li><li>给上下文创建的 <em>WebGLObject</em> 实例设置 <em>Invalidated</em> 标志；</li><li>禁用除 <em>WEBGL_LOST_CONTEXT</em> 外的所有的 <em>Extensions</em>；</li><li>执行下列任务队列：</li><li>触发 <em>canvas</em> 的名为 <code>WEBGL_CONTEXT_LOST</code> 的 <em>WebGL</em> 上下文事件，将其 <code>statusMessage</code> 设为空值；</li><li>如果未设置事件的 <em>canceled</em> 标志，则中止后续步骤；</li><li>异步执行下列步骤；</li><li>等待可恢复的图形缓冲区；</li><li>任务队列恢复上下文的图形缓冲区；</li></ol><p>第 <em>1</em> 点会有些难以理解，<em>specification</em> 中的原话是 <em>Let canvas be the context's canvas</em>，下面介绍一下创建上下文的相应过程，可能会有助于了解这句话。</p><p>从上面的描述中可以知道，我们必须通过 <em>canvas</em> 或离屏 <em>canvas</em> 获取到上下文才能使用 <em>WebGL API</em>，也就是说每个 <em>WebGLRenderingContext</em> 创建时都需要关联 <em>canvas</em>。每个 <em>canvas</em> 在创建时都会有相应的创建参数：</p><pre><code class="language-cpp">dictionary WebGLContextAttributes {
    boolean alpha = true;
    boolean depth = true;
    boolean stencil = false;
    boolean antialias = true;
    boolean premultipliedAlpha = true;
    boolean preserveDrawingBuffer = false;
    WebGLPowerPreference powerPreference = "default";
    boolean failIfMajorPerformanceCaveat = false;
    boolean desynchronized = false;
};
</code></pre><p>平时我们在获取 <em>context</em> 时仅仅是通过 <code>gl.getContext('webgl')</code> 来获取，其实我们可以通过该方法的第二个参数给 <em>context</em> 传递相应属性：</p><pre><code class="language-js">gl.getContext("webgl", { alpha: false, depth: false });
</code></pre><p>当调用 <code>getContext()</code> 方法返回 <em>context</em> 实例时，客户端会执行以下操作：</p><ol><li>创建新的 <em>WebGLRenderingContext</em> 对象：<em>context</em>；</li><li>让 <em>context</em> 的 <em>canvas</em> 成为 <code>getContext()</code> 方法所关联的 <em>canvas</em> 或离屏 <em>canvas</em>;</li><li>创建新的 <em>WebGLContextAttributes</em> 对象：<em>contextAttributes</em>；</li><li>如果调用 <code>getContext()</code> 方法时传递了第二个参数，则使用给定的参数作为 <em>contextAttributes</em>；</li><li>使用 <em>contextAttributes</em> 创建图形缓冲区，并将图形缓冲区与 <em>context</em> 相关联；</li><li>如果创建失败，则触发相应错误并返回 <code>null</code>；</li><li>创建新的 <em>WebGLContextAttributes</em> 对象：<em>actualAttributes</em>；</li><li>基于创建的图形缓冲区属性设置 <em>actualAttributes</em> 的属性；</li><li>将 <em>context</em> 的创建参数设为 <em>contextAttributes</em>；</li><li>将 <em>context</em> 的实际参数设为 <em>actualAttributes</em>；</li><li>返回 <em>context</em>；</li></ol><p>解释了创建 <em>context</em> 的流程后，或许有对理解刚刚的疑惑点有所帮助 （翻了好久 <em>specification</em>）。</p><p>说了那么久 <em>context</em>，让我们再重新 focus 在 <code>render()</code> 方法上。既然我们会在 <code>requestAnimationFrame</code> 中调用 <code>render()</code>，那么也就意味着其实 <code>render()</code> 方法内部做的应该就是去绘制每一帧应该展现的场景。在 <em>WebGL</em> 基础文章中绘制时，在每次 <code>requestAnimationFrame</code> 的 <code>loop</code> 中都会都 <em>clear</em> 一下，当然 <em>ThreeJS</em> 中也一样，当判断 <em>context</em> 存在后，就会立即为本次绘制重置上一帧的缓存：</p><pre><code class="language-js">bindingStates.resetDefaultState();
_currentMaterialId = - 1;
_currentCamera = null;
</code></pre><p>随后便更新 <em>相应变换矩阵</em>，并渲染背景 <code>background.render( currentRenderList, scene, camera, forceClear )</code>。对于 <em>background</em> 的 <code>render</code> 方法，其内部主要对 <em>background</em> 分为了3种类型：1. 如果 <em>background</em> 为 <em>null</em>，则使用默认的参数 <em>render</em>；2. 如果是 <em>color</em>， 则使用已有的 <em>color render</em>；3. 如果是 <em>texture</em>，则更新贴图（小声哔哔一句，传入的 <em>camera</em> 并没有用到）。</p><p>在 <em>ThreeJS</em> 中，将要渲染的物体分为了两类：<em>透明物体</em> 和 <em>不透明物体</em>（先不详细介绍，在后续阅读 <em>Object3D</em> 源码时再聊），并通过下面的语句获取并处理这两类 <em>object</em>：</p><pre><code class="language-js">const opaqueObjects = currentRenderList.opaque;	// 可见物体
const transparentObjects = currentRenderList.transparent;	// 透明物体

if ( opaqueObjects.length &gt; 0 ) renderObjects( opaqueObjects, scene, camera );
if ( transparentObjects.length &gt; 0 ) renderObjects( transparentObjects, scene, camera );
</code></pre><p><strong>renderObjects</strong></p><pre><code class="language-js">function renderObjects( renderList, scene, camera ) {
  const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

  for ( let i = 0, l = renderList.length; i &lt; l; i ++ ) {
    const renderItem = renderList[ i ];
		// ...

    if ( camera.isArrayCamera ) {
      _currentArrayCamera = camera;
      const cameras = camera.cameras;
      for ( let j = 0, jl = cameras.length; j &lt; jl; j ++ ) {
        const camera2 = cameras[ j ];
        if ( object.layers.test( camera2.layers ) ) {
          // ...
          renderObject( object, scene, camera2, geometry, material, group );
        }
      }
    } else {
			// ...
      renderObject( object, scene, camera, geometry, material, group );
    }
  }
}
</code></pre><p>对于 <code>secene.isScene</code> 这条语句很好理解，因为我们的函数签名中传入的 <code>scene</code> 是 <code>Object3D</code> 类型，所以在 <code>Scene</code> 中有个 <code>readonly</code> 的属性叫做 <code>isScene</code>（永远为 <code>true</code>）来判别当前的 <code>Object3D</code> 是否是 <code>Scene</code>。同理，<code>camera.isArrayCamera</code> 也是一个永远为 <code>true</code> 的 <code>readonly</code> 属性，来用来判别是否是 <code>camera</code>，只不过这并不是 <code>Camera</code> 中的属性，而是 <code>ArrayCamera</code> 类中的属性，该类继承自 <code>PerspectiveCamera</code>。</p><p>如果传入的 <code>camera</code> 不是 <code>ArrayCamera</code> 类型，则调用 <code>renderObject</code>；否则，循环遍历 <code>ArrayCamera.cameras</code> 并调用 <code>renderObject</code>。</p><p><strong>renderObject</strong></p><pre><code class="language-js">function renderObject( object, scene, camera, geometry, material, group ) {
  object.onBeforeRender( _this, scene, camera, geometry, material, group );
  // ...

  // 矩阵变换：模型视图矩阵 * 世界逆矩阵 * 世界矩阵
  object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
  object.normalMatrix.getNormalMatrix( object.modelViewMatrix );

  if ( object.isImmediateRenderObject ) {
    // ...
    renderObjectImmediate( object, program );
  } else {
    _this.renderBufferDirect( camera, scene, geometry, material, object, group );
  }

  object.onAfterRender( _this, scene, camera, geometry, material, group );
  // ...
}
</code></pre><p><code>object.onBeforeRender</code> 是 <code>Object3D</code> 的一个钩子，在渲染之前调用，每个继承 <code>Object3D</code> 的 <code>Object</code> 有自己独立的 <code>onBeforeRender</code> 的实现。<code>object.isImmediateRenderObject</code> 又是 <code>ImmediateRenderObject</code> 的一个属性，用来判断是否是需要立即渲染的物体，如果满足条件则调用 <code>renderObjectImmediate</code> 否则调用 <code>renderBufferDirect</code>。</p><p><strong>renderObjectImmediate</strong></p><pre><code class="language-js">this.renderBufferImmediate = function ( object, program ) {
  bindingStates.initAttributes();
  const buffers = properties.get( object );

  if ( object.hasPositions &amp;&amp; ! buffers.position ) buffers.position = _gl.createBuffer();
  // ...
  const programAttributes = program.getAttributes();

  if ( object.hasPositions ) {
    _gl.bindBuffer( _gl.ARRAY_BUFFER, buffers.position );
    _gl.bufferData( _gl.ARRAY_BUFFER, object.positionArray, _gl.DYNAMIC_DRAW );
    bindingStates.enableAttribute( programAttributes.position );
    _gl.vertexAttribPointer( programAttributes.position, 3, _gl.FLOAT, false, 0, 0 );
  }
  // ...
  bindingStates.disableUnusedAttributes();
  _gl.drawArrays( _gl.TRIANGLES, 0, object.count );
  object.count = 0;
};
</code></pre><p><code>renderObjectImmediate</code> 中的内容看起来是不是很熟悉呢？<code>createBuffer -&gt; bindBuffer -&gt; bufferData -&gt; enableAttribute -&gt; vertexAttribPointer</code> 这一波操作不就是将缓冲区分配给 <code>attrubute</code> 变量并从缓冲区读取变量嘛？聪明啊老弟~它就是干了这个事儿！</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ WebGL 纹理 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 在WebGL中有一项很重要的技术 —— 纹理映射。所谓纹理映射，就是将一张图片映射到一个几何图形的表面上去（就像孩童时喜欢在胳膊、手背上贴贴纸一样） 将“贴纸”贴到一个矩形上之后，这个矩形表面看上去就像是一张图片，而此时，这张图片又可以称为纹理图像或纹理。 纹理映射的作用，就是根据纹理图像为之前光栅化后的每个片元涂上适当的颜色，组成纹理图像的像素又被称为纹素，每一个纹素的颜色都使用RGB或RGBA格式编码。如图： 图中的每个小方块都是一个纹素（图片来源 [https://www.kmeel.com/wp-content/uploads/2019/01/Uneven-stone-texture-44710-1246x831.jpg] ）。 纹理映射 问：在WebGL中进行纹理映射，分为几步？ 答：4步。 第一步 - 准备纹理图像 作为一名龙珠的爱好者，在此我就准备了一张悟空的图片（图片来源 [https://res.cloudinary.com/jerrick/image/upload/fl_progressive,q_auto,w_1024/clabm2l3mg6ij4 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/webgl-texture/</link>
                <guid isPermaLink="false">60d88973fff62a063e576984</guid>
                
                    <category>
                        <![CDATA[ WebGL ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Sat, 26 Jun 2021 11:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/06/pawel-czerwinski-2dyR13FNg2I-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-"><strong>前言</strong></h2><p>在<code>WebGL</code>中有一项很重要的技术 —— <strong>纹理映射</strong>。所谓纹理映射，就是将一张图片映射到一个几何图形的表面上去（就像孩童时喜欢在胳膊、手背上贴贴纸一样） 将“贴纸”贴到一个矩形上之后，这个矩形表面看上去就像是一张图片，而此时，这张图片又可以称为<strong>纹理图像</strong>或<strong>纹理</strong>。</p><p>纹理映射的作用，就是根据纹理图像为之前光栅化后的每个片元涂上适当的颜色，组成纹理图像的像素又被称为<strong>纹素</strong>，每一个纹素的颜色都使用<code>RGB</code>或<code>RGBA</code>格式编码。如图：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-a49b5d49156434978d194c2106e59690_720w.jpg" class="kg-image" alt="v2-a49b5d49156434978d194c2106e59690_720w" width="600" height="400" loading="lazy"></figure><p>图中的每个小方块都是一个纹素（<a href="https://www.kmeel.com/wp-content/uploads/2019/01/Uneven-stone-texture-44710-1246x831.jpg">图片来源</a>）。</p><h2 id="--1"><strong>纹理映射</strong></h2><p>问：在<code>WebGL</code>中进行纹理映射，分为几步？</p><p>答：4步。</p><h2 id="--2">第一步 - 准备纹理图像</h2><p>作为一名<strong>龙珠</strong>的爱好者，在此我就准备了一张悟空的图片（<a href="https://res.cloudinary.com/jerrick/image/upload/fl_progressive,q_auto,w_1024/clabm2l3mg6ij4anv5hu.png">图片来源</a>）：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-2fc6a0e529a64dcda4ebbd4bd08f9529_720w.jpg" class="kg-image" alt="v2-2fc6a0e529a64dcda4ebbd4bd08f9529_720w" width="600" height="400" loading="lazy"></figure><h2 id="--3">第二步 - 为几何图形配置映射方式</h2><p>指定映射方式就是确定“几何图形的某个片元”的颜色如何决定。我们利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖，使用<strong>纹理坐标</strong>来确定纹理图像的哪部分将覆盖到几何图形上。纹理坐标是一套新的坐标系统，下面将会对纹理坐标进行简单的介绍。</p><h3 id="--4">纹理坐标</h3><p>纹理坐标是纹理图像上的坐标，通过纹理坐标可以在纹理图像上获取纹素颜色。<code>WebGL</code>系统中的纹理坐标系统是<strong>二维</strong>的，为了将纹理坐标和我们平时使用的坐标系统区分开来，<code>WebGL</code>中使用<code>s</code>和<code>t</code>命名纹理坐标系统（<code>st</code>坐标系统）：</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-628b0a7fb1506368b821299d80922c47_720w.jpg" class="kg-image" alt="v2-628b0a7fb1506368b821299d80922c47_720w" width="600" height="400" loading="lazy"></figure><p>如图，在纹理坐标系中，纹理图像的左下角为<code>(0.0, 0.0)</code>，右上角为<code>(1.0, 1.0)</code>。不要与<code>WebGL</code>的坐标系统搞混哦！</p><h3 id="--5">将纹理映射到几何图形</h3><p>来看看这张图：</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-14c462d9196a5bb87716e89b7996ef46_720w.jpg" class="kg-image" alt="v2-14c462d9196a5bb87716e89b7996ef46_720w" width="600" height="400" loading="lazy"></figure><p>这张图是将纹理图像的顶点映射到<code>WebGL</code>坐标系统中的四个顶点处，有小伙伴可能会想到“将这个长方形的图片映射到一个正方形的区域，图片岂不是会变形”，要注意在<code>WebGL</code>坐标系统中我们使用的<code>(0.5, 0.5, 0.0)</code>这种坐标是一个相对的坐标值，如果我们的<code>canvas</code>是个正方形，那么上图中对应的映射区域就是个正方形，如果是长方形，同理映射区域就是个长方形。下面来看看我们的着色器如何编写：</p><pre><code class="language-glsl">// 顶点着色器
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;

void main() {
  gl_Position = a_Position;
  v_TexCoord = a_TexCoord;
}</code></pre><p>顶点着色器中多声明了一个<code>vec2</code>变量，用来接收纹理图像的坐标，而在片元着色器会在稍后介绍。再修改一下<code>initVertexBuffers</code>方法：</p><pre><code class="language-js">function initVertexBuffers (gl) {
  const verticesTexCoords = new Float32Array([
    // 顶点坐标    纹理坐标
    -0.5, 0.5,    0.0, 1.0, 
    -0.5, -0.5,   0.0, 0.0,
    0.5, 0.5,     1.0, 1.0,
    0.5, -0.5,    1.0, 0.0,
  ]);
  const n = 4;
  
  // 创建缓冲区对象
  const vertexTexCoordBuffer = gl.createBuffer();
  
  // ...
  // 将顶点坐标写入缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
  
  // ...
  // 将纹理坐标分配给a_TexCoord并开启它
  const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
  
  // ...
  return n;
}
</code></pre><p>上面代码在之前的文章中写过很多遍，主要是添加了纹理坐标，就不再赘述。这样就在顶点着色器中接收到了纹理坐标，并光栅化后传给片元着色器；随后，片元着色器根据片元的纹理坐标，从纹理图像中抽取出纹素颜色，赋给当前片元，并设置顶点的纹理坐标（<code>initVertexBuffers()</code>）。</p><h2 id="--6">第三步 - 加载纹理图像</h2><p>加载纹理图像要使用我们的<code>Image</code>对象来完成：</p><pre><code class="language-js">function initTexture (gl, n) {
  const texture = gl.createTexture(); // 创建纹理对象
  
  // 获取 u_Sampler 的存储位置（会在第四步中介绍）
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
  const image = new Image();
  
  // 注册图像加载事件响应函数
  image.onload = function () {
    loadTexture(gl, n, texture, u_Sampler, image);
  };
  image.src = '...';
  
  return true;
}

function loadTexture (gl, n, u_Sampler, image) {
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); //翻转纹理图像的 y 轴
  gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元
  gl.bindTexture(gl.TEXTURE_2D, texture); // 向 target 绑定纹理对象
  
  // 配置纹理参数
  gl.texParameteri(gl.TEXTRUE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
  
  gl.uniform1i(u_Sampler, 0); // 将 0 号纹理传递给着色器
  
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // 绘制矩形
}
</code></pre><p><code>initTexture</code>函数中应该比较好理解，下面将直接介绍<code>loadTexture</code>函数。首先在我们的<code>WebGL</code>系统中有8个纹理单元分别是<code>gl.TEXTURE0</code>到<code>gl.TEXTURE7</code>，这每一个纹理单元都与<code>gl.TEXTURE_2D</code>相关联，而后者就是绑定纹理时的纹理目标：</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-eedf9f651051c3e22d7bd28bddf9c25e_720w.jpg" class="kg-image" alt="v2-eedf9f651051c3e22d7bd28bddf9c25e_720w" width="600" height="400" loading="lazy"></figure><p>当调用<code>gl.createTexture</code>后，<code>WebGL</code>系统中就会存在一个纹理对象：</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-bcfc932e0b27dfdc3a013d58cba0057b_720w.jpg" class="kg-image" alt="v2-bcfc932e0b27dfdc3a013d58cba0057b_720w" width="600" height="400" loading="lazy"></figure><h3 id="--7">坐标轴翻转</h3><p><code>gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)</code>函数是<code>WebGL</code>中的图像预处理函数，第一个参数是处理方式，第二个参数为处理方式的参数。</p><p><code>WebGL</code>中的纹理坐标系统的<code>t</code>轴方向与PNG/BMP/JPG等格式图片的坐标系统的<code>y</code>轴方向是相反的。所以只有先将图像的<code>y</code>轴进行反转，才能将图像正确地映射到图形上：</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-c356908e3f20a82575c603fa12315ca2_720w.jpg" class="kg-image" alt="v2-c356908e3f20a82575c603fa12315ca2_720w" width="600" height="400" loading="lazy"></figure><h3 id="--8">激活纹理单元</h3><p><code>WebGL</code>通过一种叫做<strong>纹理单元</strong>的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像，一些其他系统支持的个数更多。内置变量<code>gl.TEXTURE0</code>到<code>gl.TEXTURE7</code>各代表一个纹理单元。</p><p>在使用纹理单元之前，需要调用<code>gl.activeTexture(gl.TEXTURE0)</code>来激活它（下图中激活的是<code>TEXTURE0</code>）：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-9e81d61e36a18f63cba9dc72e1249820_720w.jpg" class="kg-image" alt="v2-9e81d61e36a18f63cba9dc72e1249820_720w" width="600" height="400" loading="lazy"></figure><h3 id="--9">绑定纹理对象</h3><p>接下来，我们还要告诉<code>WebGL</code>系统纹理对象使用的是哪种类型的纹理。在对纹理对象操作之前，我们需要绑定纹理对象，这里会发现这一系列的操作和缓冲区很相似：在对缓冲区对象进行操作之前，也需要绑定缓冲区对象。<code>WebGL</code>支持两种类型的纹理：<code>gl.TEXTURE_2D</code>和<code>gl.TEXTURE_CUBE_MAP</code>，分别为二维纹理和立方体纹理。当调用<code>gl.bindTexture</code>后：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-db33eec4ab8a88ac5df0fabd531c42a8_720w.jpg" class="kg-image" alt="v2-db33eec4ab8a88ac5df0fabd531c42a8_720w" width="600" height="400" loading="lazy"></figure><p>这样我们就指定了纹理对象的类型（<code>gl.TEXTURE_2D</code>）。</p><h3 id="--10">配置纹理对象参数</h3><p>配置纹理对象的参数的目标主要是设置：如何根据纹理坐标获取纹素颜色、以及按哪种方式重复填充纹理。对于<code>gl.texParameteri()</code>方法的参数含义如下图：</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-1e02f3e116a3e7c24d46e8d3d8180e8f_720w.jpg" class="kg-image" alt="v2-1e02f3e116a3e7c24d46e8d3d8180e8f_720w" width="600" height="400" loading="lazy"></figure><p><code>gl.TEXTURE_MAG_FILTER</code>和<code>gl.TEXTURE_MIN_FILTER</code>的非金字塔纹理类型常量：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-cbe5a588b2f0f782a709b45f1411eb98_720w.jpg" class="kg-image" alt="v2-cbe5a588b2f0f782a709b45f1411eb98_720w" width="600" height="400" loading="lazy"></figure><p>可以赋值给<code>gl.TEXTURE_WRAP_S</code>和<code>gl.TEXTURE_WRAP_T</code>的常量（可以想象一下以往在Windows系统中设置桌面壁纸时的平铺/拉伸等选项）：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-744d3f4984c40375cd583231a3b9c6c0_720w.jpg" class="kg-image" alt="v2-744d3f4984c40375cd583231a3b9c6c0_720w" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-c3be790565c846f22b650cd0cc35fc5e_720w.jpg" class="kg-image" alt="v2-c3be790565c846f22b650cd0cc35fc5e_720w" width="600" height="400" loading="lazy"></figure><h3 id="--11">将纹理图像分配给纹理对象</h3><p>使用<code>gl.texImage2D</code>方法将纹理图像分配给纹理对象，同时该函数还允许告诉<code>WebGL</code>系统关于该图像的一些特性。此API参数比较复杂，详细了解请参考<a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/texImage2D" rel="nofollow noreferrer">MDN texImage2D</a>。</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-d22f8e89b4c5b8d7b864294f3b4cf6b2_720w.jpg" class="kg-image" alt="v2-d22f8e89b4c5b8d7b864294f3b4cf6b2_720w" width="600" height="400" loading="lazy"></figure><h2 id="-fs-">第四步 - 在FS中抽取纹素并赋给片元</h2><h3 id="--12">将纹理单元传递给片元着色器</h3><p>首先让我们来看一下片元着色器代码：</p><pre><code class="language-glsl">// 片元着色器
#ifdef GL_ES
	precision mediump float;
#endif
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;

void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}</code></pre><p>我们在示例程序中使用了<code>gl.TEXTURE_2D</code>这种二维纹理，所以在片元着色器中定义的<code>uniform</code>变量的数据类型应该为<code>sampler2D</code>，除此之外还有<code>samplerCube</code>（这种数据类型对应<code>gl.TEXTURE_CUBE_MAP</code>）。</p><p>在<code>initTexture</code>函数中，我们获取到了<code>uniform</code>变量<code>u_Sampler</code>的存储地址，并将其作为参数传给<code>loadTexture</code>函数。我们必须通过指定<strong>纹理单元编号</strong>（即<code>gl.TEXTUREn</code>中的<code>n</code>）将纹理传给<code>u_Sampler</code>。因为我们绑定到了<code>gl.TEXTURE0</code>上，所以调用<code>gl.uniform1i</code>时，第二个参数设为0：</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-052b8e7a7452088095fdb3177eae18c3_720w.jpg" class="kg-image" alt="v2-052b8e7a7452088095fdb3177eae18c3_720w" width="600" height="400" loading="lazy"></figure><h3 id="--13">从顶点着色器向片元着色器传输纹理坐标</h3><p>我们是通过<code>attribute</code>变量<code>a_TexCoord</code>接收顶点的纹理坐标，所以将数据赋值给<code>varying</code>变量<code>v_TexCoord</code>并将纹理坐标传入片元着色器是行得通的。</p><p>剩下的工作就是，根据片元的纹理坐标，从纹理图像上抽取出纹素的颜色，然后涂到当前的片元上。</p><h3 id="--14">在片元着色器中获取纹理像素颜色</h3><pre><code class="language-glsl">gl_FragColor = texture2D(u_Sampler, v_TexCoord);</code></pre><p>我们使用<code>GLSL ES</code>内置函数<code>texture2D()</code>来抽取纹素颜色，该函数使用起来十分方便，只需要传入两个参数——纹理单元编号和纹理坐标，就可以获取到纹理上的像素颜色。</p><p>纹理放大和缩小方法的参数将决定<code>WebGL</code>系统将以何种方式内插出片元。我们将<code>texture2D()</code>函数的返回值赋给了<code>gl_FragColor</code>变量，然后片元着色器就将当前片元涂成这个颜色。最后，纹理图像就被映射到了图形上，并最终被画了出来。</p><p>下面让我们打开页面看一下效果（因为跨域原因，大家需在本地启用<code>http</code>服务器）：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-b9e388d7c6478eaaa2c3a27e27e25578_720w.jpg" class="kg-image" alt="v2-b9e388d7c6478eaaa2c3a27e27e25578_720w" width="600" height="400" loading="lazy"></figure><p>怎么漆黑一片呢？</p><p>别急，先来仔细看一下<code>console</code>信息：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-d7171b45183160d204267552785bfad0_720w.png" class="kg-image" alt="v2-d7171b45183160d204267552785bfad0_720w" width="600" height="400" loading="lazy"></figure><p>会发现<code>warning</code>中有说到我们的纹理图像无法渲染，可能因为图片尺寸不是2的整数次方，那么让我们把图片裁剪成<code>256 x 256</code>大小的再试一下呢？</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-1b1cc6b4ca1db636e0b354c51d2d268e_720w.jpg" class="kg-image" alt="v2-1b1cc6b4ca1db636e0b354c51d2d268e_720w" width="600" height="400" loading="lazy"></figure><p>完美 我们目前使用的都是<code>WebGL1 .0</code>的特性，在<code>WebGL 2.0</code>中支持了非2的整数次方大小的纹理图像！</p><p>我们已经成功展示出一张图片了，但是在<code>WebGL</code>系统中有多个纹理单元，所以我们可以展示多张图片，比如我给悟空图片上再加一张图片：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-14e1a85d243407b9d4094f4c3c99887d_720w.jpg" class="kg-image" alt="v2-14e1a85d243407b9d4094f4c3c99887d_720w" width="600" height="400" loading="lazy"></figure><p>这里就不详细描述了，给一点提示：片元着色器中<code>texture2D</code>内置函数返回的是<code>vec4</code>类型的<code>color</code>，而对于两张图片的重叠部分：</p><pre><code class="language-glsl">gl_FragColor = color0 * color1;</code></pre><p>可以通过以上方式计算得出！</p><h2 id="--15">结束语</h2><p>纹理部分内容较多，大家可以慢慢学习一下，再次总结一下主要分为四步：</p><ol><li>准备纹理图像；</li><li>为几何图形配置映射方式；</li><li>加载纹理图像：</li><li>翻转坐标轴（<code>gl.pixelStorei</code>）；</li><li>激活纹理单元（<code>gl.activeTexture</code>）；</li><li>绑定纹理对象（<code>gl.bindTexture</code>）；</li><li>配置纹理参数（<code>gl.texParameteri</code>）；</li><li>配置纹理图像（<code>gl.texImage2D</code>）；</li><li>将纹理单元传给着色器。</li><li>在FS中抽取纹素并赋给片元（<code>texture2D</code>）。</li></ol><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-7cda0ca7c71e0e7c8badecd5f0b98c84_720w.jpg" class="kg-image" alt="v2-7cda0ca7c71e0e7c8badecd5f0b98c84_720w" width="600" height="400" loading="lazy"></figure><p>有趣的纹理映射介绍到这里啦，后续会出更多好玩并且有用的文章分享给大家，欢迎关注公众号：Refactor，感谢阅读！</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ ThreeJS 源码剖析之 Renderer（一） ]]>
                </title>
                <description>
                    <![CDATA[ 前言 当学习一种新技术时，大部分情况都会首选其官方文档，而文档中能最快让我们上手的章节便是 Getting Started。ThreeJS  也不例外，我们进入到其对应的 Getting Started 页面后，便能看到 Creating the scene 的代码仅有下面寥寥几行： var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); var renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth, window.innerHeight ); document.body.appendChild( renderer.domElement ); 再滚动至最下方，各位会看到完整的 Demo： // ... var animate = function ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/threejs-renderer/</link>
                <guid isPermaLink="false">609408d70998fd05ae8c84bb</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Thu, 06 May 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/marc-mintel-Q-ioK6NPFos-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-"><strong>前言</strong></h2><p>当学习一种新技术时，大部分情况都会首选其官方文档，而文档中能最快让我们上手的章节便是 <em>Getting Started</em>。<em>ThreeJS</em> 也不例外，我们进入到其对应的 <em>Getting Started</em> 页面后，便能看到 <em>Creating the scene</em> 的代码仅有下面寥寥几行：</p><pre><code class="language-js">var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
</code></pre><p>再滚动至最下方，各位会看到完整的 Demo：</p><pre><code class="language-js">// ...
var animate = function () {
  requestAnimationFrame( animate );

  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;

  renderer.render( scene, camera );
};

animate();
</code></pre><p>阅读完简单的十几行代码之后，各位可能会发现这个 Demo 中在一直调用 <code>animate()</code> 方法，而此方法中我们修改了立方体的旋转角度，并执行了这一行代码：<code>renderer.render( scene, camera )</code>。假如你没有接触过图形学或 <em>WebGL</em>，仅凭语义会翻译成：“渲染器渲染场景和摄像机”。没错，这一行的作用正是如此！</p><p>假如我们将上面那句话拆解一下：“渲染器 - 渲染 - 场景和摄像机”，这句话正符合了我们的 “主 - 谓 - 宾”的结构。那么就可见主语（即渲染器）是个很重要的存在！所以此系列的第一篇文章我们就从渲染器 —— <em>Renderer</em> 说起。</p><h2 id="renderer-"><strong>Renderer⚙️</strong></h2><p>将 <em>ThreeJS</em> 文档目录滚动至 <em>Renderers</em> 一栏，可以看到有 <em>WebGLMultisampleRenderTarget</em> 、<em>WebGLRenderer</em> 、<em>WebGL1Renderer</em> 、<em>WebGLRenderTarget</em> 和 <em>WebGLCubeRenderTarget</em> 五类渲染器，而我们就聚焦于 <em>WebGLRenderer</em> 这个渲染器即可，稍后会给大家介绍一下为何会有 <em>WebGL1Renderer</em>，至于其他三种渲染器，我们就不先讨论（其实我也没用过，后续了解了咱们再聊）</p><h3 id="webgl1renderer"><strong>WebGL1Renderer</strong></h3><p>假如你对 <em>WebGL</em> 稍有了解，那么就会知道其实 <em>WebGL</em> 有 <em>1.0</em> 和 <em>2.0</em> 两个版本，那么两个版本分别是基于 <em>OpenGL ES 2.0</em> 和 <em>OpenGL ES 3.0</em> 的，至于不基于 <em>OpenGL ES 1.0</em> 是因为 <em>1.0</em> 是固定管线的，<em>2.0</em> 和 <em>3.0</em> 是可编程管线。所谓固定管线就是我们给管线配置相应参数和开关即可，可编程管线的可编程部分就是我们所熟知的 <em>顶点着色器</em> 和 <em>片元着色器</em>。</p><p>扯远了，再说回 <em>WebGL</em>，这俩版本有什么差别呢？大家可以看<a href="https://chinese.freecodecamp.org/news/threejs-renderer/WebGL%20What's%20New%E2%80%8Bwebgl2fundamentals.org">这个链接</a>。</p><p>简单概括一下就是多了更多纹理格式、内置函数、3D 纹理贴图，同时还支持了非 2 的整数次方大小的图片。同时，<em>WebGL 2.0</em> 与 <em>WebGL 1.0</em> 在对浏览器的兼容性上有很大的差异，以 <em>Chrome</em> 为例，<em>WebGL 1.0</em> 兼容 <em>9</em> 及以上的 <em>Chrome</em> 版本，而 <em>WebGL 2.0</em> 则兼容 <em>56</em> 及以上的 <em>Chrome</em> 版本，对浏览器的兼容性的巨大差异就有极大的可能让一些陈旧的 <em>WebGL 1.0</em> 的系统崩溃，故 <em>ThreeJS</em> 提供了 <em>WebGL1Renderer</em> 来进行适配兼容。<em>ThreeJS</em> 已经从 <em>r118</em> 版本就全面升级到 <em>WebGL 2.0</em> 了，所以如果你的系统使用的 <em>ThreeJS</em> 版本低于 <em>r118</em>，并且准备升级到最新的 <em>r120</em> 版本，为了避免程序挂掉，你需要将 <em>Renderer</em> 替换成 <em>WebGL1Renderer</em>！如何查看最新 <em>ThreeJS</em> 版本，请见下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-1.png" class="kg-image" alt="image-1" width="600" height="400" loading="lazy"></figure><p>而查看当前项目使用的 <em>ThreeJS</em> 版本请见 <em>package.json</em> 中的 <em>ThreeJS</em> 依赖版本的 <em>MINOR</em> 版本号即可，比如：<em>three: 0.120.0</em> 就表明是 <em>r120</em> 版本，<em>href="<a href="https://link.zhihu.com/?target=https%3A//docs.npmjs.com/about-semantic-versioning" rel="nofollow noreferrer">https://docs.npmjs.com/about-semantic-versioning</a>"&gt;npm 语义化版本请见官方文档。</em></p><p><strong>WebGLRenderer</strong></p><p>说回我们的主角 <em>WebGLRenderer</em>，进入到源码（<code>src/renderers/WebGLRenderer.js</code>）后可以看到头部有很多参数初始化的操作，在此就不一一介绍，关于参数含义大家可参考文档：<a href="https://link.zhihu.com/?target=https%3A//threejs.org/docs/index.html%23api/en/renderers/WebGLRenderer" rel="nofollow noreferrer">了解参数含义请阅读 Constructor 部分</a>。其中需要提一下的是，大家可能会发现这条语句：</p><pre><code class="language-js">const _canvas = parameters.canvas !== undefined ?
  parameters.canvas :
  document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' );
</code></pre><p>这里使用的是 <code>createElementNS</code> 并非 <code>createElement</code>，二者区别及用途请阅读：</p><p><a href="https://link.zhihu.com/?target=http%3A//zhangwenli.com/blog/2017/07/26/createelementns/">Document.createElementNS: What's the difference and why we need it​zhangwenli.com</a></p><h3 id="getcontext"><strong>getContext</strong></h3><p>当我们 <em>new</em> 一个 <em>Renderer</em> 时，其内部会最先进入这下面的语句：</p><pre><code class="language-js">if ( _gl === null ) {
  const contextNames = [ 'webgl2', 'webgl', 'experimental-webgl' ];
  if ( _this.isWebGL1Renderer === true ) {
    contextNames.shift();
  }
  _gl = getContext( contextNames, contextAttributes );
  if ( _gl === null ) {
    if ( getContext( contextNames ) ) {
      throw new Error( 'Error creating WebGL context with your selected attributes.' );
    } else {
      throw new Error( 'Error creating WebGL context.' );
    }
  }
}
</code></pre><p>首先当 <em>WebGL</em> 上下文不存在时，<em>Renderer</em> 内部列出了浏览器所提供的所有 <em>WebGL</em> 上下文名称，如若是 <em>WebGL1Renderer</em>，则删去 <em>WebGL 2.0</em> 的上下文名称，然后调用 <code>getContext</code> 方法获取上下文。<code>getContext</code> 方法实现也十分简单：</p><pre><code class="language-js">function getContext( contextNames, contextAttributes ) {
  for ( let i = 0; i &lt; contextNames.length; i ++ ) {
    const contextName = contextNames[ i ];
    const context = _canvas.getContext( contextName, contextAttributes );
    if ( context !== null ) return context;
  }
  return null;
}
</code></pre><p>遍历上方列举的上下文名称，如果获取上下文则返回，否则返回 <code>null</code>。</p><h3 id="initcontext"><strong>initContext</strong></h3><p>当获取到上下文后，紧接着调用的就是 <code>initGLContext</code> 方法了：</p><pre><code class="language-js">function initGLContext() {
  extensions = new WebGLExtensions( _gl );
  capabilities = new WebGLCapabilities( _gl, extensions, parameters );
  // 若不是 WebGL 2.0，则需额外获取以下 extensions
  if ( capabilities.isWebGL2 === false ) {
    // ...
  }
  extensions.get( 'OES_texture_float_linear' );
  
  utils = new WebGLUtils( _gl, extensions, capabilities );

  state = new WebGLState( _gl, extensions, capabilities );
  state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor() );
  state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor() );

  // ...
}
</code></pre><p>用导游的话来讲：“首先映入我们眼帘的是 <code>new WebGLExtensions</code> 和 <code>new WebGLCapabilities</code> 两条语句”，<code>WebGLExtensions</code> 的作用是判断某个 <code>extension</code> 是否存在，以及获取指定的 <code>extension</code> 并 <em>全量返回</em>，而 <code>WebGLCapabilities</code> 则是获取当前 <em>WebGL</em> 系统的一些属性，如：支持的最大精度、是否是 <em>WebGL 2.0</em> 以及所支持的最大贴图大小等。而下面的 <code>WebGLUtils</code> 中只有一个 <code>convert</code> 方法，该方法主要是 <em>将自定义的类型转换成 WebGL 内置的类型</em>。</p><p>紧随其后的就是 <code>WebGLState</code>，别看它名字很短，但是它却很重要！通过这个 <em>state</em>，我们可以设置视口大小、设置混合、设置材质、绑定纹理等。其余的内容就先省略，后续有必要再讲，先让各位知道 <em>Renderer</em> 内部的工作流程。</p><p>调用完 <code>initGLContext</code> 方法后就有一句：</p><pre><code class="language-js">const xr = new WebXRManager( _this, _gl );
</code></pre><p><em>WebXR</em> 是一组支持将渲染3D场景用来呈现虚拟世界（虚拟现实，也称作 <em>VR</em>）或将图形图像添加到现实世界（增强现实，也称作 <em>AR</em>）的标准，详情请查看 <a href="https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/WebXR_Device_API" rel="nofollow noreferrer">MDN文档</a>。</p><h3 id="animation"><strong>animation</strong></h3><p>调用完 <code>initContext</code> 后，就来到了 <code>animation</code> 环节：</p><pre><code class="language-js">const animation = new WebGLAnimation();
animation.setAnimationLoop( onAnimationFrame );
if ( typeof window !== 'undefined' ) animation.setContext( window );
</code></pre><p><code>animation</code> 的调用更是只有这简单的 <em>3</em> 行语句，<code>WebGLAnimation</code> 的类型如下：</p><pre><code class="language-js">class WebGLAnimation {
  start(): void;
  stop(): void;
  setAnimationLoop( callback: Function ): void;
  setContext( value: Window ): void;
}
</code></pre><p>当调用 <code>setAnimationLoop</code> 时，其实就是将传入的 <code>callback</code> 赋给 <code>WenGLAnimation</code> 内部的 <code>animationLoop</code> 变量，当调用 <code>start</code> 方法时，就调用 <code>window</code> 的 <code>requestAnimationFrame</code> 方法。所以 <em>ThreeJS</em> 内部的动画也是不断调用 <code>requestAnimationFrame</code> 来实现的，呵！凡人！</p><p>而 <code>setContext</code> 则是将 <code>context</code> 设为 <code>window</code>，至于 <code>stop</code> 方法大家应该也能猜到里面如何停止动画的了吧！</p><h2 id="--1"><strong>结束语</strong></h2><p>其实 <em>WebGLRenderer</em> 大约 <em>2000</em> 行代码，但其中绝大部分是函数声明供我们后续的使用。回顾本文，大家就会发现当我们 <em>new</em> 一个 <em>Renderer</em> 时 <em>ThreeJS</em> 只不过是帮我们初始化了 <code>context</code>，并设置了一下 <code>animation</code>，其余的函数都是后续才会用到。故初始化一个 <em>Renderer</em> 也没什么故弄玄虚的！</p><p>既然内部有这么多的函数让我们后续使用，那么下篇文章就来讲讲我们常用的 <em>Renderer</em> 中的方法。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 你距离 WebGL 只差一点！ ]]>
                </title>
                <description>
                    <![CDATA[ 在本文中，我会通过绘制一个点，带领大家走进WebGL的世界。并且本文不会涉及 D3 / ThreeJS 等 WebGL 库，就用原生的WebGL API 绘制一个点！ 前言 首先，WebGL并不是一门语言，它是一个标准，它是在OpenGL ES的基础上所建立的一套适用于浏览器的图形学标准；而OpenGL ES则是OpenGL 的一个特殊版本（套娃警告 ），ES版本被广泛的应用于手机、家用游戏机等设备。想了解更多关于WebGL标准内容的小伙伴可以进入Khronos Group [https://link.zhihu.com/?target=https%3A//www.khronos.org/webgl/]的网站自行浏览。 WebGL的开发与我们普通的前端开发并没有什么太大差异，一个浏览器的网页一般是由：HTML、 JavaScript、渲染引擎等部分组成，如果我们要开发WebGL 的话，还需要什么呢？让我们来思考一下，我们在高中学习几何的时候老师讲过“点动成线，线动成面，面动成体”，那我们就以最基础的点为例，首先点 有什么属性么？在屏幕中的位置、点的颜色、点的大小，我们如何定义一个点 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-webgl-through-drawing-a-dot/</link>
                <guid isPermaLink="false">5f96cae55f583f0565090bfb</guid>
                
                    <category>
                        <![CDATA[ WebGL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Sat, 03 Apr 2021 08:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/10/azharul-islam-9LMGWHqUwnc-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在本文中，我会通过绘制一个点，带领大家走进<code>WebGL</code>的世界。并且本文不会涉及 <code>D3 </code>/ <code>ThreeJS</code> 等 <code>WebGL</code> 库，就用原生的<code>WebGL API</code>绘制一个点！</p><h2 id="-"><strong>前言</strong></h2><p>首先，<code>WebGL</code>并不是一门语言，它是一个标准，它是在<code>OpenGL ES</code>的基础上所建立的一套适用于浏览器的图形学标准；而<code>OpenGL ES</code>则是<code>OpenGL</code>的一个特殊版本（套娃警告 ），<code>ES</code>版本被广泛的应用于手机、家用游戏机等设备。想了解更多关于<code>WebGL</code>标准内容的小伙伴可以进入<a href="https://link.zhihu.com/?target=https%3A//www.khronos.org/webgl/" rel="nofollow noreferrer">Khronos Group</a>的网站自行浏览。</p><p><code>WebGL</code>的开发与我们普通的前端开发并没有什么太大差异，一个浏览器的网页一般是由：HTML、 JavaScript、渲染引擎等部分组成，如果我们要开发<code>WebGL</code>的话，还需要什么呢？让我们来思考一下，我们在高中学习几何的时候老师讲过“点动成线，线动成面，面动成体”，那我们就以最基础的<strong>点</strong>为例，首先<strong>点</strong>有什么属性么？在屏幕中的位置、点的颜色、点的大小，我们如何定义一个<strong>点</strong>的这些属性呢？这就要引入<code>GLSL ES(OpenGL Shader Language ES)</code>（后称着色器）了，着色器的写法与C语言语法有些相似，从名字也能看出<code>WebGL</code>与<code>OpenGL ES</code>是有“血缘关系”的！其次，我们还需要的就是浏览器厂商基于<code>WebGL</code>标准提供的<code>API</code>。</p><p><code>WebGL</code>并不像<code>OpenGL</code>一样有繁琐的环境配置的流程，也没有对系统的要求，只要有一个支持<code>WebGL</code>的浏览器即可！</p><blockquote>本次我们使用字符串的形式编写着色器，暂时不新建单独的着色器文件​​</blockquote><h2 id="--1"><strong>给这个“点”一点自由的空间</strong></h2><p>为了使用浏览器提供的<code>WebGL</code>接口，我们需要使用<code>&lt;canvas&gt;</code>来获取<code>WebGL</code>上下文：</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;title&gt;Point&lt;/title&gt;
&lt;/head&gt;
&lt;body onload="main()" style="padding: 0; margin: 0;"&gt;
  &lt;canvas id="webgl" width="600" height="400"&gt;
    您使用的浏览器不支持 WebGL！
  &lt;/canvas&gt;
  &lt;script&gt;
    function main() {
      // get canvas element
      const canvas = document.getElementById("webgl");
      const gl = canvas.getContext('webgl');
    }
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p><code>gl</code>就是我们所获取到的<code>WebGL</code>渲染的上下文 让我们给画布填充个背景色吧：</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;!-- ... --&gt;
  &lt;script&gt;
    function main() {
      // ...
      const gl = canvas.getContext('webgl');
      
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);
    }
  &lt;/script&gt;
  &lt;!-- ... --&gt;
&lt;/html&gt;</code></pre><p>这样我们要绘制点的画布就拥有了浩瀚宇宙一般深邃的黑色:) 喝点庆祝一下</p><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-214a4e5c2c9bc82b80375a13694a4a0e_720w.jpg" class="kg-image" alt="v2-214a4e5c2c9bc82b80375a13694a4a0e_720w" width="600" height="400" loading="lazy"></figure><p><br>解释一下<code>gl.clearColor</code>方法是设置清除画布的背景色，形式是<code>RGBA</code>；<code>gl.clear</code>则是调用清除画布的方法，可传递的参数<code>gl.COLOR_BUFFER_BIT</code>是个什么呢 其实该方法继承自<code>OpenGL</code>，<code>OpenGL</code>是基于多缓冲区模型的，清空绘图区域实际上是在清空颜色缓冲区，传递参数<code>gl.COLOR_BUFFER_BIT</code>是在告诉<code>WebGL</code>清空颜色缓冲区；除此之外还有深度缓冲区以及模板缓冲区，可<a href="https://link.zhihu.com/?target=https%3A//books.google.com.hk/books%3Fid%3D9H10DwAAQBAJ%26pg%3DSA16-PA39%26lpg%3DSA16-PA39%26dq%3Dcolor%2Bbuffer%2Bbit%26source%3Dbl%26ots%3DmTRM9iN19f%26sig%3DACfU3U2QEwxLDrB9I4rnjjwmLOZm07E-5A%26hl%3Dzh-CN%26sa%3DX%26ved%3D2ahUKEwjtk5f5zNjoAhW1JaYKHQLZAQEQ6AEwDXoECA0QPg%23v%3Donepage%26q%3Dcolor%2520buffer%2520bit%26f%3Dfalse" rel="nofollow noreferrer">查看此了解</a>。</p><h2 id="--2"><strong>让距离近一“点”</strong></h2><p>下面我们开始绘制<strong>点</strong> 在前面我们分析道一个点有<strong>位置、颜色及大小</strong>三个属性，下面我们将编写着色器给深邃的画布增添一点色彩</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;!-- ... --&gt;
  &lt;script&gt;
    function main() {
      // ...
      const VertexShader = `
        void main() {
          gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
          gl_PointSize = 10.0;
        }
      `;
      const FragmentShader = `
        void main() {
          gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
        }
      `;
    }
  &lt;/script&gt;
  &lt;!-- ... --&gt;
&lt;/html&gt;</code></pre><p>上面我们定义了<code>VertexShader</code>和<code>FragmentShader</code>，在<code>WebGL</code>中有两种着色器分别是：顶点着色器和片元着色器：</p><ul><li>顶点着色器：用来描述顶点的特性的程序，比如位置、大小等。顶点是指二维或三维空间中的一个点，比如二维图形或三维图形的顶点或交点；</li><li>片元着色器：也称像素着色器，进行逐片的处理过程比如光照。片元可以理解为像素。</li></ul><p>同时，每个着色器都有一个<code>main()</code>方法，并且该方法不能指定参数，每行语句结束之后必须有分号！！！ <code>gl_Position</code>、<code>gl_PointSize</code>和<code>gl_FragColor</code>三个变量则是着色器内置的变量，其中<code>gl_PointSize</code>可以不赋值，默认值为1.0。各位注意到，上面赋值语句中我们给的值是0.0而不是0，这是因为这些内置变量是有其变量类型的：</p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-4d4177718230a6815b383ba6eb7510e9_720w.jpg" class="kg-image" alt="v2-4d4177718230a6815b383ba6eb7510e9_720w" width="600" height="400" loading="lazy"></figure><p><br>问：明明一个点的坐标只有(x, y, z)，为什么要传4个值呢？<br>答：这里使用的是齐次坐标的形式，了解齐次坐标可查看我上篇文章<a href="https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/-aZ3tUgMv0uGOmbov-RRhw" rel="nofollow noreferrer">《客官，进来看看图形的几何变换？》</a>。<br>问：使用上面定义的顶点着色器和片元着色器分几步呢？<br>答：分三步！第一步，创建着色器；第二步，创建着色器程序；第三步，在WebGL上下文中使用着色器程序。</p><h2 id="1-">1️⃣创建着色器</h2><p>为了方便使用我把创建着色器的步骤抽取了一个<code>createShader()</code>方法：</p><pre><code class="language-js">function createShader (gl, type, source) {
  const shader = gl.createShader(type);
  if (shader == null) {
    console.warn('无法创建着色器');
    return null;
  }
​
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
​
  const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (!compiled) {
    console.log('编译着色器失败： ' + gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
​
  return shader;
}
</code></pre><p><code>gl.shaderSource</code>是将<code>gl.createShader</code>创建的着色器的<code>source</code>设置为我们定义的<code>VertextShader</code>或<code>FragmentShader</code>，剩下的就不解释了，函数名都很表意:)</p><h2 id="2-">2️⃣创建着色器程序</h2><p>也为了更简洁，创建着色器程序的步骤也抽成了<code>createProgram()</code>方法：</p><pre><code class="language-js">function createProgram (gl, vshader, fshader) {
  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vshader);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fshader);
  if (!vertexShader || !fragmentShader) {
    return null;
  }
​
  const program = gl.createProgram();
  if (!program) {
    return null;
  }
​
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
​
  gl.linkProgram(program);
​
  const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {
    console.warn('Link 着色器程序失败： ' + gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    gl.deleteShader(fragmentShader);
    gl.deleteShader(vertexShader);
    return null;
  }
  return program;
}
</code></pre><p><code>gl.attachShader</code>是将创建好的着色器attach到我们着色器程序上，然后调用<code>gl.linkProgram</code>方法将<code>program</code>整合起来。</p><h2 id="3-">3️⃣在上下文中使用着色器程序</h2><pre><code class="language-js">function initShaders(gl, vshader, fshader) {
  const program = createProgram(gl, vshader, fshader);
  if (!program) {
    console.warn('创建着色器程序失败！');
    return false;
  }
​
  gl.useProgram(program);
  gl.program = program;
​
  return true;
}
</code></pre><p>这里就很简单啦，就不做过多介绍了！然后在<code>main()</code>中调用此方法初始化着色器：</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;!-- ... --&gt;
  &lt;script&gt;
    function main() {
      const canvas = document.getElementById('webgl');
      const gl = canvas.getContext('webgl');
​
      if (!initShaders(gl, VertexShader, FragmentShader)) {
        return alert('初始化着色器失败');
      }
​
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS, 0, 1);
    }
    // createShader
    // createProgram
    // initShaders
  &lt;/script&gt;
  &lt;!-- ... --&gt;
&lt;/html&gt;</code></pre><p><code>gl.drawArrays</code>的第一个参数是指定绘制方式，第二个参数是从哪个顶点开始绘制，第三个参数是指定绘制要用到多少个顶点。这样我们就能在黑色的画布上的正中心看到一个蓝色的点：</p><figure class="kg-card kg-image-card"><img src="https://pic4.zhimg.com/80/v2-c7d3dbf43eba3912dd22e39a675f3afb_720w.jpg" class="kg-image" alt="v2-c7d3dbf43eba3912dd22e39a675f3afb_720w" width="600" height="400" loading="lazy"></figure><p>但是，小朋友你是否有很多的问号？<br></p><figure class="kg-card kg-image-card"><img src="https://pic2.zhimg.com/80/v2-53a9c506e5838e2b4b1a687f5d399399_720w.jpg" class="kg-image" alt="v2-53a9c506e5838e2b4b1a687f5d399399_720w" width="600" height="400" loading="lazy"></figure><p><br>明明定义的点的位置在<code>(0, 0, 0)</code>，为什么点会出现在<code>&lt;canvas&gt;</code>的正中央呢？<code>WebGL</code>相对于<code>&lt;canvas&gt;</code>的位置如下图：</p><figure class="kg-card kg-image-card"><img src="https://pic1.zhimg.com/80/v2-be538701cc036ed336cdd4255a5f3e88_720w.jpg" class="kg-image" alt="v2-be538701cc036ed336cdd4255a5f3e88_720w" width="600" height="400" loading="lazy"></figure><p>中间的是<code>WebGL</code>相对于<code>&lt;canvas&gt;</code>的坐标，而<code>canvas</code>的坐标则是相对于屏幕的！<code>WebGL</code>相对于<code>&lt;canvas&gt;</code>的坐标并不是绝对的像素值，而是相对的<code>[-1.0, 1.0]</code>。 举个例子：我们展示的点在<code>canvas</code>的正中央，如果我们把点的坐标设置为<code>(1.0, 0.0, 0.0, 1.0)</code>，那么点就会出现在<code>canvas</code>的最右侧，同理设置为<code>(-1.0, 1.0, 0.0, 1.0)</code>，点则展示在<code>canvas</code>的左上角:P</p><h2 id="--3"><strong>渲染这个点经历了什么？</strong></h2><figure class="kg-card kg-image-card"><img src="https://pic3.zhimg.com/80/v2-68aa4b9806166affbbd2d1382b361e8a_720w.jpg" class="kg-image" alt="v2-68aa4b9806166affbbd2d1382b361e8a_720w" width="600" height="400" loading="lazy"></figure><h2 id="--4"><strong>就这么结束了？</strong></h2><p>怎么可能就这么结束！让我们给绘制<strong>点</strong>的程序升级一下，现在我们的位置、大小都是在着色器中定义好的。当然<code>WebGL</code>也为我们提供了方法让我们可以从外部传入相应参数值。让我们对着色器改造一下：</p><pre><code class="language-js">const VertexShader = `
  attribute vec4 a_Position;
  void main() {
    gl_Position = a_Position;
    gl_PointSize = 10.0;
  }
`;
</code></pre><p><code>attribute</code>是一种<code>GLSL SE</code>变量，被用来从外部向顶点着色器内传数据，<strong>只有顶点着色器可以使用</strong>；同时还有一种变量类型<code>uniform</code>，<code>uniform</code>变量传输的是对于所有顶点都相同（或与顶点无关）的数据。上面是着色器代码中，我们将从外部获取到的<code>a_Position</code>和<code>a_PointSize</code>分别赋值给<code>gl_Position</code>和<code>gl_PointSize</code>。怎么通过JavaScript向着色器的<code>attribute</code>变量传值呢？</p><pre><code class="language-js">function main () {
  // ...
  if (!initShaders(gl, VertexShader, FragmentShader)) {
    return alert('初始化着色器失败');
  }
​
  const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  gl.vertexAttrib3f(a_Position, 1.0, 0.0, 0.0);
  // ...
}
</code></pre><p>使用<code>vertextAttrib3f</code>方法就可以将使用<code>getAttribLocation</code>获取到的<code>attribute</code>变量赋值，<code>vertexAttrib3f</code>方法会将齐次坐标的最后一个值默认赋值为<code>1.0</code>，当然使用<code>vertexAttrib4f</code>也是可以的:)</p><h2 id="--5"><strong>再加点功能</strong></h2><p>当我在<code>canvas</code>上点击的时候，就在点击<code>canvas</code>的地方展示一个点，这就需要我们给<code>canvas</code>绑定方法了：</p><pre><code class="language-js">canvas.onmousedown = function (e) {
  click(e, gl, canvas, a_Position);
};
</code></pre><p>在此就不给详细代码了，<code>canvas</code>绑定事件方式如上，并简单说一下思路：当点击之后获取鼠标在<code>canvas</code>点击的坐标值；然后将坐标转换为<code>WebGL</code>相对于<code>canvas</code>的<code>[-1.0, 1.0]</code>形式的坐标；然后清空画布，在重新绘制点。坐标转换的代码如下：</p><pre><code class="language-js">let x = e.clientX;
let y = e.clientY;
const rect = e.target.getBoundingClientRect();
​
x = (x - rect.left - canvas.width / 2) / (canvas.width / 2);
y = (canvas.height / 2 - y + rect.top) / (canvas.height / 2);
</code></pre><p>比如还能再给每个点设置不同的颜色，<strong>提示：使用 uniform 变量。</strong></p><h2 id="--6"><strong>结束语</strong></h2><p>使用原生<code>WebGL</code>绘制一个“简单的点”就讲到这里啦:)我自己也在不断的学习中，后续会出更多关于<code>WebGL</code>的文章。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
