<?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[ WebGL - 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[ WebGL - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 16 Jun 2026 17:18:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/webgl/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <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[ 你距离 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>
