From 1f273821b15d0a3153bbd0821401365dd35f93a0 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sat, 21 Jun 2025 08:20:34 +0800 Subject: [PATCH 01/22] translate Chinese toc --- .gitignore | 1 + webgl/lessons/zh_cn/webgl-points-lines-triangles.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 33eaacbd5..275e85707 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules out package-lock.json webgl2fundamentals.check.json +.idea diff --git a/webgl/lessons/zh_cn/webgl-points-lines-triangles.md b/webgl/lessons/zh_cn/webgl-points-lines-triangles.md index 8fe4b6791..ef3f26c97 100644 --- a/webgl/lessons/zh_cn/webgl-points-lines-triangles.md +++ b/webgl/lessons/zh_cn/webgl-points-lines-triangles.md @@ -1,6 +1,6 @@ Title: WebGL2 点、线段与三角形 Description: WebGL2 中点、线段与三角形的绘制详解 -TOC: Points, Lines, and Triangles +TOC: 点、线段与三角形 这个网站的大部分内容都是用三角形来绘制所有图形的。可以说,这是 99% 的 WebGL 程序的常规做法。不过,为了内容的完整性,我们再来讨论一些其他情况。 From a8e236990e72923dca900b15429904d9d95105cf Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sat, 21 Jun 2025 08:25:06 +0800 Subject: [PATCH 02/22] translate toc of webgl-multiple-views.md --- webgl/lessons/zh_cn/webgl-multiple-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgl/lessons/zh_cn/webgl-multiple-views.md b/webgl/lessons/zh_cn/webgl-multiple-views.md index 4631b99af..675beff3a 100644 --- a/webgl/lessons/zh_cn/webgl-multiple-views.md +++ b/webgl/lessons/zh_cn/webgl-multiple-views.md @@ -1,6 +1,6 @@ Title: WebGL2 多视图与多画布 Description: 绘制多个视图 -TOC: Multiple Views, Multiple Canvases +TOC: 多视图与多画布 本文假设你已经阅读过[码少趣多](webgl-less-code-more-fun.html)一文, 因为我们将使用其中提到的库来简化示例。 From b8846e69ca0270f0cd04e44becb97ddf7d10ce16 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sat, 21 Jun 2025 19:11:55 +0800 Subject: [PATCH 03/22] add Chinese webgl-drawing-without-data.md --- .../zh_cn/webgl-drawing-without-data.md | 489 ++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-drawing-without-data.md diff --git a/webgl/lessons/zh_cn/webgl-drawing-without-data.md b/webgl/lessons/zh_cn/webgl-drawing-without-data.md new file mode 100644 index 000000000..45d78f074 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-drawing-without-data.md @@ -0,0 +1,489 @@ +Title: WebGL2 无数据绘制 +Description: 创意编程 - 无数据绘制技术 +TOC: 无数据绘制 + +本文假设您已阅读从[基础教程](webgl-fundamentals.html)开始的多篇相关文章。若尚未阅读,请先查阅。 + +在[WebGL2 最小的程序](webgl-smallest-programs.html)一文中, +我们演示了极简代码的绘制示例。本文将实现无数据绘制。 + +传统WebGL应用将几何数据存入缓冲区,通过属性(attribute)从缓冲区提取顶点数据到着色器, +最终转换为裁剪空间坐标。 + +需注意**传统**一词仅表示惯例做法,并非强制要求。 +WebGL仅关注顶点着色器向 `gl_Position` 赋值裁剪空间坐标,并不关心如何实现。 + +GLSL ES 3.0为顶点着色器提供了 `gl_VertexID` 特殊变量,可对顶点进行计数。 +我们将基于此变量计算圆形顶点坐标,实现无数据绘制。 + +```glsl +#version 300 es +uniform int numVerts; + +#define PI radians(180.0) + +void main() { + float u = float(gl_VertexID) / float(numVerts); // 0 到 1 + float angle = u * PI * 2.0; // 0 到 2π + float radius = 0.8; + + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + + gl_Position = vec4(pos, 0, 1); + gl_PointSize = 5.0; +} +``` + +上述代码逻辑应较为直观: `gl_VertexID` 将从0开始计数至我们指定的顶点数量(通过 `numVerts` 传递)。基于此生成圆形顶点坐标。 + +若止步于此,圆形将显示为椭圆——因为裁剪空间在画布横纵方向上采用归一化坐标(-1到1范围)。 +通过传入分辨率参数,可解决横向-1到1与纵向-1到1实际表示空间比例不一致的问题。 + + +```glsl +#version 300 es +uniform int numVerts; ++uniform vec2 resolution; + +#define PI radians(180.0) + +void main() { + float u = float(gl_VertexID) / float(numVerts); // goes from 0 to 1 + float angle = u * PI * 2.0; // goes from 0 to 2PI + float radius = 0.8; + + vec2 pos = vec2(cos(angle), sin(angle)) * radius; + ++ float aspect = resolution.y / resolution.x; ++ vec2 scale = vec2(aspect, 1); + ++ gl_Position = vec4(pos * scale, 0, 1); + gl_PointSize = 5.0; +} +``` + +而片段着色器仅需输出一个纯色。 + +```glsl +#version 300 es +precision highp float; + +out vec4 outColor; + +void main() { + outColor = vec4(1, 0, 0, 1); +} +``` + +在JavaScript初始化阶段,我们将编译着色器并获取uniform变量的位置。 + +```js +// setup GLSL program +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const numVertsLoc = gl.getUniformLocation(program, 'numVerts'); +const resolutionLoc = gl.getUniformLocation(program, 'resolution'); +``` + +渲染时我们将使用着色器程序,设置 `resolution` 和 `numVerts` uniform变量并绘制顶点。 + +```js +gl.useProgram(program); + +const numVerts = 20; + +// tell the shader the number of verts +gl.uniform1i(numVertsLoc, numVerts); +// tell the shader the resolution +gl.uniform2f(resolutionLoc, gl.canvas.width, gl.canvas.height); + +const offset = 0; +gl.drawArrays(gl.POINTS, offset, numVerts); +``` + +最终将呈现点阵构成的圆形。 + +{{{example url="../webgl-no-data-point-circle.html"}}} + +该技术是否实用?通过创造性编码,我们几乎无需数据且仅用单次绘制调用即可实现星空或简单降雨效果。 + +以下以降雨效果为例。首先修改顶点着色器 + +```glsl +#version 300 es +uniform int numVerts; +uniform float time; + +void main() { + float u = float(gl_VertexID) / float(numVerts); // goes from 0 to 1 + float x = u * 2.0 - 1.0; // -1 to 1 + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 5.0; +} +``` + +此场景无需分辨率参数。 + +我们添加了 `time` uniform变量,表示从页面加载完成后的秒数。 + +x坐标设置为-1到1范围。 + +y坐标通过 `time + u` 计算,但 `fract` 仅返回小数部分(0.0到1.0)。 +将其扩展至1.0到-1.0范围,可获得随时间不断循环但各点偏移不同的y坐标。 + +将片段着色器中的颜色改为蓝色。 + +```glsl +precision highp float; + +out vec4 outColor; + +void main() { +- outColor = vec4(1, 0, 0, 1); ++ outColor = vec4(0, 0, 1, 1); +} +``` + +随后在JavaScript中需获取 `time` uniform变量的位置。 + +```js +// setup GLSL program +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const numVertsLoc = gl.getUniformLocation(program, 'numVerts'); +-const resolutionLoc = gl.getUniformLocation(program, 'resolution'); ++const timeLoc = gl.getUniformLocation(program, 'time'); +``` +通过创建渲染循环并设置 `time` uniform变量,将代码改为[动画](webgl-animation.html)。 + +```js ++function render(time) { ++ time *= 0.001; // convert to seconds + ++ webglUtils.resizeCanvasToDisplaySize(gl.canvas); ++ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.useProgram(program); + + const numVerts = 20; + + // tell the shader the number of verts + gl.uniform1i(numVertsLoc, numVerts); ++ // tell the shader the time ++ gl.uniform1f(timeLoc, time); + + const offset = 0; + gl.drawArrays(gl.POINTS, offset, numVerts); + ++ requestAnimationFrame(render); ++} ++requestAnimationFrame(render); +``` + +{{{example url="../webgl-no-data-point-rain-linear.html"}}} + +当前效果为顺序下落的点阵,需添加随机性。GLSL没有随机数生成器,但可使用伪随机函数实现近似效果。 + + +如下所示: + +```glsl +// hash function from https://www.shadertoy.com/view/4djSRW +// given a value between 0 and 1 +// returns a value between 0 and 1 that *appears* kind of random +float hash(float p) { + vec2 p2 = fract(vec2(p * 5.3983, p * 5.4427)); + p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137)); + return fract(p2.x * p2.y * 95.4337); +} +``` + +我们可以这样使用 + +```glsl +void main() { + float u = float(gl_VertexID) / float(numVerts); // goes from 0 to 1 +- float x = u * 2.0 - 1.0; // -1 to 1 ++ float x = hash(u) * 2.0 - 1.0; // random position + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 5.0; +} +``` + +向`hash`函数传入0到1区间的值,将返回对应的伪随机0到1区间值。 + +同时缩小点尺寸。 + +```glsl + gl_Position = vec4(x, y, 0, 1); +- gl_PointSize = 5.0; ++ gl_PointSize = 2.0; +``` + +并增加绘制点的数量。 + +```js +-const numVerts = 20; ++const numVerts = 400; +``` + +最终实现效果如下: + +{{{example url="../webgl-no-data-point-rain.html"}}} + +仔细观察可发现降雨存在重复模式。注意特定点群从底部消失后又在顶部重现。若背景存在更多元素(如3D游戏场景),这种循环可能不易察觉。 + +我们可通过增加一点随机性来修正这种重复感。 + +```glsl +void main() { + float u = float(gl_VertexID) / float(numVerts); // goes from 0 to 1 ++ float off = floor(time + u) / 1000.0; // changes once per second per vertex +- float x = hash(u) * 2.0 - 1.0; // random position ++ float x = hash(u + off) * 2.0 - 1.0; // random position + float y = fract(time + u) * -2.0 + 1.0; // 1.0 -> -1.0 + + gl_Position = vec4(x, y, 0, 1); + gl_PointSize = 2.0; +} +``` + +上述代码通过添加 `off` 变量,利用 `floor(time + u)` 为每个顶点生成每秒变化一次的次级计时器。 +该偏移量与点下移逻辑同步:当点从屏幕底部跳回顶部时,`hash`函数将获得新输入值,从而使该点获得新的水平随机位置。 + +最终实现无重复模式的降雨效果。 + +{{{example url="../webgl-no-data-point-rain-less-repeat.html"}}} + +能否实现比`gl.POINTS`更复杂的绘制?当然可以! + +让我们绘制圆形:这需要围绕中心点构建三角形扇面(类似披萨切片)。 +我们可以将每个三角形想象成饼图边缘上的两个点,以及中心上的一个点。 +然后,我们对饼图的每一片重复上述步骤。 + +
+ +首先需要构建一个计数器,该计数器在每处理一个切片时递增。 + +```glsl +int sliceId = gl_VertexID / 3; +``` + +其次需要构建圆周边缘的顶点计数器,其数值范围 + + 0, 1, ?, 1, 2, ?, 2, 3, ?, ... + +该?值实际无关紧要——如图所示,第三个顶点始终位于中心点(0,0),因此可直接乘以0忽略其值。 + +要实现上述模式,可采用如下方案: + +```glsl +int triVertexId = gl_VertexID % 3; +int edge = triVertexId + sliceId; +``` + +顶点分布需遵循此模式:2个边缘点后接1个中心点,如此循环往复。 + + 1, 1, 0, 1, 1, 0, 1, 1, 0, ... + +可通过以下模式实现: + +```glsl +float radius = step(1.5, float(triVertexId)); +``` + +`step(a, b)`函数在a + +作为"仅凭顶点ID能否绘制有趣图形"的智力探索,这种方案颇具巧思。 +事实上[该网站](https://www.vertexshaderart.com)就专注于此类挑战。 +但就性能而言,传统方案——通过缓冲区传入立方体顶点数据并用属性读取,或其他我们将介绍的技术——效率会高出许多。 + +需要权衡取舍:若需实现上文降雨效果,所述代码已相当高效。 +两种方案的性能边界存在于某处中间地带——通常传统方案兼具更高灵活性,但具体采用何种方式仍需根据实际场景判断。 + +本文主要目的是介绍这些创新理念,并强调理解WebGL实际工作原理的不同视角。重申:WebGL仅要求您在着色器中设置gl_Position并输出颜色值,具体实现方式无关紧要。 + +
+

关于gl.POINTS的一个问题

+

+此类技术的一个实用场景是模拟gl.POINTS的绘制效果。 +

+ +gl.POINTS存在两个主要问题: + +
    +
  1. 存在最大尺寸限制

    多数开发者使用gl.POINTS时选择较小尺寸,但若所需尺寸超过其上限,则需采用替代方案。 +
  2. +
  3. 屏幕外裁剪行为不一致问题:

    +当点中心位于画布左边缘外1像素处,而gl_PointSize设为32.0。 +
    +根据OpenGL ES 3.0规范:当32x32像素点中有15列仍处于画布内时,应予以绘制。但OpenGL(非ES版本)规范却完全相反——只要点中心位于画布外就完全不绘制。 +更糟的是,OpenGL长期缺乏充分测试,导致不同驱动实现不一:有的驱动会绘制这些像素,有的则不会。😭 +
  4. +
+

因此,若这两个问题影响您的需求,解决方案是改用gl.TRIANGLES绘制自定义四边形而非使用gl.POINTS。如此可同时解决:
+1. 最大尺寸限制问题
+2. 裁剪不一致问题
+绘制大量四边形的方法有多种,其中之一便是采用本文所述技术

+
From b692335b86ac92f26af0594e34995b6892eccbc1 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sat, 21 Jun 2025 22:21:02 +0800 Subject: [PATCH 04/22] Translate webgl-animation.md to Chinese --- webgl/lessons/zh_cn/webgl-animation.md | 98 ++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-animation.md diff --git a/webgl/lessons/zh_cn/webgl-animation.md b/webgl/lessons/zh_cn/webgl-animation.md new file mode 100644 index 000000000..13bab6fee --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-animation.md @@ -0,0 +1,98 @@ +Title: WebGL2 - 动画 +Description: WebGL动画实现方法 +TOC: 动画 + + +本文隶属于WebGL系列教程,首篇[基础教程](webgl-fundamentals.html)从核心概念讲起,前文则探讨了[3D相机](webgl-3d-camera.html)相关内容。若尚未阅读,请先参阅。 + +如何在WebGL中让物体动起来? + +本质上,这并非WebGL特有机制——任何JavaScript动画都需要随时间改变状态并重绘。 + +我们选取一个前文示例进行动画改造。 + + *var fieldOfViewRadians = degToRad(60); + *var rotationSpeed = 1.2; + + *requestAnimationFrame(drawScene); + + // Draw the scene. + function drawScene() { + * // Every frame increase the rotation a little. + * rotation[1] += rotationSpeed / 60.0; + + ... + * // Call drawScene again next frame + * requestAnimationFrame(drawScene); + } + +效果如下: + +{{{example url="../webgl-animation-not-frame-rate-independent.html" }}} + +但存在一个潜在问题,代码中的 `rotationSpeed / 60.0` 是基于浏览器每秒60次响应 `requestAnimationFrame` 的假设, +这种帧率假设并不可靠。 + +该假设实际上并不成立。用户可能使用旧款智能手机等低性能设备,或后台运行重负载程序。 +浏览器帧率受多种因素影响,未必保持60FPS——例如2020年后硬件或支持240FPS,或电竞玩家使用90Hz刷新率的CRT显示器。 + +可通过此示例观察该问题: + +{{{diagram url="../webgl-animation-frame-rate-issues.html" }}} + +上例中,我们想让所有'F'保持相同的转速。中间的F全速运行,帧率无关。 +左右两侧的F,模拟浏览器只有1/8性能运行的情况。在左侧的F是帧率**无**关的。右边的F,帧率**相**关。 + +可见:左侧F因未考虑帧率下降而无法同步,右侧F即使以1/8帧率运行仍与全速运行的中央F保持同步。 + +实现帧率无关动画的核心方法是:计算帧间时间差,并据此确定当前帧的动画状态增量。 + +先需要获取时间值。幸运的是 `requestAnimationFrame` 在回调时会自动传入页面加载后的时间戳。 + +为简化计算,转换为以秒为单位最简单。由于 `requestAnimationFrame` 提供的时间单位为毫秒(1/1000秒),需乘以0.001转换为秒。 + +由此,可按下面的方式计算时间增量: + + *var then = 0; + + requestAnimationFrame(drawScene); + + // Draw the scene. + *function drawScene(now) { + * // Convert the time to seconds + * now *= 0.001; + * // Subtract the previous time from the current time + * var deltaTime = now - then; + * // Remember the current time for the next frame. + * then = now; + + ... + +一旦获得以秒为单位的`deltaTime`,我们所有的计算都可以基于"每秒单位量"进行。在本例中: +`rotationSpeed` 设为1.2,表示每秒旋转1.2弧度。约等于1/5圆周,完整旋转一周约需5秒,且该速率与帧率无关。 + + + * rotation[1] += rotationSpeed * deltaTime; + +以下是实现效果: + +{{{example url="../webgl-animation.html" }}} + +除非在低性能设备上运行,否则您可能难以察觉与本页顶部示例的差异。但若不实现帧率无关的动画,部分用户获得的体验将与您的设计预期大相径庭。s + +接下来学习如何[应用纹理](webgl-3d-textures.html)。 + +
+

请勿使用 setInterval 或 setTimeout !

+

若您曾用JavaScript实现动画,可能习惯使用setIntervalsetTimeout调用绘制函数。

+

这类方式存在双重缺陷:首先,setIntervalsetTimeout与浏览器渲染流程无关,它们无法与屏幕刷新同步,最终将导致与用户设备不同步。 +若您假设60FPS使用它们,而实际设备以其他帧率运行,就会出现同步问题。

+

其次,浏览器无法识别您调用setIntervalsetTimeout的意图。即使页面处于后台标签页(不可见状态),浏览器仍会执行这些代码。虽然这对每分钟检查邮件/Tweet的任务无碍,但若用于WebGL渲染上千个对象,将导致用户设备资源被不可见页面的渲染任务无意义消耗。

+

+requestAnimationFrame能完美解决这些问题。 +它在屏幕刷新最佳时机触发回调,且仅在标签页可见时执行。 +

+
+ + + From d83c8ce4ceac66907f6cc9bf7101064007031a04 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sun, 22 Jun 2025 23:22:29 +0800 Subject: [PATCH 05/22] Translate add Chinese webgl-gpgpu.md --- webgl/lessons/zh_cn/webgl-gpgpu.md | 2092 ++++++++++++++++++++++++++++ 1 file changed, 2092 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-gpgpu.md diff --git a/webgl/lessons/zh_cn/webgl-gpgpu.md b/webgl/lessons/zh_cn/webgl-gpgpu.md new file mode 100644 index 000000000..8d3d91291 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-gpgpu.md @@ -0,0 +1,2092 @@ +Title: WebGL2 通用GPU计算(GPGPU) +Description: 如何使用GPU进行通用计算 +TOC: 通用GPU计算(GPGPU) + +GPGPU即"通用图形处理器计算"(“General Purpose” GPU),指将GPU用于像素渲染之外的其他计算目的。 + +理解WebGL中GPGPU的核心在于:纹理(texture)本质上是二维数值数组,而非图像。在[纹理详解](webgl-3d-textures.html)中我们探讨了纹理读取,在[渲染到纹理](webgl-render-to-texture.html)中介绍了纹理写入。 +因此,通过纹理我们实现了对二维数组的读写操作。 +同理,缓冲区(buffer)不仅可存储位置、法线、纹理坐标和颜色数据,还能承载速度、质量、股价等任意数据。 +创造性地运用这些特性进行数学计算,正是WebGL中GPGPU的精髓。 + +## 首先通过纹理实现方案: + +JavaScript中的[`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)函数可对数组元素执行遍历处理。 + +```js +function multBy2(v) { + return v * 2; +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = src.map(multBy2); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +可将`multBy2`视为着色器,而`map`则类似于调用`gl.drawArrays`或`gl.drawElements`,但存在以下差异。 + +## 着色器不会生成新数组,必须预先提供目标数组。 + +我们可以通过自定义map函数来模拟此行为: + +```js +function multBy2(v) { + return v * 2; +} + ++function mapSrcToDst(src, fn, dst) { ++ for (let i = 0; i < src.length; ++i) { ++ dst[i] = fn(src[i]); ++ } ++} + +const src = [1, 2, 3, 4, 5, 6]; +-const dst = src.map(multBy2); ++const dst = new Array(6); // to simulate that in WebGL we have to allocate a texture ++mapSrcToDst(src, multBy2, dst); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +## 着色器不返回值,而是设置out变量。 + +这一行为很容易模拟实现。 + +```js ++let outColor; + +function multBy2(v) { +- return v * 2; ++ outColor = v * 2; +} + +function mapSrcToDst(src, fn, dst) { + for (let i = 0; i < src.length; ++i) { +- dst[i] = fn(src[i]); ++ fn(src[i]); ++ dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // to simulate that in WebGL we have to allocate a texture +mapSrcToDst(src, multBy2, dst); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +## 着色器采用基于目标(destination-based)而非基于源(source-based)的处理模式。 + +换言之,着色器会遍历目标位置并自问"此处应填入何值"。 + +```js +let outColor; + +function multBy2(src) { +- outColor = v * 2; ++ return function(i) { ++ outColor = src[i] * 2; ++ } +} + +-function mapSrcToDst(src, fn, dst) { +- for (let i = 0; i < src.length; ++i) { +- fn(src[i]); ++function mapDst(dst, fn) { ++ for (let i = 0; i < dst.length; ++i) { ++ fn(i); + dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // to simulate that in WebGL we have to allocate a texture +mapDst(dst, multBy2(src)); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +## 在WebGL中,需要提供值的像素索引/ID被称为gl_FragCoord。 + +```js +let outColor; ++let gl_FragCoord; + +function multBy2(src) { +- return function(i) { +- outColor = src[i] * 2; ++ return function() { ++ outColor = src[gl_FragCoord] * 2; + } +} + +function mapDst(dst, fn) { + for (let i = 0; i < dst.length; ++i) { +- fn(i); ++ gl_FragCoord = i; ++ fn(); + dst[i] = outColor; + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // to simulate that in WebGL we have to allocate a texture +mapDst(dst, multBy2(src)); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +## 在WebGL中,纹理(textures)本质上是二维数组。 + +假设我们的dst数组表示一个3x2纹理。 + +```js +let outColor; +let gl_FragCoord; + +function multBy2(src, across) { + return function() { +- outColor = src[gl_FragCoord] * 2; ++ outColor = src[gl_FragCoord.y * across + gl_FragCoord.x] * 2; + } +} + +-function mapDst(dst, fn) { +- for (let i = 0; i < dst.length; ++i) { +- gl_FragCoord = i; +- fn(); +- dst[i] = outColor; +- } +-} +function mapDst(dst, across, up, fn) { + for (let y = 0; y < up; ++y) { + for (let x = 0; x < across; ++x) { + gl_FragCoord = {x, y}; + fn(); + dst[y * across + x] = outColor; + } + } +} + +const src = [1, 2, 3, 4, 5, 6]; +const dst = new Array(6); // to simulate that in WebGL we have to allocate a texture +mapDst(dst, 3, 2, multBy2(src, 3)); + +// dst is now [2, 4, 6, 8, 10, 12]; +``` + +我们可以继续扩展。希望上述示例能帮助您理解:WebGL中的GPGPU在概念上其实相当简单。现在让我们实际用WebGL实现上述功能。 + +要理解后续代码实现,需预先掌握以下核心知识: +- [WebGL基础原理](webgl-fundamentals.html) +- [管线工作机制](webgl-how-it-works.html) +- [GLSL着色语言](webgl-shaders-and-glsl.html) +- [纹理处理技术](webgl-3d-textures.html) + + +```js +const vs = `#version 300 es +in vec4 position; +void main() { + gl_Position = position; +} +`; + +const fs = `#version 300 es +precision highp float; + +uniform sampler2D srcTex; + +out vec4 outColor; + +void main() { + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + vec4 value = texelFetch(srcTex, texelCoord, 0); // 0 = mip level 0 + outColor = value * 2.0; +} +`; + +const dstWidth = 3; +const dstHeight = 2; + +// make a 3x2 canvas for 6 results +const canvas = document.createElement('canvas'); +canvas.width = dstWidth; +canvas.height = dstHeight; + +const gl = canvas.getContext('webgl2'); + +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const positionLoc = gl.getAttribLocation(program, 'position'); +const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); + +// setup a full canvas clip space quad +const buffer = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buffer); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, -1, + 1, -1, + -1, 1, + -1, 1, + 1, -1, + 1, 1, +]), gl.STATIC_DRAW); + +// Create a vertex array object (attribute state) +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); + +// setup our attributes to tell WebGL how to pull +// the data from the buffer above to the position attribute +gl.enableVertexAttribArray(positionLoc); +gl.vertexAttribPointer( + positionLoc, + 2, // size (num components) + gl.FLOAT, // type of data in buffer + false, // normalize + 0, // stride (0 = auto) + 0, // offset +); + +// create our source texture +const srcWidth = 3; +const srcHeight = 2; +const tex = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, tex); +gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); // see https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html +gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip level + gl.R8, // internal format + srcWidth, + srcHeight, + 0, // border + gl.RED, // format + gl.UNSIGNED_BYTE, // type + new Uint8Array([ + 1, 2, 3, + 4, 5, 6, + ])); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + +gl.useProgram(program); +gl.uniform1i(srcTexLoc, 0); // tell the shader the src texture is on texture unit 0 + +gl.drawArrays(gl.TRIANGLES, 0, 6); // draw 2 triangles (6 vertices) + +// get the result +const results = new Uint8Array(dstWidth * dstHeight * 4); +gl.readPixels(0, 0, dstWidth, dstHeight, gl.RGBA, gl.UNSIGNED_BYTE, results); + +// print the results +for (let i = 0; i < dstWidth * dstHeight; ++i) { + log(results[i * 4]); +} +``` + +以下是实际运行效果: + +{{{example url="../webgl-gpgpu-mult-by-2.html"}}} + +关于上述代码的要点说明: + +* 我们绘制了一个裁剪空间范围为-1到+1的四边形。 + + 我们通过两个三角形创建了一个-1到+1范围的四边形。这意味着,在正确设置视口的情况下,我们将绘制目标的所有像素。换句话说,我们将要求着色器为结果数组中的每个元素生成一个值——在本例中,该数组就是画布本身。 + +* `texelFetch` 是用于从纹理中获取单个纹素(texel)的纹理查询函数。 + + 该函数接收三个参数:采样器(sampler)、基于整数的纹素坐标(texel coordinate)和mip层级。 `gl_FragCoord`是vec2类型,需转换为`ivec2`才能用于`texelFetch`。 只要源纹理和目标纹理尺寸相同(本例满足此条件),就无需额外数学计算。 + +* 着色器每个像素写入4个值 + + 在此特定情况下,这将影响我们读取输出的方式。[由于其他格式/类型组合不受支持](webgl-readpixels.html),我们通过`RGBA/UNSIGNED_BYTE`格式调用`readPixels`,因此需要每间隔4个值提取有效结果。 + + 注意:利用WebGL每次处理4个值的特性可进一步提升性能。 + +* 我们使用`R8`作为纹理的内部格式。 + + 这意味着纹理中仅红色通道包含我们的有效数据值。 + +* 输入数据和输出数据(画布)均采用UNSIGNED_BYTE格式 + + 这表明我们仅能传入和获取0到255之间的整数值。通过使用不同格式的纹理作为输入,我们可以扩展输入数据的范围;同样,尝试渲染到不同格式的纹理也能获得更大范围的输出值。 + +在上例中,src和dst尺寸相同。现修改为:每2个src值相加生成1个dst值。即给定输入`[1, 2, 3, 4, 5, 6]`,输出应为`[3, 7, 11]`,同时保持源数据为3x2结构。 + +从二维数组中获取值的基本公式就像从一维数组中获取值一样 + +```js +y = floor(indexInto1DArray / widthOf2DArray); +x = indexInto1DArray % widthOf2Array; +``` + +基于此,我们的片段着色器需修改为以下形式以实现每2个值相加: + +```glsl +#version 300 es +precision highp float; + +uniform sampler2D srcTex; +uniform ivec2 dstDimensions; + +out vec4 outColor; + +vec4 getValueFrom2DTextureAs1DArray(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); +} + +void main() { + // compute a 1D index into dst + ivec2 dstPixel = ivec2(gl_FragCoord.xy); + int dstIndex = dstPixel.y * dstDimensions.x + dstPixel.x; + + ivec2 srcDimensions = textureSize(srcTex, 0); // size of mip 0 + + vec4 v1 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2); + vec4 v2 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2 + 1); + + outColor = v1 + v2; +} +``` + +`getValueFrom2DTextureAs1DArray`函数本质上是我们模拟一维数组访问的核心方法,其关键实现体现在以下两行代码: + +```glsl + vec4 v1 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2.0); + vec4 v2 = getValueFrom2DTextureAs1DArray(srcTex, srcDimensions, dstIndex * 2.0 + 1.0); +``` + +其等效于以下逻辑: + +```glsl + vec4 v1 = srcTexAs1DArray[dstIndex * 2.0]; + vec4 v2 = setTexAs1DArray[dstIndex * 2.0 + 1.0]; +``` + +在JavaScript中,我们需要获取`dstDimensions`的位置。 + +```js +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const positionLoc = gl.getAttribLocation(program, 'position'); +const srcTexLoc = gl.getUniformLocation(program, 'srcTex'); ++const dstDimensionsLoc = gl.getUniformLocation(program, 'dstDimensions'); +``` + +并设置它 + +```js +gl.useProgram(program); +gl.uniform1i(srcTexLoc, 0); // tell the shader the src texture is on texture unit 0 ++gl.uniform2f(dstDimensionsLoc, dstWidth, dstHeight); +``` + +且需要调整目标(画布)的尺寸。 + +```js +const dstWidth = 3; +-const dstHeight = 2; ++const dstHeight = 1; +``` + +至此,我们已实现结果数组可对源数组进行随机访问计算。 + +{{{example url="../webgl-gpgpu-add-2-elements.html"}}} + +如需使用更多输入数组,只需添加纹理,在同一纹理中存储更多数据即可。 + +## 现在让我们通过*变换反馈 transform feedback*实现: + +“变换反馈”(Transform Feedback)是指将顶点着色器中变量的输出写入一个或多个缓冲区的功能。 + +使用变换反馈的优势在于输出是一维的,所以推理起来可能更容易。它甚至更接近 JavaScript 中的`map`。 + +让我们输入两个数组,并输出它们的和、差和乘积。顶点着色器代码如下: + +```glsl +#version 300 es + +in float a; +in float b; + +out float sum; +out float difference; +out float product; + +void main() { + sum = a + b; + difference = a - b; + product = a * b; +} +``` + +而片段着色器仅需满足编译要求。 + +```glsl +#version 300 es +precision highp float; +void main() { +} +``` +要使用变换反馈,我们必须告诉 WebGL 需要写入哪些变量以及写入顺序。 +我们在链接着色器程序之前调用 `gl.transformFeedbackVaryings` 来实现这一点。 +为了明确说明我们需要做什么,这次我们不打算使用辅助函数来编译着色器并链接程序。 + +以下是编译着色器的代码,其实现类似于[基础教程](webgl-fundamentals.html)中的原始版本。 + +```js +function createShader(gl, type, src) { + const shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + throw new Error(gl.getShaderInfoLog(shader)); + } + return shader; +} +``` + +我们将使用该函数编译两个着色器,在程序链接前执行附着操作并调用`gl.transformFeedbackVaryings`。 + +```js +const vShader = createShader(gl, gl.VERTEX_SHADER, vs); +const fShader = createShader(gl, gl.FRAGMENT_SHADER, fs); + +const program = gl.createProgram(); +gl.attachShader(program, vShader); +gl.attachShader(program, fShader); +gl.transformFeedbackVaryings( + program, + ['sum', 'difference', 'product'], + gl.SEPARATE_ATTRIBS, +); +gl.linkProgram(program); +if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(gl.getProgramParameter(program)); +} +``` + +`gl.transformFeedbackVaryings` 接受 3 个参数。`程序program`,一个数组,其中包含我们想要写入的变量的名称,这些变量的名称按照您希望的顺序排列。 +如果您确实有一个实际执行了某些操作的片段着色器,那么某些变量可能仅适用于该片段着色器,因此无需写入。 +在本例中,我们将写入所有变量,因此我们将传入所有 3 个变量的名称。 +最后一个参数可以是两个值之一:`SEPARATE_ATTRIBS` 或 `INTERLEAVED_ATTRIBS`。 + +`SEPARATE_ATTRIBS` 表示每个`varying变量`将写入独立的缓冲区。 +`INTERLEAVED_ATTRIBS` 表示所有varying变量将按指定顺序交错写入同一缓冲区。 +在本例中,由于我们指定了`['sum', 'difference', 'product']`的顺序,若使用`INTERLEAVED_ATTRIBS`模式,输出数据将以`sum0, difference0, product0, sum1, difference1, product1, sum2, difference2, product2,...`的形式交错存储在单个缓冲区中。 +我们当前使用的是`SEPARATE_ATTRIBS`模式,因此每个输出将写入独立的缓冲区。 + +与其他示例类似,我们需要为输入属性配置缓冲区。 + +```js +const aLoc = gl.getAttribLocation(program, 'a'); +const bLoc = gl.getAttribLocation(program, 'b'); + +// Create a vertex array object (attribute state) +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); + +function makeBuffer(gl, sizeOrData) { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, gl.STATIC_DRAW); + return buf; +} + +function makeBufferAndSetAttribute(gl, data, loc) { + const buf = makeBuffer(gl, data); + // setup our attributes to tell WebGL how to pull + // the data from the buffer above to the attribute + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer( + loc, + 1, // size (num components) + gl.FLOAT, // type of data in buffer + false, // normalize + 0, // stride (0 = auto) + 0, // offset + ); +} + +const a = [1, 2, 3, 4, 5, 6]; +const b = [3, 6, 9, 12, 15, 18]; + +// put data in buffers +const aBuffer = makeBufferAndSetAttribute(gl, new Float32Array(a), aLoc); +const bBuffer = makeBufferAndSetAttribute(gl, new Float32Array(b), bLoc); +``` + +我们需要设置"变换反馈"(transform feedback)对象。 +该对象包含待写入缓冲区的状态配置,正如[顶点数组](webgl-attributes.html)管理所有输入属性的状态,"变换反馈"则管理所有输出属性的状态。 + +以下设置我们所需的代码: + +```js +// Create and fill out a transform feedback +const tf = gl.createTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + +// make buffers for output +const sumBuffer = makeBuffer(gl, a.length * 4); +const differenceBuffer = makeBuffer(gl, a.length * 4); +const productBuffer = makeBuffer(gl, a.length * 4); + +// bind the buffers to the transform feedback +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, sumBuffer); +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, differenceBuffer); +gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, productBuffer); + +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + +// buffer's we are writing to can not be bound else where +gl.bindBuffer(gl.ARRAY_BUFFER, null); // productBuffer was still bound to ARRAY_BUFFER so unbind it +``` + +我们调用`bindBufferBase`来设置每个输出(输出0、输出1和输出2)将写入哪个缓冲区。 +输出0、1、2对应我们在链接程序时传递给`gl.transformFeedbackVaryings`的名称。 + +当我们完成"变换反馈"(transform feedback)操作后,所创建的状态如下所示: + + + +另外还有一个`bindBufferRange`函数,允许我们指定缓冲区内的写入子范围,但此处我们不会使用该功能。 + +要执行着色器,我们需要执行以下操作: + +```js +gl.useProgram(program); + +// bind our input attribute state for the a and b buffers +gl.bindVertexArray(vao); + +// no need to call the fragment shader +gl.enable(gl.RASTERIZER_DISCARD); + +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); +gl.beginTransformFeedback(gl.POINTS); +gl.drawArrays(gl.POINTS, 0, a.length); +gl.endTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + +// turn on using fragment shaders again +gl.disable(gl.RASTERIZER_DISCARD); +``` + +我们禁用片段着色器的调用,绑定之前创建的变换反馈(transform feedback)对象,启用变换反馈,然后调用绘制(draw)操作。 + +要查看这些值,我们可以调用 `gl.getBufferSubData` 方法。 + +```js +log(`a: ${a}`); +log(`b: ${b}`); + +printResults(gl, sumBuffer, 'sums'); +printResults(gl, differenceBuffer, 'differences'); +printResults(gl, productBuffer, 'products'); + +function printResults(gl, buffer, label) { + const results = new Float32Array(a.length); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.getBufferSubData( + gl.ARRAY_BUFFER, + 0, // byte offset into GPU buffer, + results, + ); + // print the results + log(`${label}: ${results}`); +} +``` + +{{{example url="../webgl-gpgpu-sum-difference-product-transformfeedback.html"}}} + +可以看到它生效了。GPU 成功计算出了我们传入的 'a' 和 'b' 值的和(sum)、差(difference)以及积(product)。 + +注:您可能会发现[这个变换反馈状态图示例](https://webgl2fundamentals.org/webgl/lessons/resources/webgl-state-diagram.html?exampleId=transform-feedback) 有助于理解"变换反馈"(transform feedback)的概念。 +不过该示例与上文不同,其顶点着色器配合变换反馈生成的是圆形点阵的位置和颜色数据。 + +## 第一个示例:粒子系统 + +假设我们有一个非常简单的粒子系统。每个粒子仅包含位置(position)和速度(velocity)属性,当粒子移出屏幕一侧边界时,会从另一侧重新出现。 + +根据本站大多数其他文章的惯例,你可能会选择在JavaScript中更新粒子的位置。 + +```js +for (const particle of particles) { + particle.pos.x = (particle.pos.x + particle.velocity.x) % canvas.width; + particle.pos.y = (particle.pos.y + particle.velocity.y) % canvas.height; +} +``` + +然后逐个绘制这些粒子 + +``` +useProgram (particleShader) +setup particle attributes +for each particle + set uniforms + draw particle +``` + +或者,您也可以一次性上传所有粒子的新位置数据 + +``` +bindBuffer(..., particlePositionBuffer) +bufferData(..., latestParticlePositions, ...) +useProgram (particleShader) +setup particle attributes +set uniforms +draw particles +``` + +利用前文的**变换反馈**(transform feedback)示例,我们可以: + +1. 创建包含每个粒子**速度**(velocity)的缓冲区(buffer) +2. 建立两个**位置**(position)缓冲区 +3. 使用变换反馈将速度与一个位置缓冲区相加,结果写入另一个位置缓冲区 +4. 使用新位置数据进行绘制(draw) + +在下一帧时: +- 从存储新位置的缓冲区**读取**(read)数据 +- **回写**(write back)到另一个缓冲区以生成更新的位置 + +以下是用于更新粒子位置的顶点着色器代码: + +```glsl +#version 300 es +in vec2 oldPosition; +in vec2 velocity; + +uniform float deltaTime; +uniform vec2 canvasDimensions; + +out vec2 newPosition; + +vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); +} + +void main() { + newPosition = euclideanModulo( + oldPosition + velocity * deltaTime, + canvasDimensions); +} +``` + +使用一个简单的顶点着色器来绘制粒子 + +```glsl +#version 300 es +in vec4 position; +uniform mat4 matrix; + +void main() { + // do the common matrix math + gl_Position = matrix * position; + gl_PointSize = 10.0; +} +``` + +以下是将程序创建和链接过程封装为通用函数的实现,可同时适用于常规渲染和Transform Feedback着色器。 + +```js +function createProgram(gl, shaderSources, transformFeedbackVaryings) { + const program = gl.createProgram(); + [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => { + const shader = createShader(gl, type, shaderSources[ndx]); + gl.attachShader(program, shader); + }); + if (transformFeedbackVaryings) { + gl.transformFeedbackVaryings( + program, + transformFeedbackVaryings, + gl.SEPARATE_ATTRIBS, + ); + } + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(gl.getProgramParameter(program)); + } + return program; +} +``` + +随后利用该函数编译着色器:其中一个包含`transform feedback`输出变量。 + +```js +const updatePositionProgram = createProgram( + gl, [updatePositionVS, updatePositionFS], ['newPosition']); +const drawParticlesProgram = createProgram( + gl, [drawParticlesVS, drawParticlesFS]); +``` + +照例,我们需要查找到各个变量的位置: + +```js +const updatePositionPrgLocs = { + oldPosition: gl.getAttribLocation(updatePositionProgram, 'oldPosition'), + velocity: gl.getAttribLocation(updatePositionProgram, 'velocity'), + canvasDimensions: gl.getUniformLocation(updatePositionProgram, 'canvasDimensions'), + deltaTime: gl.getUniformLocation(updatePositionProgram, 'deltaTime'), +}; + +const drawParticlesProgLocs = { + position: gl.getAttribLocation(drawParticlesProgram, 'position'), + matrix: gl.getUniformLocation(drawParticlesProgram, 'matrix'), +}; +``` + +现在让我们生成一些随机的位置和速度数据: + +```js +// create random positions and velocities. +const rand = (min, max) => { + if (max === undefined) { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; +}; +const numParticles = 200; +const createPoints = (num, ranges) => + new Array(num).fill(0).map(_ => ranges.map(range => rand(...range))).flat(); +const positions = new Float32Array(createPoints(numParticles, [[canvas.width], [canvas.height]])); +const velocities = new Float32Array(createPoints(numParticles, [[-300, 300], [-300, 300]])); +``` + +随后我们将这些数据存入缓冲区: + +```js +function makeBuffer(gl, sizeOrData, usage) { + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage); + return buf; +} + +const position1Buffer = makeBuffer(gl, positions, gl.DYNAMIC_DRAW); +const position2Buffer = makeBuffer(gl, positions, gl.DYNAMIC_DRAW); +const velocityBuffer = makeBuffer(gl, velocities, gl.STATIC_DRAW); +``` + +请注意,我们在为两个位置缓冲区调用`gl.bufferData`时传入了`gl.DYNAMIC_DRAW`参数,因为需要频繁更新这些缓冲区。 +这只是提供给WebGL的优化提示,实际是否影响性能取决于WebGL的具体实现。 + +我们需要4个顶点数组。 + +* 第1个:在更新位置时使用`position1Buffer`和`velocity`缓冲区 +* 第2个:在更新位置时使用`position2Buffer`和`velocity`缓冲区 +* 第3个:在绘制时使用`position1Buffer` +* 第4个:在绘制时使用`position2Buffer` + +```js +function makeVertexArray(gl, bufLocPairs) { + const va = gl.createVertexArray(); + gl.bindVertexArray(va); + for (const [buffer, loc] of bufLocPairs) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer( + loc, // attribute location + 2, // number of elements + gl.FLOAT, // type of data + false, // normalize + 0, // stride (0 = auto) + 0, // offset + ); + } + return va; +} + +const updatePositionVA1 = makeVertexArray(gl, [ + [position1Buffer, updatePositionPrgLocs.oldPosition], + [velocityBuffer, updatePositionPrgLocs.velocity], +]); +const updatePositionVA2 = makeVertexArray(gl, [ + [position2Buffer, updatePositionPrgLocs.oldPosition], + [velocityBuffer, updatePositionPrgLocs.velocity], +]); + +const drawVA1 = makeVertexArray( + gl, [[position1Buffer, drawParticlesProgLocs.position]]); +const drawVA2 = makeVertexArray( + gl, [[position2Buffer, drawParticlesProgLocs.position]]); +``` + +接下来我们创建两个变换反馈(transform feedback)对象: + +* 一个用来写入 `position1Buffer` +* 一个用来写入 `position2Buffer` + +```js +function makeTransformFeedback(gl, buffer) { + const tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + return tf; +} + +const tf1 = makeTransformFeedback(gl, position1Buffer); +const tf2 = makeTransformFeedback(gl, position2Buffer); +``` + +使用变换反馈(transform feedback)时,必须解除其他绑定点的缓冲区关联。 +`ARRAY_BUFFER` 仍绑定着我们最后放入数据的缓冲区。 +调用 `gl.bindBufferBase` 时会设置 `TRANSFORM_FEEDBACK_BUFFER`。 +这里有些容易混淆:当使用`TRANSFORM_FEEDBACK_BUFFER`参数调用`gl.bindBufferBase`时,实际上会将缓冲区绑定到两个位置。 +一个绑定到变换反馈对象内部的索引化绑定点; +另一个绑定到名为`TRANSFORM_FEEDBACK_BUFFER`的全局绑定点。 + +```js +// unbind left over stuff +gl.bindBuffer(gl.ARRAY_BUFFER, null); +gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null); +``` + +为便于交换更新和绘制缓冲区,我们将设置这两个对象。 + +```js +let current = { + updateVA: updatePositionVA1, // read from position1 + tf: tf2, // write to position2 + drawVA: drawVA2, // draw with position2 +}; +let next = { + updateVA: updatePositionVA2, // read from position2 + tf: tf1, // write to position1 + drawVA: drawVA1, // draw with position1 +}; +``` + +接着我们将实现渲染循环:首先使用变换反馈(transform feedback)更新粒子位置。 + +```js +let then = 0; +function render(time) { + // convert to seconds + time *= 0.001; + // Subtract the previous time from the current time + const deltaTime = time - then; + // Remember the current time for the next frame. + then = time; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + gl.clear(gl.COLOR_BUFFER_BIT); + + // compute the new positions + gl.useProgram(updatePositionProgram); + gl.bindVertexArray(current.updateVA); + gl.uniform2f(updatePositionPrgLocs.canvasDimensions, gl.canvas.width, gl.canvas.height); + gl.uniform1f(updatePositionPrgLocs.deltaTime, deltaTime); + + // turn of using the fragment shader + gl.enable(gl.RASTERIZER_DISCARD); + + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, current.tf); + gl.beginTransformFeedback(gl.POINTS); + gl.drawArrays(gl.POINTS, 0, numParticles); + gl.endTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + + // turn on using fragment shaders again + gl.disable(gl.RASTERIZER_DISCARD); +``` + +然后绘制粒子。 + +```js + // now draw the particles. + gl.useProgram(drawParticlesProgram); + gl.bindVertexArray(current.drawVA); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.uniformMatrix4fv( + drawParticlesProgLocs.matrix, + false, + m4.orthographic(0, gl.canvas.width, 0, gl.canvas.height, -1, 1)); + gl.drawArrays(gl.POINTS, 0, numParticles); +``` + +最后交换 `current` 和 `next` 的指向,这样下一帧就能使用最新位置数据生成新的位置。 + +```js + // swap which buffer we will read from + // and which one we will write to + { + const temp = current; + current = next; + next = temp; + } + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +``` + +至此,我们完成了一个基于GPU的简易粒子系统实现。 + +{{{example url="../webgl-gpgpu-particles-transformfeedback.html"}}} + +## 下一个示例:查找离点最近的线段 + +我不确定这是否是个好示例,但这是我目前编写的案例。 +我认为这可能不是最佳示例,因为我怀疑存在比暴力检查每个线段与点之间距离更好的算法来查找离点最近的线段。 +例如,各种空间分区算法(space partitioning algorithms)可能让你轻松排除95%的线段,从而获得更快的计算速度。 +尽管如此,这个示例至少可能展示了某些GPGPU技术。 + +问题描述:现有500个点和1000条线段,需为每个点找出距离最近的一条线段。暴力计算方法的实现如下: + +``` +for each point + minDistanceSoFar = MAX_VALUE + for each line segment + compute distance from point to line segment + if distance is < minDistanceSoFar + minDistanceSoFar = distance + closestLine = line segment +``` + +对500个点各自检查1000条线段,总计需要50万次计算。 +现代GPU拥有数百至数千个核心,若能在GPU上执行此计算,理论上可获得数百至数千倍的加速。 + +这次,虽然我们可以像处理粒子那样将点数据存入缓冲区,但却无法对线段数据采用相同方式。 +缓冲区通过属性(attributes)提供数据,这意味着 +这意味着我们无法按需随机访问任意数据值,这些值是在着色器外部控制下分配给属性的。 + +因此,我们需要将线段位置存入纹理(texture)——正如前文所述, +纹理本质上就是二维数组的另一种表述,不过我们仍可将其作为一维数组来处理(根据需要)。 + +以下是用于查找单个点最近线段的顶点着色器代码,它完全实现了前文所述的暴力计算算法: + +```js + const closestLineVS = `#version 300 es + in vec3 point; + + uniform sampler2D linesTex; + uniform int numLineSegments; + + flat out int closestNdx; + + vec4 getAs1D(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); + } + + // from https://stackoverflow.com/a/6853926/128511 + // a is the point, b,c is the line segment + float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) { + vec3 ba = a - b; + vec3 bc = c - b; + float d = dot(ba, bc); + float len = length(bc); + float param = 0.0; + if (len != 0.0) { + param = clamp(d / (len * len), 0.0, 1.0); + } + vec3 r = b + bc * param; + return distance(a, r); + } + + void main() { + ivec2 linesTexDimensions = textureSize(linesTex, 0); + + // find the closest line segment + float minDist = 10000000.0; + int minIndex = -1; + for (int i = 0; i < numLineSegments; ++i) { + vec3 lineStart = getAs1D(linesTex, linesTexDimensions, i * 2).xyz; + vec3 lineEnd = getAs1D(linesTex, linesTexDimensions, i * 2 + 1).xyz; + float dist = distanceFromPointToLine(point, lineStart, lineEnd); + if (dist < minDist) { + minDist = dist; + minIndex = i; + } + } + + closestNdx = minIndex; + } + `; +``` + +我将`getValueFrom2DTextureAs1DArray`重命名为`getAs1D`,仅是为了缩短部分代码行以提升可读性。 +除此之外,这就是对我们上文所写暴力算法的直接实现。 + +`point` 表示当前检测点。`linesTex` 纹理按线段端点对存储数据:每对数据依次包含线段的起点坐标和终点坐标。 + +首先创建测试数据:这里使用2个点和5条线段。所有数据都补零(0, 0),因为每个元素都将存储在RGBA纹理中。 + +```js +const points = [ + 100, 100, + 200, 100, +]; +const lines = [ + 25, 50, + 25, 150, + 90, 50, + 90, 150, + 125, 50, + 125, 150, + 185, 50, + 185, 150, + 225, 50, + 225, 150, +]; +const numPoints = points.length / 2; +const numLineSegments = lines.length / 2 / 2; +``` + +若将这些数据可视化,其呈现效果如下所示: + + + +线段从左至右编号为0至4, +若代码正确执行,则检测结果应为: +
    +
  • 第一个点(红色)的最近线段编号应为1
  • +
  • 第二个点(绿色)的最近线段编号应为3
  • +
+ +让我们将点数据存入缓冲区,并创建一个新缓冲区用于存储每个点计算得到的最近线段索引。 +Lets put the points in a buffer as well as make a buffer to hold the computed +closest index for each + +```js +const closestNdxBuffer = makeBuffer(gl, points.length * 4, gl.STATIC_DRAW); +const pointsBuffer = makeBuffer(gl, new Float32Array(points), gl.DYNAMIC_DRAW); +``` + +让我们创建一个纹理来存储所有线段的端点数据。 + +```js +function createDataTexture(gl, data, numComponents, internalFormat, format, type) { + const numElements = data.length / numComponents; + + // compute a size that will hold all of our data + const width = Math.ceil(Math.sqrt(numElements)); + const height = Math.ceil(numElements / width); + + const bin = new Float32Array(width * height * numComponents); + bin.set(data); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip level + internalFormat, + width, + height, + 0, // border + format, + type, + bin, + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + return {tex, dimensions: [width, height]}; +} + +const {tex: linesTex, dimensions: linesTexDimensions} = + createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT); +``` + +在本实现中,我们允许代码自动确定纹理尺寸并进行数据填充。 +例如,若输入包含7个元素的数组,系统会将其存入3×3的纹理中。 +该操作将同时返回纹理对象及其最终确定的尺寸。 +之所以自动选择尺寸,是因为纹理存在最大尺寸限制。 + +理想情况下,我们更希望将数据视为一维数组来处理(如位置一维数组、线段端点一维数组等),因此只需声明N×1的纹理即可。但GPU存在最大尺寸限制(可能低至1024或2048)。 +最大尺寸限制为1024,而我们的数组需要存储1025个值时,就必须将数据存入诸如512×2这类非方形纹理中。 +通过将数据排列为方形纹理(如1024×1024),我们可将容量上限提升至最大纹理尺寸的平方值,才会触及硬件限制。 +对于1024的尺寸限制,这种排列方式可支持超过100万值(1,048,576)的数组存储。 + +采用方形纹理布局时,只有当数据量达到最大纹理尺寸的平方时才会触及限制。 +以1024的尺寸限制为例,该方案可支持超过100万(1024×1024=1,048,576)个数据值的存储。 + +接下来编译着色器并查找变量位置。 + +```js +const closestLinePrg = createProgram( + gl, [closestLineVS, closestLineFS], ['closestNdx']); + +const closestLinePrgLocs = { + point: gl.getAttribLocation(closestLinePrg, 'point'), + linesTex: gl.getUniformLocation(closestLinePrg, 'linesTex'), + numLineSegments: gl.getUniformLocation(closestLinePrg, 'numLineSegments'), +}; +``` + +并创建这些点的顶点数组对象(VAO)。 + +```js +function makeVertexArray(gl, bufLocPairs) { + const va = gl.createVertexArray(); + gl.bindVertexArray(va); + for (const [buffer, loc] of bufLocPairs) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer( + loc, // attribute location + 2, // number of elements + gl.FLOAT, // type of data + false, // normalize + 0, // stride (0 = auto) + 0, // offset + ); + } + return va; +} + +const closestLinesVA = makeVertexArray(gl, [ + [pointsBuffer, closestLinePrgLocs.point], +]); +``` + +现在我们需要设置一个transform feedback(变换反馈),以便将结果写入 `cloestNdxBuffer` 中。 + +```js +function makeTransformFeedback(gl, buffer) { + const tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + return tf; +} + +const closestNdxTF = makeTransformFeedback(gl, closestNdxBuffer); +``` + +有了以上所有的设置,我们就可以开始渲染了。 + +```js +// compute the closest lines +gl.bindVertexArray(closestLinesVA); +gl.useProgram(closestLinePrg); +gl.uniform1i(closestLinePrgLocs.linesTex, 0); +gl.uniform1i(closestLinePrgLocs.numLineSegments, numLineSegments); + +// turn of using the fragment shader +gl.enable(gl.RASTERIZER_DISCARD); + +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, closestNdxTF); +gl.beginTransformFeedback(gl.POINTS); +gl.drawArrays(gl.POINTS, 0, numPoints); +gl.endTransformFeedback(); +gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + +// turn on using fragment shaders again +gl.disable(gl.RASTERIZER_DISCARD); +``` + +并最终读取结果。 + +```js +// get the results. +{ + const results = new Int32Array(numPoints); + gl.bindBuffer(gl.ARRAY_BUFFER, closestNdxBuffer); + gl.getBufferSubData(gl.ARRAY_BUFFER, 0, results); + log(results); +} +``` + +如果我们运行它 + +{{{example url="../webgl-gpgpu-closest-line-results-transformfeedback.html"}}} + +我们应该会得到预期的结果 `[1, 3]`。 + +从 GPU 读取数据的速度很慢。假设我们想要可视化这些结果。将这些结果读取回 JavaScript 并进行绘制会相对容易,但如果不将它们读取回 JavaScript 呢?让我们直接使用这些数据并绘制结果。 + +首先,绘制这些点相对容易,这与粒子示例相同。 +我们将每个点绘制为不同的颜色,这样就可以用相同的颜色高亮显示最近的线段。 + +```js +const drawPointsVS = `#version 300 es +in vec4 point; + +uniform float numPoints; +uniform mat4 matrix; + +out vec4 v_color; + +// converts hue, saturation, and value each in the 0 to 1 range +// to rgb. c = color, c.x = hue, c.y = saturation, c.z = value +vec3 hsv2rgb(vec3 c) { + c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + gl_Position = matrix * point; + gl_PointSize = 10.0; + + float hue = float(gl_VertexID) / numPoints; + v_color = vec4(hsv2rgb(vec3(hue, 1, 1)), 1); +} +`; + +const drawClosestLinesPointsFS = `#version 300 es +precision highp float; +in vec4 v_color; +out vec4 outColor; +void main() { + outColor = v_color; +}`; +``` + +不传入颜色,而是使用 `hsv2rgb` 生成颜色,并传入一个从 `0` 到 `1` 的色相值。 +对于 500 个点来说,可能很难区分各条线,但对于大约 10 个点,我们应该能够分辨清楚。 + +将生成的颜色传递给一个简单的片元着色器。 + +```js +const drawClosestPointsLinesFS = ` +precision highp float; +varying vec4 v_color; +void main() { + gl_FragColor = v_color; +} +`; +``` + +要绘制所有的线段,即使是那些不靠近任何点的线段,做法几乎是一样的,只不过我们不再生成颜色。 +在这种情况下,我们只是使用一个硬编码的颜色。 + +```js +const drawLinesVS = `#version 300 es +uniform sampler2D linesTex; +uniform mat4 matrix; + +out vec4 v_color; + +vec4 getAs1D(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); +} + +void main() { + ivec2 linesTexDimensions = textureSize(linesTex, 0); + + // pull the position from the texture + vec4 position = getAs1D(linesTex, linesTexDimensions, gl_VertexID); + + // do the common matrix math + gl_Position = matrix * vec4(position.xy, 0, 1); + + // just so we can use the same fragment shader + v_color = vec4(0.8, 0.8, 0.8, 1); +} +`; +``` + +我们没有使用任何属性,而是像我们在[无数据绘制](webgl-drawing-without-data.html) 中提到的那样,直接使用 `gl_VertexID`。 + +最终,绘制最近线条的功能实现如下。 + +```js +const drawClosestLinesVS = `#version 300 es +in int closestNdx; +uniform float numPoints; +uniform sampler2D linesTex; +uniform mat4 matrix; + +out vec4 v_color; + +vec4 getAs1D(sampler2D tex, ivec2 dimensions, int index) { + int y = index / dimensions.x; + int x = index % dimensions.x; + return texelFetch(tex, ivec2(x, y), 0); +} + +// converts hue, saturation, and value each in the 0 to 1 range +// to rgb. c = color, c.x = hue, c.y = saturation, c.z = value +vec3 hsv2rgb(vec3 c) { + c = vec3(c.x, clamp(c.yz, 0.0, 1.0)); + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + ivec2 linesTexDimensions = textureSize(linesTex, 0); + + // pull the position from the texture + int linePointId = closestNdx * 2 + gl_VertexID % 2; + vec4 position = getAs1D(linesTex, linesTexDimensions, linePointId); + + // do the common matrix math + gl_Position = matrix * vec4(position.xy, 0, 1); + + int pointId = gl_InstanceID; + float hue = float(pointId) / numPoints; + v_color = vec4(hsv2rgb(vec3(hue, 1, 1)), 1); +} +`; +``` + +我们将 `closestNdx` 作为一个属性传入,它们是我们之前生成的结果。 +利用它我们可以查找特定的线段。但由于每条线段需要绘制两个点, +我们将使用 [实例化绘制](webgl-instanced-drawing.html) 来为每个 `closestNdx` 绘制两个点。 +然后我们可以使用 `gl_VertexID % 2` 来选择线段的起点或终点。 + +最后,我们使用与绘制点时相同的方法来计算颜色,这样线段的颜色就会与对应的点匹配。 + +我们需要编译所有这些新的着色器程序,并查找它们的变量位置。 + +```js +const closestLinePrg = createProgram( + gl, [closestLineVS, closestLineFS], ['closestNdx']); ++const drawLinesPrg = createProgram( ++ gl, [drawLinesVS, drawClosestLinesPointsFS]); ++const drawClosestLinesPrg = createProgram( ++ gl, [drawClosestLinesVS, drawClosestLinesPointsFS]); ++const drawPointsPrg = createProgram( ++ gl, [drawPointsVS, drawClosestLinesPointsFS]); + +const closestLinePrgLocs = { + point: gl.getAttribLocation(closestLinePrg, 'point'), + linesTex: gl.getUniformLocation(closestLinePrg, 'linesTex'), + numLineSegments: gl.getUniformLocation(closestLinePrg, 'numLineSegments'), +}; ++const drawLinesPrgLocs = { ++ linesTex: gl.getUniformLocation(drawLinesPrg, 'linesTex'), ++ matrix: gl.getUniformLocation(drawLinesPrg, 'matrix'), ++}; ++const drawClosestLinesPrgLocs = { ++ closestNdx: gl.getAttribLocation(drawClosestLinesPrg, 'closestNdx'), ++ linesTex: gl.getUniformLocation(drawClosestLinesPrg, 'linesTex'), ++ matrix: gl.getUniformLocation(drawClosestLinesPrg, 'matrix'), ++ numPoints: gl.getUniformLocation(drawClosestLinesPrg, 'numPoints'), ++}; ++const drawPointsPrgLocs = { ++ point: gl.getAttribLocation(drawPointsPrg, 'point'), ++ matrix: gl.getUniformLocation(drawPointsPrg, 'matrix'), ++ numPoints: gl.getUniformLocation(drawPointsPrg, 'numPoints'), ++}; +``` + +我们需要为绘制点和最近的线段分别创建顶点数组对象(vertex arrays)。 + +```js +const closestLinesVA = makeVertexArray(gl, [ + [pointsBuffer, closestLinePrgLocs.point], +]); + ++const drawClosestLinesVA = gl.createVertexArray(); ++gl.bindVertexArray(drawClosestLinesVA); ++gl.bindBuffer(gl.ARRAY_BUFFER, closestNdxBuffer); ++gl.enableVertexAttribArray(drawClosestLinesPrgLocs.closestNdx); ++gl.vertexAttribIPointer(drawClosestLinesPrgLocs.closestNdx, 1, gl.INT, 0, 0); ++gl.vertexAttribDivisor(drawClosestLinesPrgLocs.closestNdx, 1); ++ ++const drawPointsVA = makeVertexArray(gl, [ ++ [pointsBuffer, drawPointsPrgLocs.point], ++]); +``` + +因此,在渲染时,我们像之前一样计算结果,但我们不再使用 `getBufferSubData` 来读取结果,而是将结果直接传递给相应的着色器。 + +首先,我们用灰色绘制所有的线段。 + +```js +// draw all the lines in gray +gl.bindFramebuffer(gl.FRAMEBUFFER, null); +gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + +gl.bindVertexArray(null); +gl.useProgram(drawLinesPrg); + +// bind the lines texture to texture unit 0 +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, linesTex); + +// Tell the shader to use texture on texture unit 0 +gl.uniform1i(drawLinesPrgLocs.linesTex, 0); +gl.uniformMatrix4fv(drawLinesPrgLocs.matrix, false, matrix); + +gl.drawArrays(gl.LINES, 0, numLineSegments * 2); +``` + +然后,我们绘制所有最近的线段。 + +```js +gl.bindVertexArray(drawClosestLinesVA); +gl.useProgram(drawClosestLinesPrg); + +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, linesTex); + +gl.uniform1i(drawClosestLinesPrgLocs.linesTex, 0); +gl.uniform1f(drawClosestLinesPrgLocs.numPoints, numPoints); +gl.uniformMatrix4fv(drawClosestLinesPrgLocs.matrix, false, matrix); + +gl.drawArraysInstanced(gl.LINES, 0, 2, numPoints); +``` + +最后,我们绘制每一个点。 + +```js +gl.bindVertexArray(drawPointsVA); +gl.useProgram(drawPointsPrg); + +gl.uniform1f(drawPointsPrgLocs.numPoints, numPoints); +gl.uniformMatrix4fv(drawPointsPrgLocs.matrix, false, matrix); + +gl.drawArrays(gl.POINTS, 0, numPoints); +``` + +在运行之前,我们再做一件事:添加更多的点和线段。 + +```js +-const points = [ +- 100, 100, +- 200, 100, +-]; +-const lines = [ +- 25, 50, +- 25, 150, +- 90, 50, +- 90, 150, +- 125, 50, +- 125, 150, +- 185, 50, +- 185, 150, +- 225, 50, +- 225, 150, +-]; + ++function createPoints(numPoints, ranges) { ++ const points = []; ++ for (let i = 0; i < numPoints; ++i) { ++ points.push(...ranges.map(range => r(...range))); ++ } ++ return points; ++} ++ ++const r = (min, max) => min + Math.random() * (max - min); ++ ++const points = createPoints(8, [[0, gl.canvas.width], [0, gl.canvas.height]]); ++const lines = createPoints(125 * 2, [[0, gl.canvas.width], [0, gl.canvas.height]]); +const numPoints = points.length / 2; +const numLineSegments = lines.length / 2 / 2; +``` + +如果我们运行它。 + +{{{example url="../webgl-gpgpu-closest-line-transformfeedback.html"}}} + +你可以增加点和线段的数量, +但到某个程度后,就无法分辨哪些点对应哪些线段了。 +不过在数量较少的情况下,至少可以通过视觉验证它是否正常工作。 + +为了好玩,我们来把粒子示例和这个示例结合起来。 +我们将使用在粒子示例中用于更新粒子位置的技术来更新这些点的位置。 +至于线段的端点更新,我们会像开头那样,把结果写入到纹理中。 + +为此,我们复制粒子示例中的 `updatePositionFS` 顶点着色器。 +而对于线段,由于它们的值是存储在纹理中的, +所以我们需要在片元着色器中移动它们的点。 + + +```js +const updateLinesVS = `#version 300 es +in vec4 position; +void main() { + gl_Position = position; +} +`; + +const updateLinesFS = `#version 300 es +precision highp float; + +uniform sampler2D linesTex; +uniform sampler2D velocityTex; +uniform vec2 canvasDimensions; +uniform float deltaTime; + +out vec4 outColor; + +vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); +} + +void main() { + // compute texel coord from gl_FragCoord; + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + + vec2 position = texelFetch(linesTex, texelCoord, 0).xy; + vec2 velocity = texelFetch(velocityTex, texelCoord, 0).xy; + vec2 newPosition = euclideanModulo(position + velocity * deltaTime, canvasDimensions); + + outColor = vec4(newPosition, 0, 1); +} +`; +``` + +接着,我们可以编译用于更新点和线段的两个新着色器,并查找它们的变量位置。 + +```js ++const updatePositionPrg = createProgram( ++ gl, [updatePositionVS, updatePositionFS], ['newPosition']); ++const updateLinesPrg = createProgram( ++ gl, [updateLinesVS, updateLinesFS]); +const closestLinePrg = createProgram( + gl, [closestLineVS, closestLineFS], ['closestNdx']); +const drawLinesPrg = createProgram( + gl, [drawLinesVS, drawClosestLinesPointsFS]); +const drawClosestLinesPrg = createProgram( + gl, [drawClosestLinesVS, drawClosestLinesPointsFS]); +const drawPointsPrg = createProgram( + gl, [drawPointsVS, drawClosestLinesPointsFS]); + ++const updatePositionPrgLocs = { ++ oldPosition: gl.getAttribLocation(updatePositionPrg, 'oldPosition'), ++ velocity: gl.getAttribLocation(updatePositionPrg, 'velocity'), ++ canvasDimensions: gl.getUniformLocation(updatePositionPrg, 'canvasDimensions'), ++ deltaTime: gl.getUniformLocation(updatePositionPrg, 'deltaTime'), ++}; ++const updateLinesPrgLocs = { ++ position: gl.getAttribLocation(updateLinesPrg, 'position'), ++ linesTex: gl.getUniformLocation(updateLinesPrg, 'linesTex'), ++ velocityTex: gl.getUniformLocation(updateLinesPrg, 'velocityTex'), ++ canvasDimensions: gl.getUniformLocation(updateLinesPrg, 'canvasDimensions'), ++ deltaTime: gl.getUniformLocation(updateLinesPrg, 'deltaTime'), ++}; +const closestLinePrgLocs = { + point: gl.getAttribLocation(closestLinePrg, 'point'), + linesTex: gl.getUniformLocation(closestLinePrg, 'linesTex'), + numLineSegments: gl.getUniformLocation(closestLinePrg, 'numLineSegments'), +}; +const drawLinesPrgLocs = { + linesTex: gl.getUniformLocation(drawLinesPrg, 'linesTex'), + matrix: gl.getUniformLocation(drawLinesPrg, 'matrix'), +}; +const drawClosestLinesPrgLocs = { + closestNdx: gl.getAttribLocation(drawClosestLinesPrg, 'closestNdx'), + linesTex: gl.getUniformLocation(drawClosestLinesPrg, 'linesTex'), + matrix: gl.getUniformLocation(drawClosestLinesPrg, 'matrix'), + numPoints: gl.getUniformLocation(drawClosestLinesPrg, 'numPoints'), +}; +const drawPointsPrgLocs = { + point: gl.getAttribLocation(drawPointsPrg, 'point'), + matrix: gl.getUniformLocation(drawPointsPrg, 'matrix'), + numPoints: gl.getUniformLocation(drawPointsPrg, 'numPoints'), +}; +``` + +我们需要为点和线段都生成速度。 + +```js +const points = createPoints(8, [[0, gl.canvas.width], [0, gl.canvas.height]]); +const lines = createPoints(125 * 2, [[0, gl.canvas.width], [0, gl.canvas.height]]); +const numPoints = points.length / 2; +const numLineSegments = lines.length / 2 / 2; + ++const pointVelocities = createPoints(numPoints, [[-20, 20], [-20, 20]]); ++const lineVelocities = createPoints(numLineSegments * 2, [[-20, 20], [-20, 20]]); +``` + +我们需要为点创建两个缓冲区,以便像上面处理粒子那样进行交换。 +同时也需要一个缓冲区来存储点的速度。 +此外,还需要一个从 -1 到 +1 的裁剪空间四边形(quad),用于更新线段的位置。 + +```js +const closestNdxBuffer = makeBuffer(gl, points.length * 4, gl.STATIC_DRAW); +-const pointsBuffer = makeBuffer(gl, new Float32Array(points), gl.STATIC_DRAW); ++const pointsBuffer1 = makeBuffer(gl, new Float32Array(points), gl.DYNAMIC_DRAW); ++const pointsBuffer2 = makeBuffer(gl, new Float32Array(points), gl.DYNAMIC_DRAW); ++const pointVelocitiesBuffer = makeBuffer(gl, new Float32Array(pointVelocities), gl.STATIC_DRAW); ++const quadBuffer = makeBuffer(gl, new Float32Array([ ++ -1, -1, ++ 1, -1, ++ -1, 1, ++ -1, 1, ++ 1, -1, ++ 1, 1, ++]), gl.STATIC_DRAW); +``` + +同样地,我们现在需要两个纹理来存储线段的端点, +通过相互更新并进行交换。 +此外,我们还需要一个纹理来存储线段端点的速度。 + +```js +-const {tex: linesTex, dimensions: linesTexDimensions} = +- createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT); ++const {tex: linesTex1, dimensions: linesTexDimensions1} = ++ createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT); ++const {tex: linesTex2, dimensions: linesTexDimensions2} = ++ createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT); ++const {tex: lineVelocitiesTex, dimensions: lineVelocitiesTexDimensions} = ++ createDataTexture(gl, lineVelocities, 2, gl.RG32F, gl.RG, gl.FLOAT); +``` + +我们需要创建多个顶点数组对象(vertex arrays): + +* 2 个用于更新位置: + 一个使用 `pointsBuffer1` 作为输入,另一个使用 `pointsBuffer2` 作为输入。 + +* 1 个用于更新线段时使用的裁剪空间(-1 到 +1)四边形。 + +* 2 个用于计算最近线段: + 一个读取 `pointsBuffer1` 中的点,另一个读取 `pointsBuffer2` 中的点。 + +* 2 个用于绘制点: + 一个读取 `pointsBuffer1` 中的点,另一个读取 `pointsBuffer2` 中的点。 + + +```js ++const updatePositionVA1 = makeVertexArray(gl, [ ++ [pointsBuffer1, updatePositionPrgLocs.oldPosition], ++ [pointVelocitiesBuffer, updatePositionPrgLocs.velocity], ++]); ++const updatePositionVA2 = makeVertexArray(gl, [ ++ [pointsBuffer2, updatePositionPrgLocs.oldPosition], ++ [pointVelocitiesBuffer, updatePositionPrgLocs.velocity], ++]); ++ ++const updateLinesVA = makeVertexArray(gl, [ ++ [quadBuffer, updateLinesPrgLocs.position], ++]); + +-const closestLinesVA = makeVertexArray(gl, [ +- [pointsBuffer, closestLinePrgLocs.point], +-]); ++const closestLinesVA1 = makeVertexArray(gl, [ ++ [pointsBuffer1, closestLinePrgLocs.point], ++]); ++const closestLinesVA2 = makeVertexArray(gl, [ ++ [pointsBuffer2, closestLinePrgLocs.point], ++]); + +const drawClosestLinesVA = gl.createVertexArray(); +gl.bindVertexArray(drawClosestLinesVA); +gl.bindBuffer(gl.ARRAY_BUFFER, closestNdxBuffer); +gl.enableVertexAttribArray(drawClosestLinesPrgLocs.closestNdx); +gl.vertexAttribIPointer(drawClosestLinesPrgLocs.closestNdx, 1, gl.INT, 0, 0); +gl.vertexAttribDivisor(drawClosestLinesPrgLocs.closestNdx, 1); + +-const drawPointsVA = makeVertexArray(gl, [ +- [pointsBuffer, drawPointsPrgLocs.point], +-]); ++const drawPointsVA1 = makeVertexArray(gl, [ ++ [pointsBuffer1, drawPointsPrgLocs.point], ++]); ++const drawPointsVA2 = makeVertexArray(gl, [ ++ [pointsBuffer2, drawPointsPrgLocs.point], ++]); +``` + +我们还需要另外 2 个 Transform Feedback 对象,用于更新点的位置。 + +```js +function makeTransformFeedback(gl, buffer) { + const tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer); + return tf; +} + ++const pointsTF1 = makeTransformFeedback(gl, pointsBuffer1); ++const pointsTF2 = makeTransformFeedback(gl, pointsBuffer2); + +const closestNdxTF = makeTransformFeedback(gl, closestNdxBuffer); +``` + +我们需要创建帧缓冲对象(framebuffers)用于更新线段端点: +一个用于写入 `linesTex1`,另一个用于写入 `linesTex2`。 + + +```js +function createFramebuffer(gl, tex) { + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + return fb; +} + +const linesFB1 = createFramebuffer(gl, linesTex1); +const linesFB2 = createFramebuffer(gl, linesTex2); +``` + +由于我们希望写入浮点纹理,而这在 WebGL2 中是一个可选特性, +因此我们需要通过检查 `EXT_color_buffer_float` 扩展是否可用来确认是否支持。 + +```js +// Get A WebGL context +/** @type {HTMLCanvasElement} */ +const canvas = document.querySelector("#canvas"); +const gl = canvas.getContext("webgl2"); +if (!gl) { + return; +} ++const ext = gl.getExtension('EXT_color_buffer_float'); ++if (!ext) { ++ alert('need EXT_color_buffer_float'); ++ return; ++} +``` + +我们还需要设置一些对象来跟踪当前帧和下一帧的状态, +这样每一帧我们就可以轻松地交换所需的资源。 + +```js +let current = { + // for updating points + updatePositionVA: updatePositionVA1, // read from points1 + pointsTF: pointsTF2, // write to points2 + // for updating line endings + linesTex: linesTex1, // read from linesTex1 + linesFB: linesFB2, // write to linesTex2 + // for computing closest lines + closestLinesVA: closestLinesVA2, // read from points2 + // for drawing all lines and closest lines + allLinesTex: linesTex2, // read from linesTex2 + // for drawing points + drawPointsVA: drawPointsVA2, // read form points2 +}; + +let next = { + // for updating points + updatePositionVA: updatePositionVA2, // read from points2 + pointsTF: pointsTF1, // write to points1 + // for updating line endings + linesTex: linesTex2, // read from linesTex2 + linesFB: linesFB1, // write to linesTex1 + // for computing closest lines + closestLinesVA: closestLinesVA1, // read from points1 + // for drawing all lines and closest lines + allLinesTex: linesTex1, // read from linesTex1 + // for drawing points + drawPointsVA: drawPointsVA1, // read form points1 +}; +``` + +然后我们需要一个渲染循环。 +我们将所有的部分拆分成多个函数来组织。 + +```js + +let then = 0; +function render(time) { + // convert to seconds + time *= 0.001; + // Subtract the previous time from the current time + const deltaTime = time - then; + // Remember the current time for the next frame. + then = time; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + gl.clear(gl.COLOR_BUFFER_BIT); + + updatePointPositions(deltaTime); + updateLineEndPoints(deltaTime); + computeClosestLines(); + + const matrix = m4.orthographic(0, gl.canvas.width, 0, gl.canvas.height, -1, 1); + + drawAllLines(matrix); + drawClosestLines(matrix); + drawPoints(matrix); + + // swap + { + const temp = current; + current = next; + next = temp; + } + + requestAnimationFrame(render); +} +requestAnimationFrame(render); +} +``` + +现在我们只需要填充各个部分即可。 +之前的所有部分保持不变,只是在适当的位置引用 `current`。 + + +```js +function computeClosestLines() { +- gl.bindVertexArray(closestLinesVA); ++ gl.bindVertexArray(current.closestLinesVA); + gl.useProgram(closestLinePrg); + + gl.activeTexture(gl.TEXTURE0); +- gl.bindTexture(gl.TEXTURE_2D, linesTex); ++ gl.bindTexture(gl.TEXTURE_2D, current.linesTex); + + gl.uniform1i(closestLinePrgLocs.linesTex, 0); + gl.uniform1i(closestLinePrgLocs.numLineSegments, numLineSegments); + + drawArraysWithTransformFeedback(gl, closestNdxTF, gl.POINTS, numPoints); +} + +function drawAllLines(matrix) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + gl.bindVertexArray(null); + gl.useProgram(drawLinesPrg); + + // bind the lines texture to texture unit 0 + gl.activeTexture(gl.TEXTURE0); +- gl.bindTexture(gl.TEXTURE_2D, linesTex); ++ gl.bindTexture(gl.TEXTURE_2D, current.allLinesTex); + + // Tell the shader to use texture on texture unit 0 + gl.uniform1i(drawLinesPrgLocs.linesTex, 0); + gl.uniformMatrix4fv(drawLinesPrgLocs.matrix, false, matrix); + + gl.drawArrays(gl.LINES, 0, numLineSegments * 2); +} + +function drawClosestLines(matrix) { + gl.bindVertexArray(drawClosestLinesVA); + gl.useProgram(drawClosestLinesPrg); + + gl.activeTexture(gl.TEXTURE0); +- gl.bindTexture(gl.TEXTURE_2D, linesTex); ++ gl.bindTexture(gl.TEXTURE_2D, current.allLinesTex); + + gl.uniform1i(drawClosestLinesPrgLocs.linesTex, 0); + gl.uniform1f(drawClosestLinesPrgLocs.numPoints, numPoints); + gl.uniformMatrix4fv(drawClosestLinesPrgLocs.matrix, false, matrix); + + gl.drawArraysInstanced(gl.LINES, 0, 2, numPoints); +} + +function drawPoints(matrix) { +- gl.bindVertexArray(drawPointsVA); ++ gl.bindVertexArray(current.drawPointsVA); + gl.useProgram(drawPointsPrg); + + gl.uniform1f(drawPointsPrgLocs.numPoints, numPoints); + gl.uniformMatrix4fv(drawPointsPrgLocs.matrix, false, matrix); + + gl.drawArrays(gl.POINTS, 0, numPoints); +} +``` + +我们还需要两个新函数,分别用于更新点和线段。 + +```js +function updatePointPositions(deltaTime) { + gl.bindVertexArray(current.updatePositionVA); + gl.useProgram(updatePositionPrg); + gl.uniform1f(updatePositionPrgLocs.deltaTime, deltaTime); + gl.uniform2f(updatePositionPrgLocs.canvasDimensions, gl.canvas.width, gl.canvas.height); + drawArraysWithTransformFeedback(gl, current.pointsTF, gl.POINTS, numPoints); +} + +function updateLineEndPoints(deltaTime) { + // Update the line endpoint positions --------------------- + gl.bindVertexArray(updateLinesVA); // just a quad + gl.useProgram(updateLinesPrg); + + // bind texture to texture units 0 and 1 + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, current.linesTex); + gl.activeTexture(gl.TEXTURE0 + 1); + gl.bindTexture(gl.TEXTURE_2D, lineVelocitiesTex); + + // tell the shader to look at the textures on texture units 0 and 1 + gl.uniform1i(updateLinesPrgLocs.linesTex, 0); + gl.uniform1i(updateLinesPrgLocs.velocityTex, 1); + gl.uniform1f(updateLinesPrgLocs.deltaTime, deltaTime); + gl.uniform2f(updateLinesPrgLocs.canvasDimensions, gl.canvas.width, gl.canvas.height); + + // write to the other lines texture + gl.bindFramebuffer(gl.FRAMEBUFFER, current.linesFB); + gl.viewport(0, 0, ...lineVelocitiesTexDimensions); + + // drawing a clip space -1 to +1 quad = map over entire destination array + gl.drawArrays(gl.TRIANGLES, 0, 6); +} +``` + +至此,我们就可以看到它动态运行了,所有的计算都在 GPU 上完成。 + +{{{example url="../webgl-gpgpu-closest-line-dynamic-transformfeedback.html"}}} + +## 一些关于 GPGPU 的注意事项 + +* 在 WebGL1 中,GPGPU 基本上仅限于使用二维数组(纹理)作为输出。 + WebGL2 增加了使用 Transform Feedback 来处理任意大小的一维数组的能力。 + + 如果你感兴趣,可以查看 [同一主题的 WebGL1 版本文章](https://webglfundamentals.org/webgl/lessons/webgl-gpgpu.html), + 看看如何仅使用输出到纹理的方式完成这一切。 + 当然,稍加思考其实也很容易理解其原理。 + + WebGL2 中也有使用纹理而非 Transform Feedback 的版本。因为使用 `texelFetch` 并具备更多的纹理格式选择,会让实现方式有所不同。 + + * [粒子系统](../webgl-gpgpu-particles.html) + * [最近线段的计算结果](../webgl-gpgpu-closest-line-results.html) + * [最近线段可视化](../webgl-gpgpu-closest-line.html) + * [最近线段动态演示](../webgl-gpgpu-closest-line-dynamic.html) + +* Firefox Bug + + 截至 Firefox 84 版本,[存在一个 bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1677552), + 要求在调用 `drawArraysInstanced` 时,必须至少存在一个使用除数为 0 的活动属性,否则调用会失败。 这意味着上述示例中使用 `drawArraysInstanced` 绘制最近线段的部分在 Firefox 中会失败。 + + 为了解决这个问题,我们可以创建一个仅包含 `[0, 1]` 的缓冲区, + 并将其作为一个属性用于代替之前通过 `gl_VertexID % 2` 实现的逻辑。 + 也就是说,我们不再依赖 `gl_VertexID`,而是使用这个属性来区分起点和终点。 + + ```glsl + in int endPoint; // needed by firefox + + ... + -int linePointId = closestNdx * 2 + gl_VertexID % 2; + +int linePointId = closestNdx * 2 + endPoint; + ... + ``` + + 这样就可以 [让它在 Firefox 中正常工作](../webgl/webgl-gpgpu-closest-line-dynamic-transformfeedback-ff.html)。 + +* GPU 的精度与 CPU 并不相同。 + + 请检查你的结果,确保它们在可接受的范围内。 + +* GPGPU 存在一定的开销。 + + 在上面的前几个示例中,我们使用 WebGL 计算了一些数据然后读取结果。 + 设置缓冲区和纹理、配置属性和 uniform 都需要时间。 + 这些操作的开销足以让一些规模较小的任务更适合直接用 JavaScript 完成。 + 比如那些将 6 个数相乘或对 3 对数字求和的示例,数据量太小,使用 GPGPU 根本得不偿失。 + 到底在哪个规模点 GPGPU 才值得使用,这个临界值并不明确。你可以自行尝试。但可以大致估计,如果你处理的对象少于 1000 个,那就还是用 JavaScript 更合适。 + +* `readPixels` 和 `getBufferSubData` 的速度很慢。 + + 从 WebGL 读取结果是很慢的操作,因此尽可能避免读取是非常重要的。 + 例如,上面的粒子系统和动态最近线段的示例都**从未**将结果读取回 JavaScript。 + 只要有可能,就尽量让结果保留在 GPU 上。 换句话说,你可以这样做: + + * 在 GPU 上计算内容 + * 读取结果 + * 为下一步准备结果 + * 将准备好的结果上传到 GPU + * 在 GPU 上继续计算 + * 读取结果 + * 为下一步准备结果 + * 将准备好的结果上传到 GPU + * 在 GPU 上继续计算 + * 读取结果 + + 而如果通过一些巧妙的设计,效率会高得多,比如: + + * 在 GPU 上计算内容 + * 使用 GPU 为下一步准备结果 + * 在 GPU 上继续计算 + * 使用 GPU 为下一步准备结果 + * 在 GPU 上继续计算 + * 最后再读取结果 + + 我们的动态最近线段示例就是这样做的:结果从未离开 GPU。 + + 再举一个例子:我曾经写过一个计算直方图的着色器,最初我是将结果读取回 JavaScript,计算出最小值和最大值,然后再使用这些最小值和最大值作为 uniform 参数,把图像绘制回 canvas,实现图像自动拉伸(auto-level)。 + + 但后来我发现,与其将直方图读取回 JavaScript, + 不如直接在 GPU 上运行一个着色器,让它对直方图纹理进行处理, + 输出一个 2 像素的纹理,分别存储最小值和最大值。 + + 然后我可以将这张 2 像素的纹理传入第三个着色器, + 让它在 GPU 内部读取最小值和最大值来做图像处理, + 无需再从 GPU 中读取数据来设置 uniform。 + + 类似地,为了显示直方图本身, + 起初我也是从 GPU 读取直方图数据, + 但后来我改为编写一个着色器,直接在 GPU 上可视化直方图, + 完全不需要将数据读取回 JavaScript。 + + 通过这种方式,整个处理流程都保持在 GPU 上进行, + 性能更高,效率更好。 + +* GPU能够并行处理许多任务,但大多数无法像CPU那样进行多任务处理。GPU通常无法实现"[抢占式多任务处理](https://www.google.com/search?q=preemptive+multitasking)"。这意味着如果你运行一个非常复杂的着色器,比如需要5分钟才能完成,它可能会导致你的整个机器冻结5分钟。 + 大多数完善的操作系统会通过CPU检查自上次向GPU发送命令后经过的时间来处理这个问题。如果时间过长(5-6秒)且GPU没有响应,它们唯一的选择就是重置GPU。 + + 这也是WebGL可能会*丢失上下文*并出现"Aw,rats!"或类似消息的原因之一。 + + 虽然很容易让GPU超负荷工作,但在图形处理中,通常不会达到5-6秒的程度。更多是0.1秒级别的情况,这虽然也不理想,但通常我们希望图形能快速运行,因此程序员应该会进行优化或采用其他技术来保持应用程序的响应性。 + + 另一方面,在GPGPU计算中,你可能确实需要让GPU运行繁重任务。这里没有简单的解决方案。手机的GPU性能远低于高端PC。除了自己进行计时外,没有确切的方法知道在GPU变得"太慢"之前可以给它分配多少工作量。 + + 我没有现成的解决方案可以提供。只是提醒一下,根据你要实现的功能,可能会遇到这个问题。 + +* 移动设备通常不支持渲染到浮点纹理。 + + 有几种方法可以解决这个问题。其中一种是使用GLSL函数: + `floatBitsToInt`、`floatBitsToUint`、`intBitsToFloat`和`uintBitsToFloat`。 + + 例如,[基于纹理的粒子示例](../webgl-gpgpu-particles.html)需要写入浮点纹理。我们可以通过将纹理声明为`RG32I`类型(32位整数纹理)但仍上传浮点数数据来解决这个问题。 + + 在着色器中,我们需要将纹理作为整数读取,将其解码为浮点数,然后将结果重新编码为整数。例如: + + ```glsl + #version 300 es + precision highp float; + + -uniform highp sampler2D positionTex; + -uniform highp sampler2D velocityTex; + +uniform highp isampler2D positionTex; + +uniform highp isampler2D velocityTex; + uniform vec2 canvasDimensions; + uniform float deltaTime; + + out ivec4 outColor; + + vec2 euclideanModulo(vec2 n, vec2 m) { + return mod(mod(n, m) + m, m); + } + + void main() { + // there will be one velocity per position + // so the velocity texture and position texture + // are the same size. + + // further, we're generating new positions + // so we know our destination is the same size + // as our source + + // compute texcoord from gl_FragCoord; + ivec2 texelCoord = ivec2(gl_FragCoord.xy); + + - vec2 position = texelFetch(positionTex, texelCoord, 0).xy; + - vec2 velocity = texelFetch(velocityTex, texelCoord, 0).xy; + + vec2 position = intBitsToFloat(texelFetch(positionTex, texelCoord, 0).xy); + + vec2 velocity = intBitsToFloat(texelFetch(velocityTex, texelCoord, 0).xy); + vec2 newPosition = euclideanModulo(position + velocity * deltaTime, canvasDimensions); + + - outColor = vec4(newPosition, 0, 1); + + outColor = ivec4(floatBitsToInt(newPosition), 0, 1); + } + ``` + + [这里有一个可运行的示例](../webgl-gpgpu-particles-no-floating-point-textures.html) + +希望这些示例能帮助您理解WebGL中GPGPU的核心概念——关键在于WebGL读写的是**数据**数组,而非像素。 + +着色器的工作机制类似于`map`函数——每个被调用的处理函数并不能决定其返回值的存储位置,这个决策完全由外部控制。在WebGL中,这个控制权取决于您配置的绘制方式。当调用`gl.drawXXX`时,系统会为每个需要计算的值调用着色器,询问"这个位置应该生成什么值?" + +整个过程就是如此简单直接 .]() + +--- + +既然我们已经用GPGPU创建了一些粒子,[这个精彩的视频](https://www.youtube.com/watch?v=X-iSQQgOd1A)后半段使用计算着色器实现了"粘液"模拟。 + +运用上述技术,这里有一个WebGL2的实现版本。 + From 4916450c2f27995b12f219329f4cb6e7ef834038 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Sun, 22 Jun 2025 23:55:43 +0800 Subject: [PATCH 06/22] fixed translate bug --- webgl/lessons/zh_cn/webgl-gpgpu.md | 12 +- webgl/lessons/zh_cn/webgl-pulling-vertices.md | 456 ++++++++++++++++++ 2 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 webgl/lessons/zh_cn/webgl-pulling-vertices.md diff --git a/webgl/lessons/zh_cn/webgl-gpgpu.md b/webgl/lessons/zh_cn/webgl-gpgpu.md index 8d3d91291..e9cba9792 100644 --- a/webgl/lessons/zh_cn/webgl-gpgpu.md +++ b/webgl/lessons/zh_cn/webgl-gpgpu.md @@ -1997,22 +1997,22 @@ function updateLineEndPoints(deltaTime) { * 最后再读取结果 我们的动态最近线段示例就是这样做的:结果从未离开 GPU。 - + 再举一个例子:我曾经写过一个计算直方图的着色器,最初我是将结果读取回 JavaScript,计算出最小值和最大值,然后再使用这些最小值和最大值作为 uniform 参数,把图像绘制回 canvas,实现图像自动拉伸(auto-level)。 - + 但后来我发现,与其将直方图读取回 JavaScript, 不如直接在 GPU 上运行一个着色器,让它对直方图纹理进行处理, 输出一个 2 像素的纹理,分别存储最小值和最大值。 - + 然后我可以将这张 2 像素的纹理传入第三个着色器, 让它在 GPU 内部读取最小值和最大值来做图像处理, 无需再从 GPU 中读取数据来设置 uniform。 - + 类似地,为了显示直方图本身, 起初我也是从 GPU 读取直方图数据, 但后来我改为编写一个着色器,直接在 GPU 上可视化直方图, 完全不需要将数据读取回 JavaScript。 - + 通过这种方式,整个处理流程都保持在 GPU 上进行, 性能更高,效率更好。 @@ -2082,7 +2082,7 @@ function updateLineEndPoints(deltaTime) { 着色器的工作机制类似于`map`函数——每个被调用的处理函数并不能决定其返回值的存储位置,这个决策完全由外部控制。在WebGL中,这个控制权取决于您配置的绘制方式。当调用`gl.drawXXX`时,系统会为每个需要计算的值调用着色器,询问"这个位置应该生成什么值?" -整个过程就是如此简单直接 .]() +整个过程就是如此简单直接。 --- diff --git a/webgl/lessons/zh_cn/webgl-pulling-vertices.md b/webgl/lessons/zh_cn/webgl-pulling-vertices.md new file mode 100644 index 000000000..99bcf244e --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-pulling-vertices.md @@ -0,0 +1,456 @@ +Title: WebGL2 Pulling Vertices +Description: Using independent indices +TOC: Pulling Vertices + +This article assumes you've read many of the other articles +starting with [the fundamentals](webgl-fundamentals.html). +If you have not read them please start there first. + +Traditionally, WebGL apps put geometry data in buffers. +They then use attributes to automatically supply vertex data from those buffers +to the vertex shader where the programmer provides code to convert them to clip space. + +The word **traditionally** is important. It's only a **tradition** +to do it this way. It is in no way a requirement. WebGL doesn't +care how we do it, it only cares that our vertex shaders +assign clip space coordinates to `gl_Position`. + +Let's draw a texture mapped cube using code like the examples in [the article on textures](webgl-3d-textures.html). +We're told we need at least 24 unique vertices. This is because even though there are only 8 corner +positions the same corner gets used on 3 different faces of the +cube and each face needs different texture coordinates. + +
+ +In the diagram above we can see that the left face's use of corner 3 needs +texture coordinates 1,1 but the right face's use of corner 3 needs texture coordinates +0,1. The top face would need different texture coordinates as well. + +This is usually accomplished by expanding from 8 corner positions +to 24 vertices + +```js + // front + { pos: [-1, -1, 1], uv: [0, 1], }, // 0 + { pos: [ 1, -1, 1], uv: [1, 1], }, // 1 + { pos: [-1, 1, 1], uv: [0, 0], }, // 2 + { pos: [ 1, 1, 1], uv: [1, 0], }, // 3 + // right + { pos: [ 1, -1, 1], uv: [0, 1], }, // 4 + { pos: [ 1, -1, -1], uv: [1, 1], }, // 5 + { pos: [ 1, 1, 1], uv: [0, 0], }, // 6 + { pos: [ 1, 1, -1], uv: [1, 0], }, // 7 + // back + { pos: [ 1, -1, -1], uv: [0, 1], }, // 8 + { pos: [-1, -1, -1], uv: [1, 1], }, // 9 + { pos: [ 1, 1, -1], uv: [0, 0], }, // 10 + { pos: [-1, 1, -1], uv: [1, 0], }, // 11 + // left + { pos: [-1, -1, -1], uv: [0, 1], }, // 12 + { pos: [-1, -1, 1], uv: [1, 1], }, // 13 + { pos: [-1, 1, -1], uv: [0, 0], }, // 14 + { pos: [-1, 1, 1], uv: [1, 0], }, // 15 + // top + { pos: [ 1, 1, -1], uv: [0, 1], }, // 16 + { pos: [-1, 1, -1], uv: [1, 1], }, // 17 + { pos: [ 1, 1, 1], uv: [0, 0], }, // 18 + { pos: [-1, 1, 1], uv: [1, 0], }, // 19 + // bottom + { pos: [ 1, -1, 1], uv: [0, 1], }, // 20 + { pos: [-1, -1, 1], uv: [1, 1], }, // 21 + { pos: [ 1, -1, -1], uv: [0, 0], }, // 22 + { pos: [-1, -1, -1], uv: [1, 0], }, // 23 +``` + +Those positions and texture coordinates are +put into buffers and provided to the vertex shader +via attributes. + +But do we really need to do it this way? What if +we wanted to actually have just the 8 corners +and 4 texture coordinates. Something like + +```js +positions = [ + -1, -1, 1, // 0 + 1, -1, 1, // 1 + -1, 1, 1, // 2 + 1, 1, 1, // 3 + -1, -1, -1, // 4 + 1, -1, -1, // 5 + -1, 1, -1, // 6 + 1, 1, -1, // 7 +]; +uvs = [ + 0, 0, // 0 + 1, 0, // 1 + 0, 1, // 2 + 1, 1, // 3 +]; +``` + +And then for each of the 24 vertices we'd specify which of those +to use. + +```js +positionIndexUVIndex = [ + // front + 0, 1, // 0 + 1, 3, // 1 + 2, 0, // 2 + 3, 2, // 3 + // right + 1, 1, // 4 + 5, 3, // 5 + 3, 0, // 6 + 7, 2, // 7 + // back + 5, 1, // 8 + 4, 3, // 9 + 7, 0, // 10 + 6, 2, // 11 + // left + 4, 1, // 12 + 0, 3, // 13 + 6, 0, // 14 + 2, 2, // 15 + // top + 7, 1, // 16 + 6, 3, // 17 + 3, 0, // 18 + 2, 2, // 19 + // bottom + 1, 1, // 20 + 0, 3, // 21 + 5, 0, // 22 + 4, 2, // 23 +]; +``` + +Could we use this on the GPU? Why not!? + +We'll upload the positions and texture coordinates +each to their own textures like +we covered in [the article on data textures](webgl-data-textures.html). + +```js +function makeDataTexture(gl, data, numComponents) { + // expand the data to 4 values per pixel. + const numElements = data.length / numComponents; + const expandedData = new Float32Array(numElements * 4); + for (let i = 0; i < numElements; ++i) { + const srcOff = i * numComponents; + const dstOff = i * 4; + for (let j = 0; j < numComponents; ++j) { + expandedData[dstOff + j] = data[srcOff + j]; + } + } + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D( + gl.TEXTURE_2D, + 0, // mip level + gl.RGBA32F, // format + numElements, // width + 1, // height + 0, // border + gl.RGBA, // format + gl.FLOAT, // type + expandedData, + ); + // we don't need any filtering + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + return tex; +} + +const positionTexture = makeDataTexture(gl, positions, 3); +const texcoordTexture = makeDataTexture(gl, uvs, 2); +``` + +Since textures have up to 4 values per pixel `makeDataTexture` +expands whatever data we give it to 4 values per pixel. + +Then we'll create a vertex array to hold our attribute state + +```js +// create a vertex array object to hold attribute state +const vao = gl.createVertexArray(); +gl.bindVertexArray(vao); +``` + + +Next we need upload the position and texcoord indices to a buffer. + +```js +// Create a buffer for the position and UV indices +const positionIndexUVIndexBuffer = gl.createBuffer(); +// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer) +gl.bindBuffer(gl.ARRAY_BUFFER, positionIndexUVIndexBuffer); +// Put the position and texcoord indices in the buffer +gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(positionIndexUVIndex), gl.STATIC_DRAW); +``` + +and setup the attribute + +```js +// Turn on the position index attribute +gl.enableVertexAttribArray(posTexIndexLoc); + +// Tell the position/texcoord index attribute how to get data out +// of positionIndexUVIndexBuffer (ARRAY_BUFFER) +{ + const size = 2; // 2 components per iteration + const type = gl.INT; // the data is 32bit integers + const stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position + const offset = 0; // start at the beginning of the buffer + gl.vertexAttribIPointer( + posTexIndexLoc, size, type, stride, offset); +} +``` + +Notice we're calling `gl.vertexAttribIPointer` not `gl.vertexAttribPointer`. +The `I` is for integer and is used for integer and unsigned integer attributes. +Also note the size is 2, since there is 1 position index and 1 texcoord +index per vertex. + +Even though we only need 24 vertices we still need draw 6 faces, 12 triangles +each, 3 vertices per triangle for 36 vertices. To tell it which 6 vertices +to use for each face we'll use [vertex indices](webgl-indexed-vertices.html). + +```js +const indices = [ + 0, 1, 2, 2, 1, 3, // front + 4, 5, 6, 6, 5, 7, // right + 8, 9, 10, 10, 9, 11, // back + 12, 13, 14, 14, 13, 15, // left + 16, 17, 18, 18, 17, 19, // top + 20, 21, 22, 22, 21, 23, // bottom +]; +// Create an index buffer +const indexBuffer = gl.createBuffer(); +gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); +// Put the indices in the buffer +gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); +``` + +As we want to draw an image on the cube itself we need a 3rd texture +with that image. Let's just make another 4x4 data texture with a checkerboard. +We'll use `gl.LUMINANCE` as the format since then we only need one byte per pixel. + +```js +// Create a checker texture. +const checkerTexture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, checkerTexture); +// Fill the texture with a 4x4 gray checkerboard. +gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.LUMINANCE, + 4, + 4, + 0, + gl.LUMINANCE, + gl.UNSIGNED_BYTE, + new Uint8Array([ + 0xDD, 0x99, 0xDD, 0xAA, + 0x88, 0xCC, 0x88, 0xDD, + 0xCC, 0x88, 0xCC, 0xAA, + 0x88, 0xCC, 0x88, 0xCC, + ]), +); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); +gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +``` + +On to the vertex shader... We can look up a pixel from the texture like +this + +```glsl +vec4 color = texelFetch(sampler2D tex, ivec2 pixelCoord, int mipLevel); +``` + +So given an integer pixel coordinate the code above will pull out a pixel value. + +Using the `texelFetch` function we can take a 1D array index +and lookup a value out of a 2D texture like this + +```glsl +vec4 getValueByIndexFromTexture(sampler2D tex, int index) { + int texWidth = textureSize(tex, 0).x; + int col = index % texWidth; + int row = index / texWidth; + return texelFetch(tex, ivec2(col, row), 0); +} +``` + +So given that function here is our shader + +```glsl +#version 300 es +in ivec2 positionAndTexcoordIndices; + +uniform sampler2D positionTexture; +uniform sampler2D texcoordTexture; + +uniform mat4 u_matrix; + +out vec2 v_texcoord; + +vec4 getValueByIndexFromTexture(sampler2D tex, int index) { + int texWidth = textureSize(tex, 0).x; + int col = index % texWidth; + int row = index / texWidth; + return texelFetch(tex, ivec2(col, row), 0); +} + +void main() { + int positionIndex = positionAndTexcoordIndices.x; + vec3 position = getValueByIndexFromTexture( + positionTexture, positionIndex).xyz; + + // Multiply the position by the matrix. + gl_Position = u_matrix * vec4(position, 1); + + int texcoordIndex = positionAndTexcoordIndices.y; + vec2 texcoord = getValueByIndexFromTexture( + texcoordTexture, texcoordIndex).xy; + + // Pass the texcoord to the fragment shader. + v_texcoord = texcoord; +} +``` + +At the bottom it's effectively the same shader we used +in [the article on textures](webgl-3d-textures.html). +We multiply a `position` by `u_matrix` and we output +a texcoord to `v_texcoord` to pass on the fragment shader. + +The difference is only in how we get the position and +texcoord. We're using the indices passed in and getting +those values from their respective textures. + +To use the shader we need to lookup all the locations + +```js +// setup GLSL program +const program = webglUtils.createProgramFromSources(gl, [vs, fs]); + ++// look up where the vertex data needs to go. ++const posTexIndexLoc = gl.getAttribLocation( ++ program, "positionAndTexcoordIndices"); ++ ++// lookup uniforms ++const matrixLoc = gl.getUniformLocation(program, "u_matrix"); ++const positionTexLoc = gl.getUniformLocation(program, "positionTexture"); ++const texcoordTexLoc = gl.getUniformLocation(program, "texcoordTexture"); ++const u_textureLoc = gl.getUniformLocation(program, "u_texture"); +``` + +At render time we setup the attributes + +```js +// Tell it to use our program (pair of shaders) +gl.useProgram(program); + +// Set the buffer and attribute state +gl.bindVertexArray(vao); +``` + +Then we need to bind all 3 textures and setup all the +uniforms + +```js +// Set the matrix. +gl.uniformMatrix4fv(matrixLoc, false, matrix); + +// put the position texture on texture unit 0 +gl.activeTexture(gl.TEXTURE0); +gl.bindTexture(gl.TEXTURE_2D, positionTexture); +// Tell the shader to use texture unit 0 for positionTexture +gl.uniform1i(positionTexLoc, 0); + +// put the texcoord texture on texture unit 1 +gl.activeTexture(gl.TEXTURE0 + 1); +gl.bindTexture(gl.TEXTURE_2D, texcoordTexture); +// Tell the shader to use texture unit 1 for texcoordTexture +gl.uniform1i(texcoordTexLoc, 1); + +// put the checkerboard texture on texture unit 2 +gl.activeTexture(gl.TEXTURE0 + 2); +gl.bindTexture(gl.TEXTURE_2D, checkerTexture); +// Tell the shader to use texture unit 2 for u_texture +gl.uniform1i(u_textureLoc, 2); +``` + +And finally draw + +```js +// Draw the geometry. +gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0); +``` + +And we get a textured cube using only 8 positions and +4 texture coordinates + +{{{example url="../webgl-pulling-vertices.html"}}} + +Some things to note. The code is lazy and uses 1D +textures for the positions and texture coordinates. +Textures can only be so wide. [How wide is machine +specific](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE) which you can query with + +```js +const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); +``` + +If we wanted to handle more data than that we'd need +to pick some texture size that fits our data, and spread +the data across multiple rows possibly +padding the last row to make a rectangle. + +Another thing we're doing here is using 2 textures, +one for positions, one for texture coordinates. +There is no reason we couldn't put both data in the +same texture either interleaved + + pos,uv,pos,uv,pos,uv... + +or in different places in the texture + + pos,pos,pos,... + uv, uv, uv,... + +We'd just have to change the math in the vertex shader +that computes how to pull them out of the texture. + +The question comes up, should you do things like this? +The answer is "it depends". Depending on the GPU this +might be slower than the more traditional way. + +The point of this article was to point out yet again, +WebGL doesn't care how you set `gl_Position` with +clip space coordinates nor does it care how you +output a color. All it cares is that you set them. +Textures are really just 2D arrays of random access +data. + +When you have a problem you want to solve in WebGL +remember that WebGL just runs shaders and those shaders +have access to data via uniforms (global variables), +attributes (data that comes per vertex shader iteration), +and textures (random access 2D arrays). Don't let the +traditional ways of using WebGL prevent you from +seeing the real flexibility that's there. + +
+

Why is it called Vertex Pulling?

+

I'd actually only heard the term recently (July 2019) +even though I'd used the technique before. It comes +from OpenGL Insights "Programmable Vertex Pulling" article by Daniel Rakos. +

+

It's called vertex *pulling* since it's the vertex shader +that decides which vertex data to read vs the traditional way where +vertex data is supplied automatically via attributes. Effectively +the vertex shader is *pulling* data out of memory.

+
From e2021edcc2511a286601c35708b2137467b34e8a Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 07:05:34 +0800 Subject: [PATCH 07/22] Translate zh_cn webgl-pulling-vertices.md --- webgl/lessons/zh_cn/webgl-pulling-vertices.md | 186 ++++++++---------- 1 file changed, 79 insertions(+), 107 deletions(-) diff --git a/webgl/lessons/zh_cn/webgl-pulling-vertices.md b/webgl/lessons/zh_cn/webgl-pulling-vertices.md index 99bcf244e..0dcc7cc29 100644 --- a/webgl/lessons/zh_cn/webgl-pulling-vertices.md +++ b/webgl/lessons/zh_cn/webgl-pulling-vertices.md @@ -1,33 +1,23 @@ -Title: WebGL2 Pulling Vertices -Description: Using independent indices -TOC: Pulling Vertices +Title: WebGL2 顶点拉取 +Description: 使用独立索引 +TOC: 顶点拉取 -This article assumes you've read many of the other articles -starting with [the fundamentals](webgl-fundamentals.html). -If you have not read them please start there first. +本文假设你已经阅读了其他许多文章,从 [基础知识](webgl-fundamentals.html) 开始。如果你还没有阅读它们,请先从那里开始。 -Traditionally, WebGL apps put geometry data in buffers. -They then use attributes to automatically supply vertex data from those buffers -to the vertex shader where the programmer provides code to convert them to clip space. +传统上,WebGL应用会将几何数据放入缓冲区中,然后通过属性(attributes)自动将这些缓冲区中的顶点数据传递给顶点着色器,由程序员编写代码将其转换为裁剪空间(clip space)坐标。 -The word **traditionally** is important. It's only a **tradition** -to do it this way. It is in no way a requirement. WebGL doesn't -care how we do it, it only cares that our vertex shaders -assign clip space coordinates to `gl_Position`. +这里的 **“传统上”** 非常重要。 +这只是一种**传统做法**,并不是必须如此。 +WebGL 并不关心我们是如何处理的,它只关心顶点着色器是否为 `gl_Position` 赋予了裁剪空间坐标。 -Let's draw a texture mapped cube using code like the examples in [the article on textures](webgl-3d-textures.html). -We're told we need at least 24 unique vertices. This is because even though there are only 8 corner -positions the same corner gets used on 3 different faces of the -cube and each face needs different texture coordinates. +让我们使用类似于 [纹理](webgl-3d-textures.html) 中示例的方式,绘制一个带纹理映射的立方体。我们通常会说需要至少 24 个唯一顶点,这是因为虽然立方体只有 8 个角点位置,但每个角点会出现在立方体的 3 个不同面上,而每个面又需要不同的纹理坐标。
-In the diagram above we can see that the left face's use of corner 3 needs -texture coordinates 1,1 but the right face's use of corner 3 needs texture coordinates -0,1. The top face would need different texture coordinates as well. +在上面的图示中,我们可以看到左侧面的角点 3 需要的纹理坐标是 (1,1),而右侧面对角点 3 的使用则需要纹理坐标 (0,1)。顶部面则会需要另一组不同的纹理坐标。 + +通常,我们是通过将 8 个角点位置扩展为 24 个顶点来实现这一点的。 -This is usually accomplished by expanding from 8 corner positions -to 24 vertices ```js // front @@ -62,13 +52,10 @@ to 24 vertices { pos: [-1, -1, -1], uv: [1, 0], }, // 23 ``` -Those positions and texture coordinates are -put into buffers and provided to the vertex shader -via attributes. +这些位置和纹理坐标通常会被放入缓冲区中,并通过属性传递给顶点着色器。 -But do we really need to do it this way? What if -we wanted to actually have just the 8 corners -and 4 texture coordinates. Something like +但我们真的需要以这种方式来做吗?如果我们实际上只想保留 8 个角点和 4 个纹理坐标,会怎样? +类似于下面这样: ```js positions = [ @@ -89,8 +76,7 @@ uvs = [ ]; ``` -And then for each of the 24 vertices we'd specify which of those -to use. +然后,对于这 24 个顶点中的每一个,我们指定要使用哪一个位置和哪一个纹理坐标。 ```js positionIndexUVIndex = [ @@ -127,11 +113,9 @@ positionIndexUVIndex = [ ]; ``` -Could we use this on the GPU? Why not!? +我们能在 GPU 上使用这种方式吗?为什么不可以! -We'll upload the positions and texture coordinates -each to their own textures like -we covered in [the article on data textures](webgl-data-textures.html). +我们会将位置和纹理坐标分别上传到各自的纹理中,就像我们在 [数据纹理](webgl-data-textures.html) 中讲到的那样。 ```js function makeDataTexture(gl, data, numComponents) { @@ -168,10 +152,10 @@ const positionTexture = makeDataTexture(gl, positions, 3); const texcoordTexture = makeDataTexture(gl, uvs, 2); ``` -Since textures have up to 4 values per pixel `makeDataTexture` -expands whatever data we give it to 4 values per pixel. +由于纹理每个像素最多可以存储 4 个值,`makeDataTexture` 会将我们提供的数据扩展为每像素 4 个值。 + +接着,我们会创建一个顶点数组对象(vertex array)来保存我们的属性状态。 -Then we'll create a vertex array to hold our attribute state ```js // create a vertex array object to hold attribute state @@ -191,7 +175,7 @@ gl.bindBuffer(gl.ARRAY_BUFFER, positionIndexUVIndexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(positionIndexUVIndex), gl.STATIC_DRAW); ``` -and setup the attribute +接下来,我们需要将位置索引和纹理坐标索引上传到一个缓冲区。 ```js // Turn on the position index attribute @@ -209,14 +193,13 @@ gl.enableVertexAttribArray(posTexIndexLoc); } ``` -Notice we're calling `gl.vertexAttribIPointer` not `gl.vertexAttribPointer`. -The `I` is for integer and is used for integer and unsigned integer attributes. -Also note the size is 2, since there is 1 position index and 1 texcoord -index per vertex. +注意这里调用的是 `gl.vertexAttribIPointer`,而不是 `gl.vertexAttribPointer`。 +其中的 `I` 表示整数,用于整数和无符号整数类型的属性。 +另外,`size` 设置为 2,因为每个顶点包含 1 个位置索引和 1 个纹理坐标索引。 + +虽然我们只需要 24 个顶点,但绘制 6 个面,每个面 12 个三角形,每个三角形 3 个顶点,总共 36 个顶点。 +为了指定每个面使用哪 6 个顶点,我们将使用 [顶点索引](webgl-indexed-vertices.html)。 -Even though we only need 24 vertices we still need draw 6 faces, 12 triangles -each, 3 vertices per triangle for 36 vertices. To tell it which 6 vertices -to use for each face we'll use [vertex indices](webgl-indexed-vertices.html). ```js const indices = [ @@ -234,9 +217,10 @@ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); ``` -As we want to draw an image on the cube itself we need a 3rd texture -with that image. Let's just make another 4x4 data texture with a checkerboard. -We'll use `gl.LUMINANCE` as the format since then we only need one byte per pixel. +由于我们想要在立方体上绘制一张图像,因此还需要第三个纹理存储这张图像。 +这里我们用一个 4x4 的数据纹理,内容是棋盘格图案。 +纹理格式使用 `gl.LUMINANCE`,因为这样每个像素只需要一个字节。 + ```js // Create a checker texture. @@ -263,17 +247,16 @@ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); ``` -On to the vertex shader... We can look up a pixel from the texture like -this +接下来是顶点着色器…… +我们可以像这样从纹理中查找一个像素: ```glsl vec4 color = texelFetch(sampler2D tex, ivec2 pixelCoord, int mipLevel); ``` -So given an integer pixel coordinate the code above will pull out a pixel value. +因此,给定一个整数像素坐标,上述代码将提取出对应的像素值。 -Using the `texelFetch` function we can take a 1D array index -and lookup a value out of a 2D texture like this +使用 `texelFetch` 函数,我们可以将一维数组索引转换为二维纹理坐标,并从二维纹理中查找对应的值,方式如下: ```glsl vec4 getValueByIndexFromTexture(sampler2D tex, int index) { @@ -284,7 +267,7 @@ vec4 getValueByIndexFromTexture(sampler2D tex, int index) { } ``` -So given that function here is our shader +有了这个函数,我们的着色器如下所示: ```glsl #version 300 es @@ -321,16 +304,11 @@ void main() { } ``` -At the bottom it's effectively the same shader we used -in [the article on textures](webgl-3d-textures.html). -We multiply a `position` by `u_matrix` and we output -a texcoord to `v_texcoord` to pass on the fragment shader. +在底部,它实际上和我们在 [纹理](webgl-3d-textures.html) 中使用的着色器是一样的。我们将 `position` 与 `u_matrix` 相乘,并将纹理坐标输出到 `v_texcoord`,以传递给片元着色器。 -The difference is only in how we get the position and -texcoord. We're using the indices passed in and getting -those values from their respective textures. +不同之处仅在于我们获取 `position` 和 `texcoord` 的方式。我们使用传入的索引,从各自的纹理中提取这些值。 -To use the shader we need to lookup all the locations +要使用这个着色器,我们需要查找所有相关的变量位置。 ```js // setup GLSL program @@ -347,7 +325,7 @@ const program = webglUtils.createProgramFromSources(gl, [vs, fs]); +const u_textureLoc = gl.getUniformLocation(program, "u_texture"); ``` -At render time we setup the attributes +在渲染阶段,我们设置属性(attributes)。 ```js // Tell it to use our program (pair of shaders) @@ -357,8 +335,7 @@ gl.useProgram(program); gl.bindVertexArray(vao); ``` -Then we need to bind all 3 textures and setup all the -uniforms +然后,我们需要绑定全部 3 个纹理,并设置所有的 uniform。 ```js // Set the matrix. @@ -383,74 +360,69 @@ gl.bindTexture(gl.TEXTURE_2D, checkerTexture); gl.uniform1i(u_textureLoc, 2); ``` -And finally draw +最后,执行绘制操作。 ```js // Draw the geometry. gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0); ``` -And we get a textured cube using only 8 positions and -4 texture coordinates +最终,我们只使用了 8 个位置和 4 个纹理坐标,就得到了一个带贴图的立方体。 {{{example url="../webgl-pulling-vertices.html"}}} -Some things to note. The code is lazy and uses 1D -textures for the positions and texture coordinates. -Textures can only be so wide. [How wide is machine -specific](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE) which you can query with +有几点需要注意:代码实现较为简化,使用了 1D 纹理来存储位置和纹理坐标。 +但纹理的宽度是有限的,[具体有多宽依赖于硬件](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE), +你可以通过以下方式查询: ```js const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); ``` -If we wanted to handle more data than that we'd need -to pick some texture size that fits our data, and spread -the data across multiple rows possibly -padding the last row to make a rectangle. +如果我们想处理比最大纹理宽度还多的数据,就需要选择一个合适的纹理尺寸,并将数据分布到多行中,可能还需要对最后一行进行填充以保持矩形结构。 + +我们在这里还做了另一件事:使用了两张纹理,一张存储位置,另一张存储纹理坐标。 +其实我们完全可以将这两类数据存储在同一张纹理中,例如交错(interleaved)存储。 -Another thing we're doing here is using 2 textures, -one for positions, one for texture coordinates. -There is no reason we couldn't put both data in the -same texture either interleaved pos,uv,pos,uv,pos,uv... -or in different places in the texture +或者将它们存储在纹理的不同区域。 pos,pos,pos,... uv, uv, uv,... -We'd just have to change the math in the vertex shader -that computes how to pull them out of the texture. +我们只需要修改顶点着色器中的数学逻辑,以正确地从纹理中提取对应的数据。 + +那么问题来了:是否应该用这种方式? +答案是“视情况而定”。具体效果可能因 GPU 而异,有些情况下这比传统方式还慢。 + +本文的重点再次强调: +WebGL 并不在意你是如何为 `gl_Position` 设置裁剪空间坐标的,也不在意你是如何输出颜色的。它只关心你是否设置了这些值。纹理,本质上只是可以随机访问的二维数组。 + +当你在 WebGL 中遇到问题时,请记住,WebGL 只是运行一些着色器程序,而这些着色器可以通过以下方式访问数据。 -The question comes up, should you do things like this? -The answer is "it depends". Depending on the GPU this -might be slower than the more traditional way. +- uniforms(全局变量) +- attributes(每个顶点着色器执行时接收的数据) +- textures(可以随机访问的二维数组) -The point of this article was to point out yet again, -WebGL doesn't care how you set `gl_Position` with -clip space coordinates nor does it care how you -output a color. All it cares is that you set them. -Textures are really just 2D arrays of random access -data. +不要让传统的 WebGL 使用方式限制了你的思维。 +WebGL 实际上具有极强的灵活性。 -When you have a problem you want to solve in WebGL -remember that WebGL just runs shaders and those shaders -have access to data via uniforms (global variables), -attributes (data that comes per vertex shader iteration), -and textures (random access 2D arrays). Don't let the -traditional ways of using WebGL prevent you from -seeing the real flexibility that's there. +当你想在 WebGL 中解决问题时,请记住 WebGL 只是运行着色器, +这些着色器可以通过 uniforms(全局变量)、attributes(每次顶点着色器执行时传入的数据) +以及 textures(可随机访问的二维数组)来访问数据。 +不要让传统的 WebGL 使用方式阻碍你发现它真正的灵活性。
-

Why is it called Vertex Pulling?

-

I'd actually only heard the term recently (July 2019) -even though I'd used the technique before. It comes -from OpenGL Insights "Programmable Vertex Pulling" article by Daniel Rakos. +

为什么叫做顶点拉取(Vertex Pulling)?

+

实际上我最近(2019年7月)才听到这个术语, +尽管我之前就用过这种技术。 +它来源于 + +OpenGL Insights 中 Daniel Rakos 撰写的“可编程顶点拉取”文章

-

It's called vertex *pulling* since it's the vertex shader -that decides which vertex data to read vs the traditional way where -vertex data is supplied automatically via attributes. Effectively -the vertex shader is *pulling* data out of memory.

+

之所以叫做顶点*拉取*,是因为顶点着色器决定读取哪个顶点数据, +而传统方式是通过属性自动提供顶点数据。 +实际上,顶点着色器是在*拉取*内存中的数据。

From 41d85dbcab79d4c31f13628a0dac25dc14bc271c Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 07:30:47 +0800 Subject: [PATCH 08/22] small change --- webgl/lessons/zh_cn/webgl-pulling-vertices.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/webgl/lessons/zh_cn/webgl-pulling-vertices.md b/webgl/lessons/zh_cn/webgl-pulling-vertices.md index 0dcc7cc29..a674cfd17 100644 --- a/webgl/lessons/zh_cn/webgl-pulling-vertices.md +++ b/webgl/lessons/zh_cn/webgl-pulling-vertices.md @@ -6,9 +6,7 @@ TOC: 顶点拉取 传统上,WebGL应用会将几何数据放入缓冲区中,然后通过属性(attributes)自动将这些缓冲区中的顶点数据传递给顶点着色器,由程序员编写代码将其转换为裁剪空间(clip space)坐标。 -这里的 **“传统上”** 非常重要。 -这只是一种**传统做法**,并不是必须如此。 -WebGL 并不关心我们是如何处理的,它只关心顶点着色器是否为 `gl_Position` 赋予了裁剪空间坐标。 +这里的 **“传统上”** 非常重要。这只是一种**传统做法**,并不是必须如此。WebGL 并不关心我们是如何处理的,它只关心顶点着色器是否为 `gl_Position` 赋予了裁剪空间坐标。 让我们使用类似于 [纹理](webgl-3d-textures.html) 中示例的方式,绘制一个带纹理映射的立方体。我们通常会说需要至少 24 个唯一顶点,这是因为虽然立方体只有 8 个角点位置,但每个角点会出现在立方体的 3 个不同面上,而每个面又需要不同的纹理坐标。 @@ -18,7 +16,6 @@ WebGL 并不关心我们是如何处理的,它只关心顶点着色器是否 通常,我们是通过将 8 个角点位置扩展为 24 个顶点来实现这一点的。 - ```js // front { pos: [-1, -1, 1], uv: [0, 1], }, // 0 @@ -156,15 +153,13 @@ const texcoordTexture = makeDataTexture(gl, uvs, 2); 接着,我们会创建一个顶点数组对象(vertex array)来保存我们的属性状态。 - ```js // create a vertex array object to hold attribute state const vao = gl.createVertexArray(); gl.bindVertexArray(vao); ``` - -Next we need upload the position and texcoord indices to a buffer. +接下来,我们需要将位置索引和纹理坐标索引上传到缓冲区。 ```js // Create a buffer for the position and UV indices @@ -221,7 +216,6 @@ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW) 这里我们用一个 4x4 的数据纹理,内容是棋盘格图案。 纹理格式使用 `gl.LUMINANCE`,因为这样每个像素只需要一个字节。 - ```js // Create a checker texture. const checkerTexture = gl.createTexture(); From 03728c102f5c5868db4e4a505c0e4737134e7490 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 08:19:51 +0800 Subject: [PATCH 09/22] Translate webgl-picking.md to Chinese --- webgl/lessons/zh_cn/webgl-and-alpha.md | 132 +++++++++++++ webgl/lessons/zh_cn/webgl-picking.md | 253 +++++++++---------------- 2 files changed, 220 insertions(+), 165 deletions(-) create mode 100644 webgl/lessons/zh_cn/webgl-and-alpha.md diff --git a/webgl/lessons/zh_cn/webgl-and-alpha.md b/webgl/lessons/zh_cn/webgl-and-alpha.md new file mode 100644 index 000000000..8476e06c4 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-and-alpha.md @@ -0,0 +1,132 @@ +Title: WebGL2 and Alpha +Description: How alpha in WebGL is different than alpha in OpenGL +TOC: WebGL2 and Alpha + + +I've noticed some OpenGL developers having issues with how WebGL +treats alpha in the backbuffer (ie, the canvas), so I thought it +might be good to go over some of the differences between WebGL +and OpenGL related to alpha. + +The biggest difference between OpenGL and WebGL is that OpenGL +renders to a backbuffer that is not composited with anything, +or effectively not composited with anything by the OS's window +manager, so it doesn't matter what your alpha is. + +WebGL is composited by the browser with the web page and the +default is to use pre-multiplied alpha the same as .png `` +tags with transparency and 2D canvas tags. + +WebGL has several ways to make this more like OpenGL. + +### #1) Tell WebGL you want it composited with non-premultiplied alpha + + gl = canvas.getContext("webgl2", { + premultipliedAlpha: false // Ask for non-premultiplied alpha + }); + +The default is true. + +Of course the result will still be composited over the page with whatever +background color ends up being under the canvas (the canvas's background +color, the canvas's container background color, the page's background +color, the stuff behind the canvas if the canvas has a z-index > 0, etc....) +in other words, the color CSS defines for that area of the webpage. + +A really good way to find if you have any alpha problems is to set the +canvas's background to a bright color like red. You'll immediately see +what is happening. + + + +You could also set it to black which will hide any alpha issues you have. + +### #2) Tell WebGL you don't want alpha in the backbuffer + + gl = canvas.getContext("webgl", { alpha: false }}; + +This will make it act more like OpenGL since the backbuffer will only have +RGB. This is probably the best option because a good browser could see that +you have no alpha and actually optimize the way WebGL is composited. Of course +that also means it actually won't have alpha in the backbuffer so if you are +using alpha in the backbuffer for some purpose that might not work for you. +Few apps that I know of use alpha in the backbuffer. I think arguably this +should have been the default. + +### #3) Clear alpha at the end of your rendering + + .. + renderScene(); + .. + // Set the backbuffer's alpha to 1.0 by + // Setting the clear color to 1 + gl.clearColor(1, 1, 1, 1); + + // Telling WebGL to only affect the alpha channel + gl.colorMask(false, false, false, true); + + // clear + gl.clear(gl.COLOR_BUFFER_BIT); + +Clearing is generally very fast as there is a special case for it in most +hardware. I did this in many of my first WebGL demos. If I was smart I'd switch to +method #2 above. Maybe I'll do that right after I post this. It seems like +most WebGL libraries should default to this method. Those few developers +that are actually using alpha for compositing effects can ask for it. The +rest will just get the best perf and the least surprises. + +### #4) Clear the alpha once then don't render to it anymore + + // At init time. Clear the back buffer. + gl.clearColor(1,1,1,1); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Turn off rendering to alpha + gl.colorMask(true, true, true, false); + +Of course if you are rendering to your own framebuffers you may need to turn +rendering to alpha back on and then turn it off again when you switch to +rendering to the canvas. + +### #5) Handling Images + +By default if you are loading images with alpha into WebGL, WebGL will +provide the values as they are in the file with color values not +premultiplied. This is generally what I'm used to for OpenGL programs +because it's lossless whereas pre-multiplied is lossy. + + 1, 0.5, 0.5, 0 // RGBA + +Is a possible un-premultiplied value whereas pre-multiplied it's an +impossible value because `a = 0` which means `r`, `g`, and `b` have +to be zero. + +When loading an image you can have WebGL pre-multiply the alpha if you want. +You do this by setting `UNPACK_PREMULTIPLY_ALPHA_WEBGL` to true like this + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + +The default is un-premultiplied. + +Be aware that most if not all Canvas 2D implementations work with +pre-multiplied alpha. That means when you transfer them to WebGL and +`UNPACK_PREMULTIPLY_ALPHA_WEBGL` is false WebGL will convert them +back to un-premultipiled. + +### #6) Using a blending equation that works with pre-multiplied alpha. + +Almost all OpenGL apps I've writing or worked on use + + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + +That works for non pre-multiplied alpha textures. + +If you actually want to work with pre-multiplied alpha textures then you +probably want + + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + +Those are the methods I'm aware of. If you know of more please post them below. + + + diff --git a/webgl/lessons/zh_cn/webgl-picking.md b/webgl/lessons/zh_cn/webgl-picking.md index 2cb54110e..f45909724 100644 --- a/webgl/lessons/zh_cn/webgl-picking.md +++ b/webgl/lessons/zh_cn/webgl-picking.md @@ -1,45 +1,25 @@ -Title: WebGL2 Picking -Description: How to pick things in WebGL -TOC: Picking (clicking on stuff) - -This article is about how to use WebGL to let the user pick or select -things. - -If you've read the other articles on this site you have hopefully realized -that WebGL itself is just a rasterization library. It draws triangles, -lines, and points into the canvas so it has no concept of "objects to be -selected". It just outputs pixels via shaders you supply. That means -any concept of "picking" something has to come from your code. You need -to define what these things you're letting the user select are. -That means while this article can cover general concepts you'll need to -decide for yourself how to translate what you see here into usable -concepts in your own application. - -## Clicking on an Object - -One of the easiest ways figure out which thing a user clicked on is -we come up with a numeric id for each object, we can then draw -all of the objects using their id as their color with no lighting -and no textures. This will give us an image of the silhouettes of -each object. The depth buffer will handle sorting for us. -We can then read the color of the pixel under the -mouse which will give us the id of the object that was rendered there. - -To implement this technique we'll need to combine several previous -articles. The first is the article on [drawing multiple objects](webgl-drawing-multiple-things.html) -which we'll use because given it draws multiple things we can try to -pick them. - -On top of that we generally want to render these ids off screen -by [rendering to a texture](webgl-render-to-texture.html) so we'll -add in that code as well. - -So, let's start with the last example from -[the article on drawing multiple things](webgl-drawing-multiple-things.html) -that draws 200 things. - -To it let's add a framebuffer with attached texture and depth buffer from -the last example in [the article on rendering to a texture](webgl-render-to-texture.html). +Title: WebGL2 拾取 +Description: 如何在 WebGL 中实现拾取 +TOC: 拾取(点击物体) + +本文介绍如何使用 WebGL 让用户进行拾取或选择操作。 + +如果你已经阅读了本站的其他文章,你应该已经意识到 WebGL 本质上只是一个光栅化库。它将三角形、线条和点绘制到画布上,并没有“可选择对象”的概念。它只是通过你提供的着色器输出像素。这意味着“拾取”功能必须由你的代码实现。你需要定义允许用户选择的对象是什么。因此,虽然本文可以介绍一些通用概念,但你需要自行决定如何将这些内容转化为自己应用中的可用概念。 + + +## 点击物体 + +判断用户点击了哪个物体的最简单方法之一是为每个物体分配一个数字 ID,然后使用该 ID 作为颜色绘制所有物体,不使用光照和纹理。 +这样我们就得到了一张各个物体轮廓的图像,深度缓冲区会帮我们处理遮挡排序。 +接着,我们读取鼠标所在像素的颜色值,即可获得该像素所渲染的物体 ID。 + +要实现这种技术,需要结合之前的几篇文章。第一篇是关于 [绘制多个物体](webgl-drawing-multiple-things.html) 的文章,因为它演示了如何绘制多个物体,我们可以基于此进行拾取。 + +此外,我们通常希望将这些 ID 离屏渲染,即通过 [渲染到纹理](webgl-render-to-texture.html) 来实现,因此我们也会添加相关代码。 + +那么,我们从 [绘制多个物体](webgl-drawing-multiple-things.html) 文章中的最后一个示例,绘制200个物体的开始。 + +在此基础上,加入一个帧缓冲对象,并附加纹理和深度缓冲区,参照 [渲染到纹理](webgl-render-to-texture.html) 文章中的最后一个示例。 ```js // Create a texture to render to @@ -83,13 +63,9 @@ gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTe gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); ``` -We put the code to set the sizes of the texture and -the depth renderbuffer into a function so we can -call it to resize them to match the size of the -canvas. +我们将设置纹理和深度渲染缓冲区尺寸的代码封装到一个函数中,这样可以方便地调用它来调整尺寸,使其与画布大小匹配。 -In our rendering code if the canvas changes size -we'll adjust the texture and renderbuffer to match. +在渲染代码中,如果画布尺寸发生变化,我们会相应调整纹理和渲染缓冲区的尺寸以保持一致。 ```js function drawScene(time) { @@ -104,10 +80,9 @@ function drawScene(time) { ... ``` -Next we need a second shader. The shader in the -sample renders using vertex colors but we need -one we can set to a solid color to render with ids. -So first here is our second shader +接下来我们需要第二个着色器。 +示例中的着色器是使用的顶点颜色进行渲染,但我们需要一个能够设置为纯色以渲染 ID 的着色器。 +所以,首先这是我们的第二个着色器: ```js const pickingVS = `#version 300 es @@ -134,8 +109,8 @@ const pickingFS = `#version 300 es `; ``` -And we need to compile, link and look up the locations -using our [helpers](webgl-less-code-more-fun.html). +接下来我们需要使用我们的 [辅助函数](webgl-less-code-more-fun.html) +来编译、链接着色器并查找变量位置。 ```js // setup GLSL program @@ -151,19 +126,14 @@ const programInfo = twgl.createProgramInfo(gl, [vs, fs], options); const pickingProgramInfo = twgl.createProgramInfo(gl, [pickingVS, pickingFS], options); ``` -One difference above from most samples on this site, this is one -of the few times we've needed to draw the same data with 2 different -shaders. Because of that we need the attributes locations to match -across shaders. We can do that in 2 ways. One way is to set them -manually in the GLSL +与本站大多数示例不同的是,这是少数几次需要用两个不同的着色器绘制相同数据的情况之一。因为我们需要确保两个着色器中的属性位置一致。可以通过两种方式实现:其中一种是在 GLSL 中手动设置属性位置。 ```glsl layout (location = 0) in vec4 a_position; layout (location = 1) in vec4 a_color; ``` -The other is to call `gl.bindAttribLocation` **before** linking -a shader program +另一种方式是在链接着色器程序**之前**调用 `gl.bindAttribLocation`。 ```js gl.bindAttribLocation(someProgram, 0, 'a_position'); @@ -171,21 +141,19 @@ gl.bindAttribLocation(someProgram, 1, 'a_color'); gl.linkProgram(someProgram); ``` -This latter style is uncommon but it's more -[D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). -Our helper library will call `gl.bindAttribLocation` for us -if we pass in the attribute names and the location we want -which is what is happening above. +后一种方式不常见,但它更符合 +[D.R.Y.原则](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)。 +如果我们传入属性名和想要绑定的位置, +我们的辅助库会帮我们调用 `gl.bindAttribLocation`, +这就是上面代码的做法。 + +这样我们就能保证两个程序中 `a_position` 属性都使用位置 0, +从而能用同一个顶点数组对象配合两个程序。 -This we'll mean we can guarantee the `a_position` attribute uses -location 0 in both programs so we can use the same vertex array -with both programs. +接下来,我们需要能够对所有物体渲染两次: +一次使用分配给它们的着色器,另一次使用刚写的这个着色器。 +因此,我们把当前渲染所有物体的代码提取到一个函数中。 -Next we need to be able to render all the objects -twice. Once with whatever shader we assigned to -them and again with the shader we just wrote -so let's extract the code that currently renders -all the objects into a function. ```js function drawObjects(objectsToDraw, overrideProgramInfo) { @@ -208,12 +176,9 @@ function drawObjects(objectsToDraw, overrideProgramInfo) { } ``` -`drawObjects` takes an optional `overrideProgramInfo` -we can pass in to use our picking shader instead of -the object's assigned shader. +`drawObjects` 函数接受一个可选参数 `overrideProgramInfo`,我们可以传入它来使用拾取着色器,替代物体原本分配的着色器。 -Let's call it, once to draw into the texture with -ids and again to draw the scene to the canvas. +我们调用该函数两次:一次将物体绘制到带有 ID 的纹理中,另一次将场景绘制到画布上。 ```js // Draw the scene. @@ -255,8 +220,8 @@ function drawScene(time) { } ``` -Our picking shader needs `u_id` set to an id so let's -add that to our uniform data where we setup our objects. +我们的拾取着色器需要通过 `u_id` 设置一个 ID, +因此我们在设置物体时,将该值添加到它们的 uniform 数据中。 ```js // Make infos for each object for each object. @@ -295,22 +260,19 @@ for (let ii = 0; ii < numObjects; ++ii) { } ``` -This will work because our [helper library](webgl-less-code-more-fun.html) -handles applying uniforms for us. +这是可行的,因为我们的 [辅助库](webgl-less-code-more-fun.html) 会帮我们自动设置 uniform。 -We had to split ids across R, G, B, and A. Because our -texture's format/type is `gl.RGBA`, `gl.UNSIGNED_BYTE` -we get 8 bits per channel. 8 bits only represent 256 values -but by splitting the id across 4 channels we get 32bits total -which is > 4 billion values. +我们必须将 ID 拆分到 R、G、B 和 A 四个通道中。 +由于纹理的格式/类型是 `gl.RGBA` 和 `gl.UNSIGNED_BYTE`, +每个通道只有 8 位,因此最多只能表示 256 个值。 +但通过将 ID 分别存储在 4 个通道中,我们总共可以表示 32 位, +也就是超过 40 亿个不同的值。 -We add 1 to the id because we'll use 0 for meaning -"nothing under the mouse". +我们对 ID 加 1,是为了将 0 用作“鼠标下方没有物体”的标识。 -Now let's highlight the object under the mouse. +现在,让我们来高亮鼠标下的物体。 -First we need some code to get a canvas relative -mouse position. +首先需要一些代码来获取相对于画布的鼠标位置。 ```js // mouseX and mouseY are in CSS display space relative to canvas @@ -326,22 +288,18 @@ gl.canvas.addEventListener('mousemove', (e) => { }); ``` -Note that with the code above `mouseX` and `mouseY` -are in CSS pixels in display space. That means -they are in the space the canvas is displayed, -not the space of how many pixels are in the canvas. -In other words if you had a canvas like this +注意,上面代码中的 `mouseX` 和 `mouseY` 是以 CSS 像素表示的显示空间坐标。 +也就是说,它们是在画布显示出来的区域中的坐标,而不是画布实际像素空间中的坐标。 +换句话说,如果你有一个画布像下面这样的: ```html ``` -then `mouseX` will go from 0 to 33 across the canvas and -`mouseY` will go from 0 to 44 across the canvas. See [this](webgl-resizing-the-canvas.html) -for more info. +那么在该画布上,`mouseX` 的取值范围将是 0 到 33,`mouseY` 的取值范围是 0 到 44。 +更多信息请参阅 [这篇文章](webgl-resizing-the-canvas.html)。 -Now that we have a mouse position let's add some code -look up the pixel under the mouse +现在我们已经有了鼠标位置,接下来添加一些代码来查找鼠标下方的像素。 ```js const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth; @@ -358,23 +316,13 @@ gl.readPixels( const id = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24); ``` -The code above that is computing `pixelX` and `pixelY` is converting -from `mouseX` and `mouseY` in display space to pixel in the canvas -space. In other words, given the example above where `mouseX` went from -0 to 33 and `mouseY` went from 0 to 44. `pixelX` will go from 0 to 11 -and `pixelY` will go from 0 to 22. +上面的代码中对 `pixelX` 和 `pixelY` 的计算是将显示空间中的 `mouseX` 和 `mouseY` 转换为画布像素空间中的坐标。 +换句话说,以前面的例子为例,当 `mouseX` 范围是 0 到 33,`mouseY` 范围是 0 到 44 时,`pixelX` 的范围将是 0 到 11,`pixelY` 的范围将是 0 到 22。 -In our actual code we're using our utility function `resizeCanvasToDisplaySize` -and we're making our texture the same size as the canvas so the display -size and the canvas size match but at least we're prepared for the case -where they do not match. +在实际代码中,我们使用了一个工具函数 `resizeCanvasToDisplaySize`,并将纹理设置为与画布相同的大小,所以显示尺寸和画布尺寸是一致的。不过,我们的代码已经为两者不一致的情况做了准备。 -Now that we have an id, to actually highlight the selected object -let's change the color we're using to render it to the canvas. -The shader we were using has a `u_colorMult` -uniform we can use so if an object is under the mouse we'll look it up, -save off its `u_colorMult` value, replace it with a selection color, -and restore it. +现在我们得到了一个 ID,为了高亮选中的物体,我们需要更改它在画布上渲染时所使用的颜色。 +我们使用的着色器中包含一个 `u_colorMult` 的 uniform,因此我们可以在检测到某个物体被鼠标选中时,先查找该物体,保存它原本的 `u_colorMult` 值,将其替换为选中时的颜色,然后在绘制完后再恢复。 ```js // mouseX and mouseY are in CSS display space relative to canvas @@ -428,38 +376,26 @@ function drawScene(time) { ``` -And with that we should be able to move the mouse over -the scene and the object under the mouse will flash +这样一来,当我们将鼠标移动到场景中时,鼠标下方的物体就会闪烁显示出来。 {{{example url="../webgl-picking-w-gpu.html" }}} -One optimization we can make, we're rendering -the ids to a texture that's the same size -as the canvas. This is conceptually the easiest -thing to do. +我们可以进行一个优化,目前我们将 ID 渲染到与画布相同大小的纹理中, +这在概念上是最简单的方式。 -But, we could instead just render the pixel -under the mouse. To do this we use a frustum -who's math will cover just the space for that -1 pixel. +但实际上,我们只需要渲染鼠标下的那个像素即可。要实现这一点,我们可以构造一个视锥(frustum),其数学计算范围仅覆盖那一个像素的空间。 -Until now, for 3D we've been using a function called -`perspective` that takes as input a field of view, an aspect, and a -near and far value for the z-planes and makes a -perspective projection matrix that converts from the -frustum defined by those values to clip space. +到目前为止,对于 3D 场景我们一直使用的是一个名为 `perspective` 的函数, +它接收视野角(field of view)、宽高比(aspect ratio)、以及 z 轴的近远平面, +并生成一个透视投影矩阵,将由这些值定义的视锥转换为裁剪空间(clip space)。 -Most 3D math libraries have another function called -`frustum` that takes 6 values, the left, right, top, -and bottom values for the near z-plane and then the -z-near and z-far values for the z-planes and generates -a perspective matrix defined by those values. +大多数 3D 数学库还有另一个名为 `frustum` 的函数, +它接收 6 个参数:近裁剪面上的 left、right、top、bottom 值, +以及 zNear 和 zFar 两个 z 轴平面,并据此生成一个透视投影矩阵。 -Using that we can generate a perspective matrix for -the one pixel under the mouse +利用这个函数,我们可以生成一个只覆盖鼠标下那一个像素的透视矩阵。 -First we compute the edges and size of what our near plane *would be* -if we were to use the `perspective` function +首先,如果我们使用 `perspective` 函数,计算出其近裁剪面在视图空间中的边缘位置和尺寸。 ```js // compute the rectangle the near plane of our frustum covers @@ -472,12 +408,8 @@ const width = Math.abs(right - left); const height = Math.abs(top - bottom); ``` -So `left`, `right`, `width`, and `height` are the -size and position of the near plane. Now on that -plane we can compute the size and position of the -one pixel under the mouse and pass that to the -`frustum` function to generate a projection matrix -that covers just that 1 pixel +因此,`left`、`right`、`width` 和 `height` 表示近裁剪面的尺寸和位置。 +现在在这个平面上,可以计算出鼠标下方那一个像素的尺寸和位置,然后将这些值传入 `frustum` 函数,以生成一个只覆盖该像素的投影矩阵。 ```js // compute the portion of the near plane covers the 1 pixel @@ -500,14 +432,9 @@ const projectionMatrix = m4.frustum( far); ``` -To use this we need to make some changes. As it now our shader -just takes `u_matrix` which means in order to draw with a different -projection matrix we'd need to recompute the matrices for every object -twice each frame, once with our normal projection matrix for drawing -to the canvas and again for this 1 pixel projection matrix. +要使用这种方法,我们需要做一些修改。目前我们的着色器只接受一个 `u_matrix`,这意味着如果想用不同的投影矩阵绘制,我们必须在每一帧对每个物体计算两次矩阵。一次用于正常的画布投影矩阵,另一次用于这个单像素投影矩阵。 -We can remove that responsibility from JavaScript by moving that -multiplication to the vertex shaders. +我们可以将这个责任从 JavaScript 中移除,通过把矩阵乘法移动到顶点着色器中实现。 ```html const vs = `#version 300 es @@ -548,8 +475,7 @@ const pickingVS = `#version 300 es `; ``` -Then we can make our JavaScript `viewProjectionMatrix` shared -among all the objects. +这样一来,我们就可以让 JavaScript 中的 `viewProjectionMatrix` 在所有物体间共享使用。 ```js const objectsToDraw = []; @@ -584,8 +510,7 @@ for (let ii = 0; ii < numObjects; ++ii) { }; ``` -And where we compute the matrices for each object we no longer need -to include the view projection matrix +在计算每个物体的矩阵时,我们不再需要将视图投影矩阵包含在内。 ```js -function computeMatrix(viewProjectionMatrix, translation, xRotation, yRotation) { @@ -610,7 +535,7 @@ objects.forEach(function(object) { }); ``` -We'll create just a 1x1 pixel texture and depth buffer +我们将创建一个仅有 1×1 像素的纹理和深度缓冲区。 ```js setFramebufferAttachmentSizes(1, 1); @@ -629,9 +554,8 @@ function drawScene(time) { + webglUtils.resizeCanvasToDisplaySize(gl.canvas); ``` -Then before rendering the off screen ids we'll set the view projection -using our 1 pixel projection matrix and then when drawing to the canvas -we'll use the original projection matrix +然后,在渲染离屏 ID 之前,我们将使用 1 像素的投影矩阵设置视图投影, +而在绘制到画布时,我们使用原始的投影矩阵。 ```js -// Compute the projection matrix @@ -754,8 +678,7 @@ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); drawObjects(objectsToDraw); ``` -And you can see the math works, we're only drawing a single pixel -and we're still figuring out what is under the mouse +您可以看到数学计算是有效的,我们只是绘制了一个像素,但依然能够准确地确定鼠标下方的物体。 {{{example url="../webgl-picking-w-gpu-1pixel.html"}}} From 7221013c882f9a338f422184578442b84c61b50e Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 08:45:48 +0800 Subject: [PATCH 10/22] Translate webgl-and-alpha.md to Chinese --- webgl/lessons/zh_cn/webgl-and-alpha.md | 99 +++++++++----------------- 1 file changed, 32 insertions(+), 67 deletions(-) diff --git a/webgl/lessons/zh_cn/webgl-and-alpha.md b/webgl/lessons/zh_cn/webgl-and-alpha.md index 8476e06c4..4c3653e60 100644 --- a/webgl/lessons/zh_cn/webgl-and-alpha.md +++ b/webgl/lessons/zh_cn/webgl-and-alpha.md @@ -1,59 +1,39 @@ -Title: WebGL2 and Alpha -Description: How alpha in WebGL is different than alpha in OpenGL -TOC: WebGL2 and Alpha +Title: WebGL2 和 Alpha +Description: WebGL 中的 Alpha 与 OpenGL 中的 Alpha 有何不同 +TOC: WebGL2 和 Alpha +我注意到一些 OpenGL 开发者在使用 WebGL 时遇到了关于后缓冲区(即画布)中 alpha 的问题,所以我觉得有必要讲一下 WebGL 和 OpenGL 在 alpha 处理上的一些差异。 -I've noticed some OpenGL developers having issues with how WebGL -treats alpha in the backbuffer (ie, the canvas), so I thought it -might be good to go over some of the differences between WebGL -and OpenGL related to alpha. +OpenGL 和 WebGL 最大的区别是,OpenGL 渲染到一个不会被任何东西合成的后缓冲区,或者说操作系统的窗口管理器实际上不会对它进行合成,所以无论 alpha 怎么设置都无所谓。 -The biggest difference between OpenGL and WebGL is that OpenGL -renders to a backbuffer that is not composited with anything, -or effectively not composited with anything by the OS's window -manager, so it doesn't matter what your alpha is. +而 WebGL 是由浏览器与网页内容合成的,默认使用预乘 alpha(premultiplied alpha),这和带透明通道的 PNG `` 标签以及 2D canvas 标签的行为相同。 -WebGL is composited by the browser with the web page and the -default is to use pre-multiplied alpha the same as .png `` -tags with transparency and 2D canvas tags. +WebGL 有几种方式可以使其行为更像 OpenGL。 -WebGL has several ways to make this more like OpenGL. - -### #1) Tell WebGL you want it composited with non-premultiplied alpha +### #1) 告诉 WebGL 你希望使用非预乘 alpha 合成 gl = canvas.getContext("webgl2", { premultipliedAlpha: false // Ask for non-premultiplied alpha }); -The default is true. +默认值是 true。 -Of course the result will still be composited over the page with whatever -background color ends up being under the canvas (the canvas's background -color, the canvas's container background color, the page's background -color, the stuff behind the canvas if the canvas has a z-index > 0, etc....) -in other words, the color CSS defines for that area of the webpage. +当然,结果仍会与画布下方的背景颜色合成(画布背景色、画布容器背景色、页面背景色,或者当画布 z-index 大于 0 时背后的内容), +换句话说,是网页该区域 CSS 定义的颜色。 -A really good way to find if you have any alpha problems is to set the -canvas's background to a bright color like red. You'll immediately see -what is happening. +判断是否存在 alpha 问题的一个好方法是将画布背景设置为鲜艳颜色,例如红色。你能立刻看到效果: -You could also set it to black which will hide any alpha issues you have. +你也可以设置成黑色,黑色会掩盖任何 alpha 问题。 -### #2) Tell WebGL you don't want alpha in the backbuffer +### #2) 告诉 WebGL 你不需要后缓冲区的 alpha gl = canvas.getContext("webgl", { alpha: false }}; -This will make it act more like OpenGL since the backbuffer will only have -RGB. This is probably the best option because a good browser could see that -you have no alpha and actually optimize the way WebGL is composited. Of course -that also means it actually won't have alpha in the backbuffer so if you are -using alpha in the backbuffer for some purpose that might not work for you. -Few apps that I know of use alpha in the backbuffer. I think arguably this -should have been the default. +这样它的行为更像 OpenGL,因为后缓冲区只会有 RGB。 这可能是最好的选择,因为优秀的浏览器能检测到你不需要 alpha, 从而优化 WebGL 的合成方式。 但这也意味着后缓冲区实际上没有 alpha,如果你确实依赖它可能会不适用。 我所知道的应用中很少使用后缓冲区 alpha。 从某种角度看,我认为这应该是默认行为。 -### #3) Clear alpha at the end of your rendering +### #3) 在渲染结束时清除 alpha 通道 .. renderScene(); @@ -68,14 +48,10 @@ should have been the default. // clear gl.clear(gl.COLOR_BUFFER_BIT); -Clearing is generally very fast as there is a special case for it in most -hardware. I did this in many of my first WebGL demos. If I was smart I'd switch to -method #2 above. Maybe I'll do that right after I post this. It seems like -most WebGL libraries should default to this method. Those few developers -that are actually using alpha for compositing effects can ask for it. The -rest will just get the best perf and the least surprises. +清除操作通常非常快,因为大多数硬件对此有特殊优化。 我在许多早期 WebGL 示例中都这么做过。 如果聪明的话,应该用上面方法 #2。 也许我发完这条就去改。 大多数 WebGL 库也应该默认采用这种方法。 真正使用 alpha 进行合成效果的开发者可以主动开启。 其余人则能获得最佳性能和最少意外。 + -### #4) Clear the alpha once then don't render to it anymore +### #4) 清除 Alpha 通道一次,之后不再渲染到该通道 // At init time. Clear the back buffer. gl.clearColor(1,1,1,1); @@ -84,49 +60,38 @@ rest will just get the best perf and the least surprises. // Turn off rendering to alpha gl.colorMask(true, true, true, false); -Of course if you are rendering to your own framebuffers you may need to turn -rendering to alpha back on and then turn it off again when you switch to -rendering to the canvas. +当然如果你在渲染自定义的帧缓冲区时, 可能需要重新开启 alpha 渲染, 然后切回渲染画布时再关闭。 -### #5) Handling Images +### #5) 处理带 alpha 的图片 -By default if you are loading images with alpha into WebGL, WebGL will -provide the values as they are in the file with color values not -premultiplied. This is generally what I'm used to for OpenGL programs -because it's lossless whereas pre-multiplied is lossy. +默认情况下,加载带 alpha 的图片到 WebGL,WebGL 会提供文件中的原始颜色值, 且颜色值未做预乘。 这一般符合我对 OpenGL 程序的使用习惯, 因为未预乘是无损的,而预乘会有损失。 1, 0.5, 0.5, 0 // RGBA -Is a possible un-premultiplied value whereas pre-multiplied it's an -impossible value because `a = 0` which means `r`, `g`, and `b` have -to be zero. +这是一个可能的未预乘值, 但预乘情况下不可能出现这种值, 因为 alpha = 0,r、g、b 必须都是 0。 -When loading an image you can have WebGL pre-multiply the alpha if you want. -You do this by setting `UNPACK_PREMULTIPLY_ALPHA_WEBGL` to true like this +加载图像时,如果需要,可以让 WebGL 对 Alpha 进行预乘。 +你可以通过如下方式将 UNPACK_PREMULTIPLY_ALPHA_WEBGL 设置为 true 来实现。 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); -The default is un-premultiplied. +默认情况下是不进行预乘的。 -Be aware that most if not all Canvas 2D implementations work with -pre-multiplied alpha. That means when you transfer them to WebGL and -`UNPACK_PREMULTIPLY_ALPHA_WEBGL` is false WebGL will convert them -back to un-premultipiled. +请注意,大多数(如果不是全部的话)Canvas 2D 实现都使用预乘 alpha。 这意味着当你将它们传输到 WebGL 并且 UNPACK_PREMULTIPLY_ALPHA_WEBGL 设置为 false 时,WebGL 会将它们转换回非预乘状态。 -### #6) Using a blending equation that works with pre-multiplied alpha. +### #6) 使用与预乘 alpha 兼容的混合方程 -Almost all OpenGL apps I've writing or worked on use +我写过或参与过的几乎所有 OpenGL 应用, 默认都是这样设置混合函数。 gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); -That works for non pre-multiplied alpha textures. +这适用于非预乘 alpha 的纹理。 -If you actually want to work with pre-multiplied alpha textures then you -probably want +如果你确实想使用预乘 alpha 纹理,那么你可能需要使用以下设置 gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); -Those are the methods I'm aware of. If you know of more please post them below. +这些是我所知道的方法。如果你了解更多,请在下方分享。 From fbf3d4a0feb565e77e52bff4c002b24e1fc46427 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 11:32:40 +0800 Subject: [PATCH 11/22] Translate webgl-2d-vs-3d-library.md to Chinese --- webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md diff --git a/webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md b/webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md new file mode 100644 index 000000000..618f67b57 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md @@ -0,0 +1,177 @@ +Title: WebGL2 - 光栅化 vs 3D 库 +Description: 为什么 WebGL 不是 3D 库以及这点为什么重要。 +TOC: 2D vs 3D 库 + +这篇文章是关于 WebGL 系列文章的一个旁支话题。 +第一篇是 [基础知识介绍](webgl-fundamentals.html) + +我写这篇文章是因为我说 WebGL 是一个光栅化 API,而不是一个 3D API,这句话触到了某些人的神经。 +我不太清楚为什么他们会觉得被威胁,或者是什么让他们对我称 WebGL 为光栅化 API 这件事如此反感。 + +可以说,一切都是视角问题。我可能会说刀是一种餐具,别人可能会说刀是工具,还有人可能会说刀是武器。 + +但在 WebGL 的情况下,我认为将 WebGL 称为光栅化 API 是重要的,这是有原因的——那就是你需要掌握大量的 3D 数学知识,才能用 WebGL 绘制出任何 3D 内容。 + +我认为,任何自称为 3D 库的东西,都应该替你处理好 3D 的部分。你只需提供一些 3D 数据、材质参数、灯光信息,它就应该能够帮你完成 3D 渲染。 +WebGL(以及 OpenGL ES 2.0+)虽然都可以用来绘制 3D 图形,但它们都不符合这个定义。 + +个比方,C++ 并不能“原生处理文字”。尽管可以用 C++ 编写文字处理器,但我们不会把 C++ 称作“文字处理器”。 +同样,WebGL 并不能直接绘制 3D 图形。你可以基于 WebGL 编写一个绘制 3D 图形的库,但 WebGL 本身并不具备 3D 绘图功能。 + +进一步举个例子,假设我们想要绘制一个带有灯光效果的 3D 立方体。 + +以下是使用 three.js 来显示这个代码。 + +
{{#escapehtml}}
+  // Setup.
+  renderer = new THREE.WebGLRenderer({canvas: document.querySelector("#canvas")});
+  c.appendChild(renderer.domElement);
+
+  // Make and setup a camera.
+  camera = new THREE.PerspectiveCamera(70, 1, 1, 1000);
+  camera.position.z = 400;
+
+  // Make a scene
+  scene = new THREE.Scene();
+
+  // Make a cube.
+  var geometry = new THREE.BoxGeometry(200, 200, 200);
+
+  // Make a material
+  var material = new THREE.MeshPhongMaterial({
+    ambient: 0x555555,
+    color: 0x555555,
+    specular: 0xffffff,
+    shininess: 50,
+    shading: THREE.SmoothShading
+  });
+
+  // Create a mesh based on the geometry and material
+  mesh = new THREE.Mesh(geometry, material);
+  scene.add(mesh);
+
+  // Add 2 lights.
+  light1 = new THREE.PointLight(0xff0040, 2, 0);
+  light1.position.set(200, 100, 300);
+  scene.add(light1);
+
+  light2 = new THREE.PointLight(0x0040ff, 2, 0);
+  light2.position.set(-200, 100, 300);
+  scene.add(light2);
+{{/escapehtml}}
+ +它显示如下。 + +{{{example url="resources/three-js-cube-with-lights.html" }}} + +以下是在 OpenGL(非 ES 版本)中显示一个带有两个光源的立方体的类似代码。 + +
{{#escapehtml}}
+  // Setup
+  glViewport(0, 0, width, height);
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  gluPerspective(70.0, width / height, 1, 1000);
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+
+  glClearColor(0.0, 0.0, 0.0, 0.0);
+  glEnable(GL_DEPTH_TEST);
+  glShadeModel(GL_SMOOTH);
+  glEnable(GL_LIGHTING);
+
+  // Setup 2 lights
+  glEnable(GL_LIGHT0);
+  glEnable(GL_LIGHT1);
+  float light0_position[] = {  200, 100, 300, };
+  float light1_position[] = { -200, 100, 300, };
+  float light0_color[] = { 1, 0, 0.25, 1, };
+  float light1_color[] = { 0, 0.25, 1, 1, };
+  glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_color);
+  glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_color);
+  glLightfv(GL_LIGHT0, GL_POSITION, light0_position);
+  glLightfv(GL_LIGHT1, GL_POSITION, light1_position);
+...
+
+  // Draw a cube.
+  static int count = 0;
+  ++count;
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+  glLoadIdentity();
+  double angle = count * 0.1;
+  glTranslatef(0, 0, -400);
+  glRotatef(angle, 0, 1, 0);
+
+  glBegin(GL_TRIANGLES);
+  glNormal3f(0, 0, 1);
+  glVertex3f(-100, -100, 100);
+  glVertex3f( 100, -100, 100);
+  glVertex3f(-100,  100, 100);
+  glVertex3f(-100,  100, 100);
+  glVertex3f( 100, -100, 100);
+  glVertex3f( 100,  100, 100);
+
+  /*
+  ...
+  ... repeat for 5 more faces of cube
+  ...
+  */
+
+  glEnd();
+{{/escapehtml}}
+ +请注意,在这两个示例中我们几乎不需要任何 3D 数学知识。对比来看,WebGL 就不是这样。我不会去写 WebGL 所需的完整代码——代码本身其实不会多很多。关键不在于代码行数的多少,而在于所需的知识量。 + +在这两个 3D 库中,它们帮你处理了所有 3D 相关的事情。你只需要提供一个摄像机的位置和视野、几个光源以及一个立方体,其余的它们都会帮你搞定。换句话说:它们是真正的 3D 库。 + +请注意,在这两个示例中我们几乎不需要任何 3D 数学知识。对比来看,WebGL 就不是这样。我不会去写 WebGL 所需的完整代码——代码本身其实不会多很多。关键不在于代码行数的多少,而在于所需的知识量。 + +在这两个 3D 库中,它们帮你处理了所有 3D 相关的事情。你只需要提供一个摄像机的位置和视野、几个光源以及一个立方体,其余的它们都会帮你搞定。换句话说:它们是真正的 3D 库。 + +请注意,在这两个示例中我们几乎不需要任何 3D 数学知识。对比来看,WebGL 就不是这样。我不会去写 WebGL 所需的完整代码——代码本身其实不会多很多。关键不在于代码行数的多少,而在于所需的知识量。 + +在这两个 3D 库中,它们帮你处理了所有 3D 相关的事情。 你只需要提供一个摄像机的位置和视野、几个光源以及一个立方体,其余的它们都会帮你搞定。 +换句话说:它们是真正的 3D 库。 + +而在 WebGL 中,你则需要掌握矩阵运算、归一化坐标、视锥体、叉积、点积、varying 插值、光照、高光计算等等一系列内容,而这些通常需要几个月甚至几年的时间才能真正理解和掌握。 + +一个 3D 库的核心意义就在于它内部已经封装好了这些知识,因此你不需要自己去掌握它们,你只需要依赖这个库来帮你完成处理。正如上文所示,这一点在最初的 OpenGL 中就成立,对像 three.js 这样的其他 3D 库同样适用。但对于 OpenGL ES 2.0+ 或 WebGL 来说,这种封装是不存在的。 + +称 WebGL 为一个 3D 库似乎是具有误导性的。一个初学者接触 WebGL 时可能会想:“哦,这是个 3D 库,太棒了,它会帮我处理 3D。”然而他们最终会痛苦地发现,事实根本不是这样。 + +我们甚至可以更进一步。下面是使用 Canvas 绘制 3D 线框立方体的示例。 + +{{{example url="resources/3d-in-canvas.html" }}} + +下面是使用 WebGL 绘制线框立方体的示例。 + +{{{example url="resources/3d-in-webgl.html" }}} + +如果你检查这两段代码,就会发现它们在所需知识量或代码量方面并没有太大差异。归根结底,Canvas 版本是遍历顶点,使用我们提供的数学计算,然后在 2D 中绘制一些线条;而 WebGL 版本做的也是同样的事情,只不过这些数学计算是我们写在 GLSL 中,由 GPU 执行的。 + +这最后一个演示的重点是说明 WebGL 本质上只是一个光栅化引擎,就像 Canvas 2D 一样。确实,WebGL 提供了一些有助于实现 3D 的功能,比如深度缓冲区,它让深度排序变得比没有深度的系统简单得多。 +WebGL 还内置了各种数学函数,非常适合用于 3D 数学计算,尽管严格来说,这些函数本身并不属于“3D”的范畴——它们只是数学库,无论你是用于一维、二维还是三维计算都可以使用。 +但归根结底,WebGL 只负责光栅化。你必须自己提供裁剪空间(clip space)坐标来表示你想绘制的内容。确实,你可以提供 x, y, z, w,WebGL 会在渲染前将其除以 w,但这远远不足以让 WebGL 被称为一个“3D 库”。 +在一个真正的 3D 库中,你只需提供 3D 数据,库会帮你完成从 3D 到裁剪空间坐标的全部计算。 + +为了提供更多参考信息, [emscripten](https://emscripten.org/) 在 WebGL 之上实现了旧版 OpenGL 的仿真。相关代码在 +[这里](https://github.com/emscripten-core/emscripten/blob/main/src/lib/libglemu.js)。 + +如果你查看这段代码,你会发现其中很大一部分是在生成着色器,用来模拟 OpenGL ES 2.0 中被移除的旧版 OpenGL 的 3D 部分。 + +你也可以在 [Regal](https://chromium.googlesource.com/external/p3/regal/+/refs/heads/master/src/regal/RegalIff.cpp) 中看到类似的做法。 +Regal 是 NVIDIA 发起的一个项目,旨在在现代 OpenGL 中仿真包含 3D 功能的旧版 OpenGL,而现代 OpenGL 已经不再内建这些 3D 功能。 + +再举一个例子,[three.js 所使用的着色器](https://gist.github.com/greggman/41d93c00649cba78abdbfc1231c9158c) +就展示了如何在库内部提供 3D 功能。你可以看到这些例子中都做了大量工作。 + +所有这些 3D 功能以及背后的支持代码,都是由这些库提供的,而不是由 WebGL 自身提供的。 + +我希望你至少能理解我所说的“WebGL 不是一个 3D 库”是什么意思。我也希望你能意识到,一个真正的 3D 库应该为你处理好所有 3D 的相关部分。 +OpenGL 做到了这一点。Three.js 也做到了。而 OpenGL ES 2.0 和 WebGL 则没有。 +因此,可以说它们并不属于“3D 库”这个广义分类下。 + +这一切的重点,是为了让刚接触 WebGL 的开发者理解 WebGL 的本质。 +了解 WebGL 并不是一个 3D 库,而是一个栅格化 API,意味着你需要自己掌握所有与 3D 相关的知识。这能帮助你明确接下来的学习方向——是深入学习 3D 数学知识,还是选择一个能为你处理好这些细节的 3D 库来简化开发。 +同时,这也能帮助你揭开 WebGL 工作原理背后的许多神秘面纱。 From 1c4069687a8f2d5979afd9def6a8258184003028 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 15:21:16 +0800 Subject: [PATCH 12/22] Translate webgl-anti-patterns.md to Chinese --- webgl/lessons/zh_cn/webgl-anti-patterns.md | 255 +++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-anti-patterns.md diff --git a/webgl/lessons/zh_cn/webgl-anti-patterns.md b/webgl/lessons/zh_cn/webgl-anti-patterns.md new file mode 100644 index 000000000..90f1bf3b1 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-anti-patterns.md @@ -0,0 +1,255 @@ +Title: WebGL2 反模式 +Description: WebGL 编程禁忌:问题所在与正确做法 +TOC: 反模式 + + +这是一些 WebGL 中的**反模式**列表。反模式指的是你在编写 WebGL 程序时应当**避免采用的做法**。 + +1. 在 `WebGLRenderingContext` 上添加 `viewportWidth` 和 `viewportHeight` 属性 + + 有些代码会为视口的宽度和高度添加属性,并将它们直接附加到 `WebGLRenderingContext` 对象上,类似这样: + + gl = canvas.getContext("webgl2"); + gl.viewportWidth = canvas.width; // ❌ 错误做法! + gl.viewportHeight = canvas.height; // ❌ 错误做法! + + 之后可能会这样使用这些属性: + + gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight); + + **为什么这样做不好:** + + 从客观角度来看,这样做不好是因为你引入了两个属性,在每次更改 canvas 大小时都需要手动更新它们。 + 例如:当用户调整窗口大小时,如果你没有重新设置 `gl.viewportWidth` 和 `gl.viewportHeight`,它们的值就会出错。 + + 从主观角度来看,这样做也不好,因为任何一个刚接触 WebGL 的程序员在看到你的代码时, + 很可能会以为 `gl.viewportWidth` 和 `gl.viewportHeight` 是 WebGL 规范的一部分, + 从而产生误解,甚至困扰数月。 + + **正确的做法:** + + 为什么要给自己增加额外的工作量?WebGL 上下文对象中已经包含了其对应的 canvas,而且 canvas 本身就有宽高属性可用。 + +
+    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
+    
+ + 上下文对象本身也直接提供了其绘图缓冲区的宽度和高度。 + + // 当你需要将视口设置为与 canvas 的 drawingBuffer 大小时,这种方式总是正确的 + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + + 甚至更好的是,使用`gl.drawingBufferWidth` 和 `gl.drawingBufferHeight` 能处理极端情况,而使用 `gl.canvas.width` 和 `gl.canvas.height` 则无法做到。为什么会这样[请见此处](#drawingbuffer)。 + +2. 使用 canvas.width 和 canvas.height 来计算宽高比(aspect ratio) + + 很多代码会像下面这样,使用 `canvas.width` 和 `canvas.height` 来计算宽高比: + + var aspect = canvas.width / canvas.height; + perspective(fieldOfView, aspect, zNear, zFar); + + **为什么这样做不好:** + + 画布的 width 和 height 属性与画布在页面上的实际显示尺寸没有关系。 真正控制画布显示大小的是 CSS。 + + **正确的做法:** + + 使用 `canvas.clientWidth` 和 `canvas.clientHeight`。这些值表示画布在屏幕上实际的显示尺寸。使用它们可以确保你始终获得正确的纵横比,而不受 CSS 设置的影响。 + + var aspect = canvas.clientWidth / canvas.clientHeight; + perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); + + 以下是一些示例:画布的绘图缓冲区尺寸相同(width="400" height="300"),但我们通过 CSS 指定浏览器以不同的尺寸显示该画布。 请注意,这些示例中的 “F” 字母都显示在正确的宽高比下。 + + {{{diagram url="../webgl-canvas-clientwidth-clientheight.html" width="150" height="200" }}} +

+ {{{diagram url="../webgl-canvas-clientwidth-clientheight.html" width="400" height="150" }}} + + 如果我们使用的是 `canvas.width` 和 `canvas.height`,那么就不会是这种正确的显示效果了。 + + {{{diagram url="../webgl-canvas-width-height.html" width="150" height="200" }}} +

+ {{{diagram url="../webgl-canvas-width-height.html" width="400" height="150" }}} + +3. 使用 `window.innerWidth` 和 `window.innerHeight` 来进行计算 + + 许多 WebGL 程序在许多地方使用 `window.innerWidth` 和 `window.innerHeight`,例如: + + canvas.width = window.innerWidth; // ❌ 错误做法!! + canvas.height = window.innerHeight; // ❌ 错误做法!! + + **为什么这很糟糕:** + + 这不具备通用性。是的,对于那些你希望 canvas 填满整个屏幕的 WebGL 页面来说,它是可行的。但问题是,当你不这么做时,它就不合适了。也许你正在写一篇教程文章,canvas 只是页面中一个小图示;或者你需要一个侧边的属性编辑器,或者是一个游戏的计分面板。 + 当然你可以通过修改代码来应对这些情况,但何不一开始就写出可以适用于这些场景的代码?这样你在将这段代码拷贝到一个新项目或在旧项目中以新方式使用时,就不需要再进行调整。 + + **好的做法:** + + 与其对抗 Web 平台,不如按它的设计方式来使用它。使用 CSS 和 `clientWidth`、`clientHeight`。 + + var width = gl.canvas.clientWidth; + var height = gl.canvas.clientHeight; + + gl.canvas.width = width; + gl.canvas.height = height; + + 下面是 9 个不同场景的案例,它们都使用完全相同的代码。请注意,这些代码中 都没有引用 `window.innerWidth` 或 `window.innerHeight`。 + + 一个只包含 canvas 的页面,使用 CSS 让其全屏显示 + + 一个页面中的 canvas 设置为 70% 宽度,为编辑器控件留出空间 + + 一个将 canvas 嵌入段落中的页面 + + 一个将 canvas 嵌入段落并使用 box-sizing: border-box; 的页面 + + box-sizing: border-box; 会让边框和内边距从元素本身的尺寸中占用空间,而不是额外扩展到元素之外。换句话说,在默认的 box-sizing 模式下,一个 400x300 像素的元素加上 15 像素的边框,会得到一个内容区域为 400x300 像素、总尺寸为 430x330 像素的元素。而在 box-sizing: border-box; 模式中,边框会向内缩进,因此该元素保持 400x300 像素大小,但内容区域将缩小为 370x270 像素。 + + 这也是为什么使用 `clientWidth` 和 `clientHeight` 如此重要的又一原因。如果你设置了例如 `1em` 的边框,就无法预知 canvas 的实际渲染尺寸——不同的字体、不同的设备或浏览器都会导致 canvas 显示大小不同。 + + 一个只有容器的页面,使用 CSS 使其全屏显示,代码会在其中插入一个 canvas + + 一个容器占据页面 70% 宽度的页面,为编辑控件预留空间,代码会在其中插入一个 canvas + + 一个将容器嵌入段落中的页面,代码会在其中插入一个 canvas + + 一个使用 box-sizing: border-box; 将容器嵌入段落中的页面,代码会在其中插入一个 canvas + + 一个没有任何元素,仅通过 CSS 设置为全屏的页面,代码会在其中插入一个 canvas + + 再次强调,如果你遵循上述技术并拥抱 Web 平台的设计思路,无论遇到哪种使用场景,你都无需修改任何代码。 + +4. 使用 `'resize'` 事件来改变 canvas 的尺寸 + + 有些应用会监听窗口的 `'resize'` 事件来调整 canvas 的尺寸,比如这样: + + window.addEventListener('resize', resizeTheCanvas); + + 或者 + + window.onresize = resizeTheCanvas; + + **为什么这样不好:** + + 这并不绝对错误,但对于*大多数*WebGL 程序来说,它的适用范围较小。 + 具体来说,'resize' 事件只在窗口尺寸变化时触发。但当 canvas 因其他原因被调整大小时,它不会触发。 + 举个例子:假设你正在制作一个 3D 编辑器。左边是 canvas,右边是设置面板,你可以拖动中间的分隔条来调整设置区域的宽度。在这种情况下,canvas 的尺寸会改变,但你不会收到任何 'resize' 事件。 + 类似地,如果你的页面有其他内容被添加或移除,浏览器重新布局导致 canvas 尺寸变化,也不会触发 'resize' 事件。 + + **正确做法:** + + 就像前面提到的很多反模式一样,有一种更通用的写法可以让你的代码在大多数情况下都正常工作。 + 对于那些每一帧都在渲染的 WebGL 应用,可以在每次绘制时检查 canvas 是否需要调整大小,方法如下: + + function resizeCanvasToDisplaySize() { + var width = gl.canvas.clientWidth; + var height = gl.canvas.clientHeight; + if (gl.canvas.width != width || + gl.canvas.height != height) { + gl.canvas.width = width; + gl.canvas.height = height; + } + } + + function render() { + resizeCanvasToDisplaySize(); + drawStuff(); + requestAnimationFrame(render); + } + render(); + + 现在无论哪种情况,canvas 都会自动缩放到正确的尺寸。你无需针对不同的使用场景修改代码。 + 例如,使用上面第 3 点中相同的代码,这里是一个具有可调整大小编辑区域的编辑器示例。 + + {{{example url="../webgl-same-code-resize.html" }}} + + 这种情况下,以及所有由于页面中其他动态元素尺寸变化而导致 canvas 大小变化的场景中,都不会触发 `resize` 事件。 + + 对于不是每一帧都重绘的 WebGL 应用,以上代码依然适用,你只需要在 canvas 有可能被调整大小的场景中触发重绘即可。 + 一个简单的做法是使用 `ResizeObserver`。 + +
+    const resizeObserver = new ResizeObserver(render);
+    resizeObserver.observe(gl.canvas, {box: 'content-box'});
+    
+ +5. 向 `WebGLObject` 添加属性 + + `WebGLObject` 是指 WebGL 中的各种资源类型,比如 `WebGLBuffer` 或 `WebGLTexture` 等。 + 有些应用会给这些对象添加额外的属性。例如: + + var buffer = gl.createBuffer(); + buffer.itemSize = 3; // ❌ 不推荐的做法!! + buffer.numComponents = 75; // ❌ 不推荐的做法!! + + var program = gl.createProgram(); + ... + program.u_matrixLoc = gl.getUniformLocation(program, "u_matrix"); // ❌ 不推荐的做法!! + + **为什么这样不好:** + + 这是一个不推荐的做法,是因为 WebGL 有可能会“丢失上下文”(context lost)。 + 这种情况可能由于多种原因发生,其中最常见的原因是:如果浏览器发现 GPU 资源占用过高, + 它可能会故意让某些 `WebGLRenderingContext` 上下文失效,以释放资源。 + + 如果你希望 WebGL 程序能够稳定运行,就必须处理上下文丢失的问题。比如 Google Maps 就处理了这种情况。 + + 而上述代码的问题在于,一旦上下文丢失,像 `gl.createBuffer()` 这样的 WebGL 创建函数将返回 `null`, + 这实际上等价于以下代码: + + var buffer = null; + buffer.itemSize = 3; // ERROR! + buffer.numComponents = 75; // ERROR! + + 这很可能会让你的应用崩溃,并抛出如下错误: + + TypeError: Cannot set property 'itemSize' of null + + 虽然很多应用在上下文丢失时崩溃也无所谓,但如果以后开发者决定要支持上下文丢失的处理,那写出这种代码显然不是个好主意,因为它们迟早都得被修复。 + + **正确做法:** + + 如果你想把 `WebGLObject` 和它的相关信息绑定在一起,一个可行的方法是使用 JavaScript 对象。例如: + + var bufferInfo = { + id: gl.createBuffer(), + itemSize: 3, + numComponents: 75, + }; + + var programInfo = { + id: program, + u_matrixLoc: gl.getUniformLocation(program, "u_matrix"), + }; + + 我个人建议[使用一些简单的辅助工具,这会让编写 WebGL 代码变得更加轻松](webgl-less-code-more-fun.html)。 + +以上是我在网络上看到的一些 WebGL 反模式(Anti-Patterns)。 +希望我已经说明了为什么应当避免这些做法,并提供了简单实用的替代方案。 + +

什么是 drawingBufferWidth 和 drawingBufferHeight?

+

+GPU 对它们支持的像素矩形(纹理、渲染缓冲)的大小是有限制的。这个限制通常是大于当时常见显示器分辨率的 2 的幂。例如,如果某个 GPU 是为支持 1280x1024 的屏幕设计的,它的限制可能是 2048;如果是为 2560x1600 的屏幕设计的,则可能是 4096。 +

+这听起来很合理,但如果你有多个显示器会发生什么?假设我的 GPU 限制为 2048,但我有两个 1920x1080 的显示器。用户打开了一个 WebGL 页面,然后将窗口拉伸到两个显示器上。这时你的代码尝试将 canvas.width 设置为 canvas.clientWidth,也就是 3840。这种情况下该怎么办? +

+

我能想到只有 3 种选择:

+
    +
  1. +

    抛出异常。

    +

    这听起来很糟糕。大多数 Web 应用不会处理这个异常,结果就是程序崩溃。如果用户的数据没有保存,那就直接丢失了。

    +
  2. +
  3. +

    将 canvas 大小限制在 GPU 支持的最大值。

    +

    问题是这样也可能导致崩溃,或者页面显示错乱,因为代码以为 canvas 是它请求的大小,页面中的其他 UI 元素和布局也会依赖这个尺寸。

    +
  4. +
  5. +

    让 canvas 显示为用户请求的尺寸,但将其绘图缓冲区限制为 GPU 的最大限制。

    +

    这是 WebGL 实际采用的方案。如果代码写得正确,用户唯一会注意到的可能只是画面略微被缩放了。但总体来说一切工作正常。最坏情况下,如果 WebGL 程序没处理好,只是画面显示略微错位,等用户缩小窗口后就恢复正常了。

    +
  6. +
+

大多数人并没有多个显示器,所以这个问题很少遇到。或者说至少以前是这样。Chrome 和 Safari(至少在 2015 年 1 月)对 canvas 尺寸有硬编码的最大限制为 4096。而苹果的 5K iMac 分辨率就超过了这个限制,因此许多 WebGL 应用出现了奇怪的显示问题。同样地,越来越多人开始在多屏环境中使用 WebGL 做展示类工作,也在碰到这个限制。

+

+所以,如果你想处理这些情况,请像上面第 #1 条建议中那样使用 gl.drawingBufferWidthgl.drawingBufferHeight。对于大多数应用,只要你按照这些最佳实践来做,就能确保正常运行。但如果你的程序中需要知道绘图缓冲区的实际尺寸(比如 [拾取](webgl-picking.html),也就是将鼠标坐标转换为 canvas 像素坐标),你就需要特别注意这点。另一个例子是任何类型的后处理效果,它们也需要知道实际的绘图缓冲区大小。 +

+
From 8416ebfa494d89ed6ed56e708f98a1108aa41541 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 16:30:52 +0800 Subject: [PATCH 13/22] Translate webgl-matrix-vs-math.md to Chinese --- webgl/lessons/zh_cn/webgl-matrix-vs-math.md | 231 ++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-matrix-vs-math.md diff --git a/webgl/lessons/zh_cn/webgl-matrix-vs-math.md b/webgl/lessons/zh_cn/webgl-matrix-vs-math.md new file mode 100644 index 000000000..2c595782d --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-matrix-vs-math.md @@ -0,0 +1,231 @@ +Title: WebGL2中的矩阵vs数学中的矩阵 +Description: WebGL约定与数学约定之间的差异。 +TOC: WebGL2中的矩阵vs数学中的矩阵 + + +这篇文章是对若干讨论矩阵的文章的补充说明,尤其包括:[介绍矩阵的文章](webgl-2d-matrices.html), +以及 [介绍3D的文章](webgl-3d-orthographic.html)、[透视投影的文章](webgl-3d-perspective.html) 和 [摄像机相关的文章](webgl-3d-camera.html)。 + +在编程中,通常“行”是从左到右的,“列”是从上到下的。 + +> ## col·umn +> /ˈkäləm/ +> +> *名词* +> 1. 一个直立的柱子,通常是圆柱形,由石头或混凝土制成,用于支撑檐部、拱门或其他结构,或作为独立的纪念碑而存在。 +> +> *同义词*: 柱子、柱杆、杆、立柱、垂直物体、... +> +> 2. 一页或一段文字的垂直分栏。 + +> ## row +> /rō/ +> +> *名词* +> * 表格中一行水平排列的条目。 + +我们可以在各种软件中看到这些术语的使用。例如,我使用的文本编辑器中显示了“行(Lines)”和“列(columns)”, +在这种情况下,“行”就是“row”的另一种说法,因为“column”这个词已经被用于表示垂直方向了。 + +
+ +请注意左下角区域,状态栏中显示了当前的行(line)和列(column)。 + +在电子表格软件中,我们可以看到行是横向排列的。 + +
+ +而列是纵向排列的。 + +
+ +因此,当我们在 JavaScript 中为 WebGL 创建一个 3x3 或 4x4 的矩阵时,我们这样写: + +```js +const m3x3 = [ + 0, 1, 2, // row 0 + 3, 4, 5, // row 1 + 6, 7, 8, // row 2 +]; + +const m4x4 = [ + 0, 1, 2, 3, // row 0 + 4, 5, 6, 7, // row 1 + 8, 9, 10, 11, // row 2 + 12, 13, 14, 15, // row 3 +]; +``` + +根据上述惯例,`3x3`矩阵的第一行是 `0, 1, 2`,而 `4x4` 矩阵的最后一行是 `12, 13, 14, 15`。 + +正如我们在[矩阵](webgl-2d-matrices.html)中看到的,要制作一个相当标准的 WebGL 3x3 二维平移矩阵,平移值 `tx` 和 `ty` 位于位置 6 和 7。 + + +```js +const some3x3TranslationMatrix = [ + 1, 0, 0, + 0, 1, 0, + tx, ty, 1, +]; +``` + +或者在[3D基础](webgl-3d-orthographic.html)中介绍的 4x4 矩阵中,平移值(tx, ty, tz)位于第 12、13、14 的位置。 + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1, +]; +``` + +但这里有一个问题:数学中的矩阵运算通常按列优先(column-major)的惯例来书写。数学家会这样表示一个 3x3 的平移矩阵: + +
+ +以及一个 4x4 的平移矩阵如下: + +
+ +这样就留下了一个问题。如果我们想让矩阵看起来像数学中的矩阵, +我们可能尝试这样写 4x4 的矩阵: + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, tx, + 0, 1, 0, ty, + 0, 0, 1, tx, + 0, 0, 0, 1, +]; +``` + +不幸的是,这样做会有问题。正如[摄像机一文](webgl-3d-camera.html)中提到的, +4x4 矩阵的每一列通常都有特定含义。 + +第一、第二和第三列通常分别表示 x、y 和 z 轴,而最后一列表示位置或平移。 + +问题是,在代码中单独获取这些部分会很麻烦。 +想要获取 Z 轴?你得这样写: + + +```js +const zAxis = [ + some4x4Matrix[2], + some4x4Matrix[6], + some4x4Matrix[10], +]; +``` + +唉! + +WebGL(以及它所基于的OpenGL ES)的解决方案居然是——把‘行’硬说成‘列’。 + +```js +const some4x4TranslationMatrix = [ + 1, 0, 0, 0, // this is column 0 + 0, 1, 0, 0, // this is column 1 + 0, 0, 1, 0, // this is column 2 + tx, ty, tz, 1, // this is column 3 +]; +``` + +现在它就符合数学定义了。对比上面的例子,如果我们想要获取 Z 轴,只需要做: + +```js +const zAxis = some4x4Matrix.slice(8, 11); +``` + +对于熟悉 C++ 的人来说,OpenGL 本身要求一个 4x4 矩阵的 16 个值在内存中是连续的,因此在 C++ 中我们可以创建一个 `Vec4` 结构体或类: + +```c++ +// C++ +struct Vec4 { + float x; + float y; + float z; + float w; +}; +``` + +然后我们可以用这四个 `Vec4` 来创建一个 4x4 矩阵: + +```c++ +// C++ +struct Mat4x4 { + Vec4 x_axis; + Vec4 y_axis; + Vec4 z_axis; + Vec4 translation; +} +``` + +或者直接写成 + +```c++ +// C++ +struct Mat4x4 { + Vec4 column[4]; +} +``` + +看似这样就能正常工作。 + +但遗憾的是,当你在代码中真正静态声明一个矩阵时,它的形式与数学中的矩阵表示法相去甚远。 + +```C++ +// C++ +Mat4x4 someTranslationMatrix = { + { 1, 0, 0, 0, }, + { 0, 1, 0, 0, }, + { 0, 0, 1, 0, }, + { tx, ty, tz, 1, }, +}; +``` + +回到 JavaScript 环境(我们通常没有类似 C++ 结构体的数据结构),情况就有所不同。 + +```js +const someTranslationMatrix = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + tx, ty, tz, 1, +]; +``` + +因此,采用将“行”称为“列”的这种约定,有些事情会变得更简单,但如果你是数学背景的人,可能会觉得更困惑。 + +我提到这些是因为这些文章是从程序员的视角写的,而不是数学家的视角。这意味着就像其他被当作二维数组使用的一维数组一样,行是横向排列的。 + + +```js +const someTranslationMatrix = [ + 1, 0, 0, 0, // row 0 + 0, 1, 0, 0, // row 1 + 0, 0, 1, 0, // row 2 + tx, ty, tz, 1, // row 3 +]; +``` + +就像 + +```js +// happy face image +const dataFor7x8OneChannelImage = [ + 0, 255, 255, 255, 255, 255, 0, // row 0 + 255, 0, 0, 0, 0, 0, 255, // row 1 + 255, 0, 255, 0, 255, 0, 255, // row 2 + 255, 0, 0, 0, 0, 0, 255, // row 3 + 255, 0, 255, 0, 255, 0, 255, // row 4 + 255, 0, 255, 255, 255, 0, 255, // row 5 + 255, 0, 0, 0, 0, 0, 255, // row 6 + 0, 255, 255, 255, 255, 255, 0, // row 7 +] +``` + +所以这些文章会把它们称作“行”。 + +如果你是数学专业出身,可能会觉得有些困惑。很抱歉,我没有更好的解决方案。我本可以把明显的第3行称作“列”,但那样也会让人迷惑,因为这不符合其他编程语言的习惯。 + +无论如何,希望这些解释能帮助你理解为什么文中的说明看起来不像数学书里的内容,而更像代码,且遵循了编程中的惯例。希望这能帮助你搞清楚其中的原理,也不会让习惯数学规范的人觉得太难理解。 From cfcea45c77f29341978ff9058a881a7893e0f19a Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Mon, 23 Jun 2025 18:07:17 +0800 Subject: [PATCH 14/22] Translate webgl-precision-issues.md to Chinese --- webgl/lessons/zh_cn/webgl-precision-issues.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-precision-issues.md diff --git a/webgl/lessons/zh_cn/webgl-precision-issues.md b/webgl/lessons/zh_cn/webgl-precision-issues.md new file mode 100644 index 000000000..4b566658e --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-precision-issues.md @@ -0,0 +1,227 @@ +Title: WebGL2 精度问题 +Description: WebGL2里的各种精度问题 +TOC: 精度问题 + +本文讨论 WebGL2 中的各种精度问题。 + +## `lowp`, `mediump`, `highp` + +在[本站的第一篇文章](webgl-fundamentals.html)中,我们创建了顶点着色器和片段着色器。 +在创建片段着色器时,顺带提到片段着色器没有默认的精度,所以我们需要通过添加这行代码来设置。 + +```glsl +precision highp float; +``` + +这到底是怎么回事? + +`lowp`、 `mediump` 和 `highp`是精度设置。 +这里的精度实际上指的是用多少位(bit)来存储一个值。 +JavaScript 中的数字使用 64 位,大多数 WebGL 中的数字只有 32 位。 +位数越少意味着速度越快,位数越多意味着精度越高和/或范围越大。 + +我不确定自己是否能解释清楚。 +你可以搜索[double vs float](https://www.google.com/search?q=double+vs+float) +了解更多精度问题的示例,但一种简单的理解方法是将其比作字节和短整型,或者在 JavaScript 中的 Uint8Array 和 Uint16Array 的区别。 + +* Uint8Array 是一个无符号 8 位整数数组。8 位能表示 28(256)个数,范围是 0 到 255。 +* Uint16Array 是一个无符号 16 位整数数组。16 位能表示 216(65536)个数,范围是 0 到 65535。 +* Uint32Array 是一个无符号 32 位整数数组。32 位能表示 232(约42亿)个数,范围是 0 到 4294967295。 + +`lowp`、`mediump` 和 `highp` 也是类似的概念。 + +* `lowp` 至少是 9 位。对于浮点数,其值范围大致是 -2 到 +2,整数则类似于 `Uint8Array` 或 `Int8Array`。 +* `mediump` 至少是 16 位。对于浮点数,其值范围大致是 -214 到 +214,整数类似于 `Uint16Array` 或 `Int16Array`。 +* `highp` 至少是 32 位。对于浮点数,其值范围大致是 -262 到 +262,整数类似于 `Uint32Array` 或 `Int32Array`。 + +需要注意的是,并非范围内的所有数值都能被表示。 +最容易理解的是 `lowp`,它只有 9 位,因此只能表示 512 个唯一值。 +虽然它的范围是 -2 到 +2,但在这之间有无限多个值,比如 1.9999999 和 1.999998,这两个数值都不能被 `lowp` 精确表示。 +例如,如果你用 `lowp` 做颜色计算,可能会出现色带现象。颜色范围是 0 到 1,而 lowp 在 0 到 1 之间大约只有 128 个可表示值。 +这意味着如果你想加一个非常小的值(比如 1/512),它可能根本不会改变数值,因为无法被表示,实际上就像加了 0。 + +理论上,我们可以在任何地方使用 `highp` 完全避免这些问题,但在实际设备上,使用 `lowp` 和 `mediump` 通常会比 `highp` 快很多,有时甚至显著更快。 + +还有一点,和 `Uint8Array`、`Uint16Array` 不同的是,`lowp`、`mediump`、`highp` 允许在内部使用更高的精度(更多位)。 +例如,在桌面 GPU 上,如果你在着色器中写了 `mediump`,它很可能仍然使用 32 位精度。 +这导致在开发时很难测试 `lowp` 或 `mediump` 的真正表现。 +要确认你的着色器在低精度设备上能正常工作,必须在实际使用较低精度的设备上测试。 + +如果你想用 `mediump` 以提高速度,常见问题包括比如点光源的高光计算,它在世界空间或视图空间传递的值可能超出 `mediump` 的范围。 +可能在某些设备上你只能舍弃高光计算。下面是将[点光源](webgl-3d-lighting-point.html)示例的片段着色器改为 `mediump` 的代码示例: + +```glsl +#version 300 es + +-precision highp float; ++precision mediump float; + +// Passed in and varied from the vertex shader. +in vec3 v_normal; +in vec3 v_surfaceToLight; +in vec3 v_surfaceToView; + +uniform vec4 u_color; +uniform float u_shininess; + +// we need to declare an output for the fragment shader +out vec4 outColor; + +void main() { + // because v_normal is a varying it's interpolated + // so it will not be a uint vector. Normalizing it + // will make it a unit vector again + vec3 normal = normalize(v_normal); + + vec3 surfaceToLightDirection = normalize(v_surfaceToLight); +- vec3 surfaceToViewDirection = normalize(v_surfaceToView); +- vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection); + + // compute the light by taking the dot product + // of the normal to the light's reverse direction + float light = dot(normal, surfaceToLightDirection); +- float specular = 0.0; +- if (light > 0.0) { +- specular = pow(dot(normal, halfVector), u_shininess); +- } + + outColor = u_color; + + // Lets multiply just the color portion (not the alpha) + // by the light + outColor.rgb *= light; + +- // Just add in the specular +- outColor.rgb += specular; +} +``` + +注意:即便如此还不够。在顶点着色器中我们有以下代码: + +```glsl + // compute the vector of the surface to the light + // and pass it to the fragment shader + v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition; +``` + +假设光源距离表面有 1000 个单位。 +然后我们进入片段着色器,执行这一行代码: + +```glsl + vec3 surfaceToLightDirection = normalize(v_surfaceToLight); +``` + +看起来似乎没问题。除了归一化向量的常规方法是除以其长度,而计算长度的标准方式是: + + +``` + float length = sqrt(v.x * v.x + v.y * v.y * v.z * v.z); +``` + +如果 x、y 或 z 中的某一个值是 1000,那么 1000×1000 就是 1000000。 +而 1000000 超出了 `mediump` 的表示范围。 + +这里的一个解决方案是在顶点着色器中进行归一化(normalize)。 + +``` + // compute the vector of the surface to the light + // and pass it to the fragment shader +- v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition; ++ v_surfaceToLight = normalize(u_lightWorldPosition - surfaceWorldPosition); +``` + +现在赋值给 `v_surfaceToLight` 的数值范围在 -1 到 +1 之间,这正好落在 `mediump` 的有效范围内。 + +请注意,在顶点着色器中进行归一化实际上不会得到完全相同的结果,但结果可能足够接近,以至于除非并排对比,否则没人会注意到差异。 + +像 `normalize`、`length`、`distance`、`dot` 这样的函数都会面临一个问题:如果参与计算的值过大,那么在 `mediump` 精度下就可能超出其表示范围。 + +不过,你实际上需要在一个 `mediump` 为 16 位的设备上进行测试。在桌面设备上,`mediump` 实际上使用的是与 `highp` 相同的 32 位精度,因此任何相关的问题在桌面上都不会显现出来。 + +## 检测对16位 `mediump` 的支持 + +你调用 `gl.getShaderPrecisionFormat`,传入着色器类型(`VERTEX_SHADER` 或 `FRAGMENT_SHADER`),以及以下精度类型之一: + +- `LOW_FLOAT` +- `MEDIUM_FLOAT` +- `HIGH_FLOAT` +- `LOW_INT` +- `MEDIUM_INT` +- `HIGH_INT` + +它会[返回精度信息]。 + +{{{example url="../webgl-precision-lowp-mediump-highp.html"}}} + +`gl.getShaderPrecisionFormat` 会返回一个对象,包含三个属性:`precision`、`rangeMin` 和 `rangeMax`。 + +对于 `LOW_FLOAT` 和 `MEDIUM_FLOAT`,如果它们实际上就是 `highp`,那么 `precision` 将是 23。否则,它们通常分别是 8 和 15,或者至少会小于 23。对于 `LOW_INT` 和 `MEDIUM_INT`,如果它们等同于 `highp`,那么 `rangeMin` 会是 31。如果小于 31,则说明例如 `mediump int` 比 `highp int` 更高效。 + +我的 Pixel 2 XL 对于 `mediump` 和 `lowp` 都使用 16 位。我不确定自己是否用过使用 9 位表示 `lowp` 的设备,因此也不清楚在这种情况下通常会遇到哪些问题。 + +在本文系列中,我们在片段着色器中通常会指定默认精度。我们也可以为每个变量单独指定精度,例如: + + +```glsl +uniform mediump vec4 color; // a uniform +in lowp vec4 normal; // an attribute or varying input +out lowp vec4 texcoord; // a fragment shader output or varying output +lowp float foo; // a variable +``` + +## 纹理格式 + +纹理是规范中另一个指出“实际使用的精度可能高于请求精度”的地方。 + +例如,你可以请求一个每通道 4 位、总共 16 位的纹理,像这样: + +``` +gl.texImage2D( + gl.TEXTURE_2D, // target + 0, // mip level + gl.RGBA4, // internal format + width, // width + height, // height + 0, // border + gl.RGBA, // format + gl.UNSIGNED_SHORT_4_4_4_4, // type + null, +); +``` + +但实现上实际上可能在内部使用更高分辨率的格式。 +我认为大多数桌面端会这样做,而大多数移动端 GPU 不会。 + +我们可以做个测试。首先我们会像上面那样请求一个每通道 4 位的纹理。 +然后我们会通过渲染一个 0 到 1 的渐变来[渲染到它](webgl-render-to-texture.html)。 + +接着我们会将该纹理渲染到画布上。如果纹理内部确实是每通道 4 位, +那么从我们绘制的渐变中只会有 16 个颜色级别。 +如果纹理实际上是每通道 8 位,我们将看到 256 个颜色级别。 + +{{{example url="../webgl-precision-textures.html"}}} + +在我的智能手机上运行时,我看到纹理使用的是每通道4位 +(至少红色通道是4位,因为我没有测试其他通道)。 + +
+ +而在我的桌面上,我看到纹理实际上使用的是每通道8位, +尽管我只请求了4位。 + +
+ +需要注意的一点是,WebGL 默认会对结果进行抖动处理, +使这种渐变看起来更平滑。你可以通过以下方式关闭抖动: + + +```js +gl.disable(gl.DITHER); +``` + +如果我不关闭抖动处理,那么我的智能手机会产生这样的效果。 + +
+ +就我目前所知,这种情况通常只会在以下特定场景出现:当开发者将某种低比特精度的纹理格式用作渲染目标,却未在实际采用该低分辨率的设备上进行测试时。 +若仅通过桌面端设备进行测试,由此引发的问题很可能无法被发现。 From 5b162ebda7de1e650dcf76bbc5cb2df94e4db2e9 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Tue, 24 Jun 2025 12:38:18 +0800 Subject: [PATCH 15/22] Translate webgl-tips.md into Chinese --- webgl/lessons/zh_cn/webgl-tips.md | 345 ++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-tips.md diff --git a/webgl/lessons/zh_cn/webgl-tips.md b/webgl/lessons/zh_cn/webgl-tips.md new file mode 100644 index 000000000..1bc5fd749 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-tips.md @@ -0,0 +1,345 @@ +Title: WebGL2 小贴士 +Description: 使用 WebGL 时可能遇到的一些小问题 +TOC: 画布截屏 + +本文收集了一些你在使用 WebGL 时可能遇到的、看起来太小而不值得单独写一篇文章的问题。 + +--- + + + +# 对 Canvas 截图 + +在浏览器中,实际上有两种函数可以对画布进行截图。 +一种旧方法是: +[`canvas.toDataURL`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) +另一种新的更好的方法是: +[`canvas.toBlob`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) + +所以你可能会认为,只需添加如下代码就能轻松截图: + +```html + ++ +``` + +```js +const elem = document.querySelector('#screenshot'); +elem.addEventListener('click', () => { + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); +}); + +const saveBlob = (function() { + const a = document.createElement('a'); + document.body.appendChild(a); + a.style.display = 'none'; + return function saveData(blob, fileName) { + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName; + a.click(); + }; +}()); +``` + +这是来自[动画那篇文章](webgl-animation.html)的示例,在其中加入了上面的代码,并添加了一些 CSS 来放置按钮。 + +{{{example url="../webgl-tips-screenshot-bad.html"}}} + +当我尝试时,我得到了这样的截图。 + +
+ +是的,这是一个空白图像。 + +根据你的浏览器/操作系统,它可能对你有效,但通常情况下它是无法工作的。 + +问题在于,出于性能和兼容性的考虑,浏览器默认会在你绘制完后,清除 WebGL 画布的绘图缓冲区。 + +有三种解决方案。 + +1. 在截图之前调用渲染代码 + + 我们使用的代码是一个 `drawScene` 函数。 + 最好让这段代码不改变任何状态,这样我们就可以在截图时调用它来进行渲染。 + + ```js + elem.addEventListener('click', () => { + + drawScene(); + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); + }); + ``` + +2. 在渲染循环中调用截图代码 + + 在这种情况下,我们只需设置一个标志表示我们想要截图,然后在渲染循环中实际执行截图操作。 + + ```js + let needCapture = false; + elem.addEventListener('click', () => { + needCapture = true; + }); + ``` + + 然后在我们的渲染循环中,也就是当前实现于 `drawScene` 的函数中,在所有内容绘制完成之后的某个位置。 + + ```js + function drawScene(time) { + ... + + + if (needCapture) { + + needCapture = false; + + canvas.toBlob((blob) => { + + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + + }); + + } + + ... + } + ``` + +3. 在创建 WebGL 上下文时,设置 `preserveDrawingBuffer: true` + + ```js + const gl = someCanvas.getContext('webgl2', {preserveDrawingBuffer: true}); + ``` + + 这会让 WebGL 在将画布与页面其他部分合成后不清除画布,但会阻止某些*可能的*优化。 + +我会选择上面的第 1 种方法。对于这个特定示例,我首先会把更新状态的代码部分与绘制的代码部分分离开。 + +```js + var then = 0; + +- requestAnimationFrame(drawScene); ++ requestAnimationFrame(renderLoop); + ++ function renderLoop(now) { ++ // Convert to seconds ++ now *= 0.001; ++ // Subtract the previous time from the current time ++ var deltaTime = now - then; ++ // Remember the current time for the next frame. ++ then = now; ++ ++ // Every frame increase the rotation a little. ++ rotation[1] += rotationSpeed * deltaTime; ++ ++ drawScene(); ++ ++ // Call renderLoop again next frame ++ requestAnimationFrame(renderLoop); ++ } + + // Draw the scene. ++ function drawScene() { +- function drawScene(now) { +- // Convert to seconds +- now *= 0.001; +- // Subtract the previous time from the current time +- var deltaTime = now - then; +- // Remember the current time for the next frame. +- then = now; +- +- // Every frame increase the rotation a little. +- rotation[1] += rotationSpeed * deltaTime; + + webglUtils.resizeCanvasToDisplaySize(gl.canvas); + + ... + +- // Call drawScene again next frame +- requestAnimationFrame(drawScene); + } +``` + +现在我们只需在截图之前调用 `drawScene` 即可 + +```js +elem.addEventListener('click', () => { ++ drawScene(); + canvas.toBlob((blob) => { + saveBlob(blob, `screencapture-${canvas.width}x${canvas.height}.png`); + }); +}); +``` + +现在它应该可以正常工作了。 + +{{{example url="../webgl-tips-screenshot-good.html" }}} + +如果你实际检查捕获的图像,会看到背景是透明的。 +详情请参见[这篇文章](webgl-and-alpha.html)。 + +--- + + + +# 防止画布被清除 + +假设你想让用户用一个动画对象进行绘画。 +在创建 WebGL 上下文时,需要传入 `preserveDrawingBuffer: true`。 +这可以防止浏览器清除画布。 + +采用[动画那篇文章](webgl-animation.html)中的最后一个示例 + +```js +var canvas = document.querySelector("#canvas"); +-var gl = canvas.getContext("webgl2"); ++var gl = canvas.getContext("webgl2", {preserveDrawingBuffer: true}); +``` + +并修改对 `gl.clear` 的调用,使其只清除深度缓冲区。 + +``` +-// Clear the canvas. +-gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); ++// Clear the depth buffer. ++gl.clear(gl.DEPTH_BUFFER_BIT); +``` + +{{{example url="../webgl-tips-preservedrawingbuffer.html" }}} + +注意,如果你真想做一个绘图程序,这不是一个解决方案, +因为每当我们改变画布的分辨率时,浏览器仍然会清除画布。 +我们是根据显示尺寸来改变分辨率的。显示尺寸会在窗口大小改变时变化, +这可能发生在用户下载文件时,甚至在另一个标签页,浏览器添加状态栏时。 +还包括用户旋转手机,浏览器从竖屏切换到横屏时。 + +如果你真的想做绘图程序,应该[渲染到纹理](webgl-render-to-texture.html)。 + +--- + + + +# 获取键盘输入 + +如果你制作的是全页面/全屏的 WebGL 应用,那么你可以随意处理, +但通常你希望某个 canvas 只是页面的一部分, +并希望用户点击 canvas 时它能接收键盘输入。 +不过 canvas 默认是无法获取键盘输入的。为了解决这个问题, +需要将 canvas 的 [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/tabIndex) +设置为 0 或更大。例如: + +```html + +``` + +不过这会引发一个新问题。任何设置了 `tabindex` 的元素在获得焦点时都会被高亮显示。 +为了解决这个问题,需要将其获得焦点时的 CSS 边框(outline)设置为 none。 + +```css +canvas:focus { + outline:none; +} +``` + +为演示起见,这里有三个 canvas + +```html + + + +``` + +以及仅针对最后一个 canvas 的一些 CSS + +```css +#c3:focus { + outline: none; +} +``` + +让我们给所有 canvas 都附加相同的事件监听器 + +```js +document.querySelectorAll('canvas').forEach((canvas) => { + const ctx = canvas.getContext('2d'); + + function draw(str) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(str, canvas.width / 2, canvas.height / 2); + } + draw(canvas.id); + + canvas.addEventListener('focus', () => { + draw('has focus press a key'); + }); + + canvas.addEventListener('blur', () => { + draw('lost focus'); + }); + + canvas.addEventListener('keydown', (e) => { + draw(`keyCode: ${e.keyCode}`); + }); +}); +``` + +注意,第一个 canvas 无法接受键盘输入。 +第二个 canvas 可以,但它会被高亮显示。 +第三个 canvas 同时应用了这两个解决方案。 + +{{{example url="../webgl-tips-tabindex.html"}}} + +--- + + + +# 将背景设为WebGL动画 + +一个常见问题是如何将WebGL动画设置为网页背景。 + +以下是两种最常用的实现方式: + +* 将Canvas的CSS `position` 设置为 `fixed`,如下所示: + +```css +#canvas { + position: fixed; + left: 0; + top: 0; + z-index: -1; + ... +} +``` + +并将 `z-index` 设为 -1。 + +这种方案的一个小缺点是:你的 JavaScript 代码必须与页面其他部分兼容,如果页面比较复杂,就需要确保 WebGL 代码中的 JavaScript 不会与页面其他功能的 JavaScript 产生冲突。 + +* 使用 `iframe` + +这正是本站[首页](/)采用的解决方案。 + +在您的网页中,只需插入一个iframe即可实现,例如: + +```html + +
+ Your content goes here. +
+``` + +接下来将这个iframe设置为全屏背景样式,本质上和我们之前设置canvas的代码相同——只是需要额外将 `border` 设为 `none`,因为iframe默认带有边框。具体实现如下: + +```css +#background { + position: fixed; + width: 100vw; + height: 100vh; + left: 0; + top: 0; + z-index: -1; + border: none; + pointer-events: none; +} +``` + +{{{example url="../webgl-tips-html-background.html"}}} From f18e0b552986381dc7d1909553a1dc0c4ddac2c0 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Tue, 24 Jun 2025 19:15:12 +0800 Subject: [PATCH 16/22] fixed build error --- .../zh_cn/webgl-cross-platform-issues.md | 302 ++++++++++++++++++ webgl/lessons/zh_cn/webgl-tips.md | 4 +- 2 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 webgl/lessons/zh_cn/webgl-cross-platform-issues.md diff --git a/webgl/lessons/zh_cn/webgl-cross-platform-issues.md b/webgl/lessons/zh_cn/webgl-cross-platform-issues.md new file mode 100644 index 000000000..fcc8f797f --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-cross-platform-issues.md @@ -0,0 +1,302 @@ +Title: WebGL2 Cross Platform Issues +Description: Things to be aware of when trying to make your WebGL app work everywhere. +TOC: Cross Platform Issues + +I probably comes as no shock that not all WebGL programs work on all devices or +browser. + +Here's a list of most of the issues you might run into off the top of my head + +## Performance + +A top end GPU probably runs 100x faster than a low-end GPU. The only way around +that that I know of is to either aim low, or else give the user options like +most Desktop PC apps do where they can choose performance or fidelity. + +## Memory + +Similarly a top end GPU might have 12 to 24 gig of ram where as a low end GPU +probably has less than 1gig. (I'm old so it's amazing to me low end = 1gig since +I started programming on machines with 16k to 64k of memory 😜) + +## Device Limits + +WebGL has various minimum supported features but your local device might support +> than that minimum which means it will fail on other devices that support less. + +Examples include: + +* The max texture size allowed + + 2048 or 4096 seems to be reasonable limits. At least as of 2020 it looks like + [99% of devices support 4096 but only 50% support > 4096](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE). + + Note: the max texture size is the maximum dimension the GPU can process. It + doesn't mean that GPU has enough memory for that dimension squared (for a 2D + texture) or cubed (for a 3D texture). For example some GPUs have a max size of + 16384. But a 3D texture 16384 on each side would require 16 terabytes of + memory!!! + +* The maximum number of vertex attributes in a single program + + In WebGL1 the minimum supported is 8. In WebGL2 it's 16. If you're using more than that + then your code will fail on a machine with only the minimum + +* The maximum number of uniform vectors + + These are specified separately for vertex shaders and fragment shaders. + + In WebGL1 it's 128 for vertex shaders and 16 for fragment shaders + In WebGL2 it's 256 for vertex shaders and 224 for fragment shaders + + Note that uniforms can be "packed" so the number above is how many `vec4`s + can be used. Theoretically you could have 4x the number of `float` uniforms. + but there is an algorithm that fits them in. You can imagine the space as + an array with 4 columns, one row for each of the maximum uniform vectors above. + + ``` + +-+-+-+-+ + | | | | | <- one vec4 + | | | | | | + | | | | | | + | | | | | V + | | | | | max uniform vectors rows + | | | | | + | | | | | + | | | | | + ... + + ``` + + First `vec4`s are allocated with a `mat4` being 4 `vec4`s. Then `vec3`s are + fit in the space left. Then `vec2`s followed by `float`s. So imagine we had 1 + `mat4`, 2 `vec3`s, 2 `vec2`s and 3 `float`s + + ``` + +-+-+-+-+ + |m|m|m|m| <- the mat4 takes 4 rows + |m|m|m|m| + |m|m|m|m| + |m|m|m|m| + |3|3|3| | <- the 2 vec3s take 2 rows + |3|3|3| | + |2|2|2|2| <- the 2 vec2s can squeeze into 1 row + |f|f|f| | <- the 3 floats fit in one row + ... + + ``` + + Further, an array of uniforms is always vertical so for example if the maximum + allowed uniform vectors is 16 then you can not have a 17 element `float` array + and in fact if you had a single `vec4` that would take an entire row so there + are only 15 rows left meaning the largest array you can have would be 15 + elements. + + My advice though is don't count on perfect packing. Although the spec says the + algorithm above is required to pass there are too many combinations to test + that all drivers pass. Just be aware if you're getting close the limit. + + note: varyings and attributes can not be packed. + +* The maximum varying vectors. + + WebGL1 the minimum is 8. WebGL2 it's 16. + + If you use more than your code will not work on a machine with only the minimum. + +* The maximum texture units + + There are 3 values here. + + 1. How many texture units there are + 2. How many texture units a vertex shader can reference + 3. How many texture units a fragment shader can reference + + + + + + + + + + +
WebGL1WebGL2
min texture units that exist832
min texture units a vertex shader can reference0!16
min texture units a fragment shader can reference816
+ + It's important to note the **0** for a vertex shader in WebGL1. Note that that's probably not the end of the world. + Apparently [~97% of all devices support at least 4](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_TEXTURE_IMAGE_UNITS). + Still, you might want to check so you can either tell the user that your app is not going to work for them or + you can fallback to some other shaders. + +There are other limits as well. To look them up you call `gl.getParameter` with +the following values. + +
+ + + + + + + + + + + + + + +
MAX_TEXTURE_SIZE max size of a texture
MAX_VERTEX_ATTRIBS num attribs you can have
MAX_VERTEX_UNIFORM_VECTORS num vec4 uniforms a vertex shader can have
MAX_VARYING_VECTORS num varyings you have
MAX_COMBINED_TEXTURE_IMAGE_UNITSnum texture units that exist
MAX_VERTEX_TEXTURE_IMAGE_UNITS num texture units a vertex shader can reference
MAX_TEXTURE_IMAGE_UNITS num texture units a fragment shader can reference
MAX_FRAGMENT_UNIFORM_VECTORS num vec4 uniforms a fragment shader can have
MAX_CUBE_MAP_TEXTURE_SIZE max size of a cubemap
MAX_RENDERBUFFER_SIZE max size of a renderbuffer
MAX_VIEWPORT_DIMS max size of the viewport
+
+ +That is not the entire list. For example the max point size and max line thickness +but you should basically assume the max line thickness is 1.0 and that POINTS +are only useful for simple demos where you don't care about +[the clipping issues](#points-lines-viewport-scissor-behavior). + +WebGL2 adds several more. A few common ones are + +
+ + + + + + + + + + + +
MAX_3D_TEXTURE_SIZE max size of a 3D texture
MAX_DRAW_BUFFERS num color attachments you can have
MAX_ARRAY_TEXTURE_LAYERS max layers in a 2D texture array
MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS num varyings you can output to separate buffers when using transform feedback
MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTSnum varyings you can output when sending them all to a single buffer
MAX_COMBINED_UNIFORM_BLOCKS num uniform blocks you can use overall
MAX_VERTEX_UNIFORM_BLOCKS num uniform blocks a vertex shader can use
MAX_FRAGMENT_UNIFORM_BLOCKS num uniform blocks a fragment shader can use
+
+ +## Depth Buffer resolution + +A few really old mobile devices use 16bit depth buffers. Otherwise, AFAICT 99% +of devices use a 24bit depth buffer so you probably don't have to worry about +this. + +## readPixels format/type combos + +Only certain format/type combos are guaranteed to work. Other combos are +optional. This is covered in [this article](webgl-readpixels.html). + +## framebuffer attachment combos + +Framebuffers can have 1 or more attachments of textures and renderbuffers. + +In WebGL1 only 3 combinations of attachments are guaranteed to work. + +1. a single format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` +2. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a + format = `DEPTH_COMPONENT` renderbuffer attached as `DEPTH_ATTACHMENT` +3. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a + format = `DEPTH_STENCIL` renderbuffer attached as `DEPTH_STENCIL_ATTACHMENT` + +All other combinations are up to the implementation which you check by calling +`gl.checkFramebufferStatus` and seeing if it returned `FRAMEBUFFER_COMPLETE`. + +WebGL2 guarantees to be able to write to many more formats but still has the +limit in that **any combination can fail!** Your best bet might be if all the +color attachments are the same format if you attach more than 1. + +## Extensions + +Many features of WebGL1 and WebGL2 are optional. The entire point of having an +API called `getExtension` is that it can fail if the extension does not exist +and so you should be checking for that failure and not blindly assuming it will +succeed. + +Probably the most common missing extension on WebGL1 and WebGL2 is +`OES_texture_float_linear` which is the ability to filter a floating point +texture, meaning the ability to support setting `TEXTURE_MIN_FILTER` and +`TEXTURE_MAX_FILTER` to anything except `NEAREST`. Many mobile devices do not +support this. + +In WebGL1 another often missing extension is `WEBGL_draw_buffers` which is the +ability to attach more than 1 color attachment to a framebuffer is still at +around 70% for desktop and almost none for smartphones (that seems wrong). +Basically any device that can run WebGL2 should also support +`WEBGL_draw_buffers` in WebGL1 but still, it's apparently still an issue. If you +are needing to render to multiple textures at once it's likely your page needs a +high end GPU period. Still, you should check if the user device supports it and +if not provide a friendly explanation. + +For WebGL1 the following 3 extensions seem almost universally supported so while +you might want to warn the user your page is not going to work if they are +missing it's likely that user has an extremely old device that wasn't going to +run your page well anyway. + +They are, `ANGLE_instance_arrays` (the ability to use [instanced drawing](webgl-instanced-drawing.html)), +`OES_vertex_array_object` (the ability to store all the attribute state in an object so you can swap all +that state with a single function call. See [this](webgl-attributes.html)), and `OES_element_index_uint` +(the ability to use `UNSIGNED_INT` 32 bit indices with [`drawElements`](webgl-indexed-vertices.html)). + +## attribute locations + +A semi common bug is not looking up attribute locations. For example you have a vertex shader like + +```glsl +attribute vec4 position; +attribute vec2 texcoord; + +uniform mat4 matrix; + +varying vec2 v_texcoord; + +void main() { + gl_Position = matrix * position; + v_texcoord = texcoord; +} +``` + +Your code assumes that `position` will be attribute 0 and `texcoord` will be +attribute 1 but that is not guaranteed. So it runs for you but fails for someone +else. Often this can be a bug in that you didn't do this intentionally but +through an error in the code things work when the locations are one way but not +another. + +There are 3 solutions. + +1. Always look up the locations. +2. Assign locations by calling `gl.bindAttribLocation` before calling `gl.linkProgram` +3. WebGL2 only, set the locations in the shader as in + + ```glsl + #version 300 es + layout(location = 0) vec4 position; + latout(location = 1) vec2 texcoord; + ... + ``` + + Solution 2 seems the most [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) where as solution 3 + seems the most [W.E.T.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions) unless + you're generating your textures at runtime. + +## GLSL undefined behavior + +Several GLSL functions have undefined behavior. For example `pow(x, y)` is +undefined if `x < 0`. There is a longer list at [the bottom of the article on +spot lighting](webgl-3d-lighting-spot.html). + +## Shader precision issues + +In 2020 the biggest issue here is if you use `mediump` or `lowp` in your shaders +then on desktop the GPU will really use `highp` but on mobile they'll actually be +`mediump` and or `lowp` and so you won't notice any issues when developing on desktop. + +See [this article for more details](webgl-precision-issues.html). + +## Points, Lines, Viewport, Scissor behavior + +`POINTS` and `LINES` in WebGL can have a max size of 1 and in fact for `LINES` +that is now the most common limit. Further whether points are clipped when their +center is outside the viewport is implementation defined. See the bottom of +[this article](webgl-drawing-without-data.html#pointissues). + +Similarly, whether or not the viewport clips vertices only or also pixels is +undefined. The scissor always clips pixels so turn on the scissor test and set +the scissor size if you set the viewport smaller than the thing you're drawing +to and you're drawing LINES or POINTS. + diff --git a/webgl/lessons/zh_cn/webgl-tips.md b/webgl/lessons/zh_cn/webgl-tips.md index 1bc5fd749..40a3a62cb 100644 --- a/webgl/lessons/zh_cn/webgl-tips.md +++ b/webgl/lessons/zh_cn/webgl-tips.md @@ -1,6 +1,6 @@ Title: WebGL2 小贴士 Description: 使用 WebGL 时可能遇到的一些小问题 -TOC: 画布截屏 +TOC: # 本文收集了一些你在使用 WebGL 时可能遇到的、看起来太小而不值得单独写一篇文章的问题。 @@ -8,7 +8,7 @@ TOC: 画布截屏 -# 对 Canvas 截图 +# 画布截屏 在浏览器中,实际上有两种函数可以对画布进行截图。 一种旧方法是: From 499a50aecd6f1926354296b3eed6374928343b86 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Tue, 24 Jun 2025 22:32:39 +0800 Subject: [PATCH 17/22] Translate webgl-cross-platform-issues.md into Chinese --- .../zh_cn/webgl-cross-platform-issues.md | 325 +++++++++--------- 1 file changed, 156 insertions(+), 169 deletions(-) diff --git a/webgl/lessons/zh_cn/webgl-cross-platform-issues.md b/webgl/lessons/zh_cn/webgl-cross-platform-issues.md index fcc8f797f..5284feb29 100644 --- a/webgl/lessons/zh_cn/webgl-cross-platform-issues.md +++ b/webgl/lessons/zh_cn/webgl-cross-platform-issues.md @@ -1,58 +1,55 @@ -Title: WebGL2 Cross Platform Issues +Title: WebGL2 跨平台问题 Description: Things to be aware of when trying to make your WebGL app work everywhere. -TOC: Cross Platform Issues +TOC: 跨平台问题 -I probably comes as no shock that not all WebGL programs work on all devices or -browser. +你也许早就知道,并不所有 WebGL 程序都能在所有设备或浏览器上运行。 -Here's a list of most of the issues you might run into off the top of my head +以下是我脑海中能想到的大多数可能遇到的问题列表。 -## Performance +## 性能 -A top end GPU probably runs 100x faster than a low-end GPU. The only way around -that that I know of is to either aim low, or else give the user options like -most Desktop PC apps do where they can choose performance or fidelity. +高端 GPU 的运行速度可能是低端 GPU 的 100 倍。 +我知道的唯一解决方法是要么把目标定得低一些,要么像大多数桌面程序那样提供性能与画质的选项供用户选择。 -## Memory +## 内存 -Similarly a top end GPU might have 12 to 24 gig of ram where as a low end GPU -probably has less than 1gig. (I'm old so it's amazing to me low end = 1gig since -I started programming on machines with 16k to 64k of memory 😜) +同样,高端 GPU 可能拥有 12 到 24 GB 的内存, +而低端 GPU 可能不到 1 GB。 +(我年纪大了,觉得“低端 = 1GB”已经很神奇了, +因为我最初是在只有 16K 到 64K 内存的机器上编程的 😜) -## Device Limits +## 设备限制 -WebGL has various minimum supported features but your local device might support -> than that minimum which means it will fail on other devices that support less. +WebGL 规定了各种最低支持特性,但你本地的设备可能支持 +> 高于这个最低标准的能力,这意味着代码可能会在其他支持较少的设备上运行失败。 -Examples include: +一些示例包括: -* The max texture size allowed +* 允许的最大纹理尺寸 - 2048 or 4096 seems to be reasonable limits. At least as of 2020 it looks like - [99% of devices support 4096 but only 50% support > 4096](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE). + 2048 或 4096 被认为是比较合理的限制。至少截至 2020 年, + 看起来[99% 的设备支持 4096,但只有 50% 支持大于 4096 的尺寸](https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE)。 - Note: the max texture size is the maximum dimension the GPU can process. It - doesn't mean that GPU has enough memory for that dimension squared (for a 2D - texture) or cubed (for a 3D texture). For example some GPUs have a max size of - 16384. But a 3D texture 16384 on each side would require 16 terabytes of - memory!!! + 注意:最大纹理尺寸是 GPU 可以处理的最大维度。 + 它并不意味着 GPU 有足够的内存来存储该维度平方(对于 2D 纹理)或立方(对于 3D 纹理)大小的数据。 + 例如,某些 GPU 最大尺寸为 16384,但一个每边都是 16384 的 3D 纹理将需要 16 TB 的内存!!! -* The maximum number of vertex attributes in a single program +* 单个程序中支持的最大顶点属性数量 - In WebGL1 the minimum supported is 8. In WebGL2 it's 16. If you're using more than that - then your code will fail on a machine with only the minimum + 在 WebGL1 中,最低支持为 8 个;在 WebGL2 中为 16 个。 + 如果你使用的数量超过这些,那么在只有最小支持能力的设备上,代码就会失败。 -* The maximum number of uniform vectors +* 支持的最大 uniform 向量数量 - These are specified separately for vertex shaders and fragment shaders. + 这些数量在顶点着色器和片段着色器中是分别指定的。 - In WebGL1 it's 128 for vertex shaders and 16 for fragment shaders - In WebGL2 it's 256 for vertex shaders and 224 for fragment shaders + WebGL1 中,顶点着色器为 128,片段着色器为 16 + WebGL2 中,顶点着色器为 256,片段着色器为 224 - Note that uniforms can be "packed" so the number above is how many `vec4`s - can be used. Theoretically you could have 4x the number of `float` uniforms. - but there is an algorithm that fits them in. You can imagine the space as - an array with 4 columns, one row for each of the maximum uniform vectors above. + 注意,uniform 是可以被“打包(packed)”的,上面的数字表示你可以使用的 `vec4` 数量。 + 理论上你可以使用 4 倍数量的 `float` 类型 uniform, + 但这依赖于打包算法。你可以将这个空间想象成一个 4 列的数组, + 每一行对应一个最大 uniform 向量。 ``` +-+-+-+-+ @@ -67,10 +64,10 @@ Examples include: ... ``` - - First `vec4`s are allocated with a `mat4` being 4 `vec4`s. Then `vec3`s are - fit in the space left. Then `vec2`s followed by `float`s. So imagine we had 1 - `mat4`, 2 `vec3`s, 2 `vec2`s and 3 `float`s + + 首先会分配 `vec4`,其中一个 `mat4` 占用 4 个 `vec4`。 + 然后是将 `vec3` 填入剩余空间,接着是 `vec2`,最后是 `float`。 + 所以假设我们有:1 个 `mat4`,2 个 `vec3`,2 个 `vec2` 和 3 个 `float` ``` +-+-+-+-+ @@ -86,156 +83,148 @@ Examples include: ``` - Further, an array of uniforms is always vertical so for example if the maximum - allowed uniform vectors is 16 then you can not have a 17 element `float` array - and in fact if you had a single `vec4` that would take an entire row so there - are only 15 rows left meaning the largest array you can have would be 15 - elements. + 此外,uniform 数组总是按“垂直”方式分布的,例如如果最大允许的 uniform 向量是 16,那么你就不能拥有一个 17 元素的 `float` 数组。 + 实际上,如果你有一个 `vec4`,它将占据整整一行,也就是说只剩下 15 行,因此你最多只能拥有 15 个元素的数组。 - My advice though is don't count on perfect packing. Although the spec says the - algorithm above is required to pass there are too many combinations to test - that all drivers pass. Just be aware if you're getting close the limit. + 不过我的建议是:不要指望完美的打包。尽管规范中说明上面那个打包算法是必须支持的, + 但组合太多,无法测试所有驱动都正确实现了它。 + 只要你知道自己正在接近上限即可。 - note: varyings and attributes can not be packed. + 注意:varyings 和 attributes 是无法打包的。 -* The maximum varying vectors. +* 最大 varying 向量数 - WebGL1 the minimum is 8. WebGL2 it's 16. + WebGL1 的最小值是 8,WebGL2 是 16。 - If you use more than your code will not work on a machine with only the minimum. + 如果你使用的数量超过了这个限制,那么代码将在只支持最低值的设备上无法运行。 -* The maximum texture units +* 最大纹理单元数 - There are 3 values here. + 这里有三个相关值: - 1. How many texture units there are - 2. How many texture units a vertex shader can reference - 3. How many texture units a fragment shader can reference + 1. 一共有多少个纹理单元 + 2. 顶点着色器最多可以引用多少个纹理单元 + 3. 片段着色器最多可以引用多少个纹理单元 - - - + + +
WebGL1WebGL2
min texture units that exist832
min texture units a vertex shader can reference0!16
min texture units a fragment shader can reference816
最少存在的纹理单元数量832
顶点着色器最少可引用的纹理单元数量0!16
片段着色器最少可引用的纹理单元数量816
- It's important to note the **0** for a vertex shader in WebGL1. Note that that's probably not the end of the world. - Apparently [~97% of all devices support at least 4](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_TEXTURE_IMAGE_UNITS). - Still, you might want to check so you can either tell the user that your app is not going to work for them or - you can fallback to some other shaders. + 需要特别注意的是,WebGL1 中顶点着色器的纹理单元数量是 **0**。 + 不过这可能并不是什么致命问题。 + 显然,[大约 97% 的设备至少支持 4 个](https://web3dsurvey.com/webgl/parameters/MAX_VERTEX_TEXTURE_IMAGE_UNITS)。 + 尽管如此,你可能还是希望进行检测,以便在不兼容时提醒用户应用无法运行, + 或者退回到其他着色器方案。 + +此外还有其他一些限制。要查看这些限制,你可以使用以下参数调用 `gl.getParameter`。 -There are other limits as well. To look them up you call `gl.getParameter` with -the following values.
- - - - - - - - - - - + + + + + + + + + + +
MAX_TEXTURE_SIZE max size of a texture
MAX_VERTEX_ATTRIBS num attribs you can have
MAX_VERTEX_UNIFORM_VECTORS num vec4 uniforms a vertex shader can have
MAX_VARYING_VECTORS num varyings you have
MAX_COMBINED_TEXTURE_IMAGE_UNITSnum texture units that exist
MAX_VERTEX_TEXTURE_IMAGE_UNITS num texture units a vertex shader can reference
MAX_TEXTURE_IMAGE_UNITS num texture units a fragment shader can reference
MAX_FRAGMENT_UNIFORM_VECTORS num vec4 uniforms a fragment shader can have
MAX_CUBE_MAP_TEXTURE_SIZE max size of a cubemap
MAX_RENDERBUFFER_SIZE max size of a renderbuffer
MAX_VIEWPORT_DIMS max size of the viewport
MAX_TEXTURE_SIZE 纹理的最大尺寸
MAX_VERTEX_ATTRIBS 可用的顶点属性数量
MAX_VERTEX_UNIFORM_VECTORS 顶点着色器中可用的 vec4 uniform 数量
MAX_VARYING_VECTORS 可用的 varying 数量
MAX_COMBINED_TEXTURE_IMAGE_UNITS存在的纹理单元总数
MAX_VERTEX_TEXTURE_IMAGE_UNITS 顶点着色器可引用的纹理单元数
MAX_TEXTURE_IMAGE_UNITS 片段着色器可引用的纹理单元数
MAX_FRAGMENT_UNIFORM_VECTORS 片段着色器中可用的 vec4 uniform 数量
MAX_CUBE_MAP_TEXTURE_SIZE 立方体贴图的最大尺寸
MAX_RENDERBUFFER_SIZE 渲染缓冲区的最大尺寸
MAX_VIEWPORT_DIMS 视口的最大尺寸
-That is not the entire list. For example the max point size and max line thickness -but you should basically assume the max line thickness is 1.0 and that POINTS -are only useful for simple demos where you don't care about -[the clipping issues](#points-lines-viewport-scissor-behavior). -WebGL2 adds several more. A few common ones are +这并不是完整的列表。例如最大点大小和最大线宽也有限制,但你基本可以假设最大线宽就是 1.0,而 POINTS 通常只适用于不在意[裁剪问题](#points-lines-viewport-scissor-behavior)的简单演示。 + + +WebGL2 增加了更多限制。几个常见的如下:
- - - - - - - - + + + + + + + +
MAX_3D_TEXTURE_SIZE max size of a 3D texture
MAX_DRAW_BUFFERS num color attachments you can have
MAX_ARRAY_TEXTURE_LAYERS max layers in a 2D texture array
MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS num varyings you can output to separate buffers when using transform feedback
MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTSnum varyings you can output when sending them all to a single buffer
MAX_COMBINED_UNIFORM_BLOCKS num uniform blocks you can use overall
MAX_VERTEX_UNIFORM_BLOCKS num uniform blocks a vertex shader can use
MAX_FRAGMENT_UNIFORM_BLOCKS num uniform blocks a fragment shader can use
MAX_3D_TEXTURE_SIZE 3D 纹理的最大尺寸
MAX_DRAW_BUFFERS 可用的颜色附件数量
MAX_ARRAY_TEXTURE_LAYERS 2D 纹理数组中的最大图层数
MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 使用 transform feedback 时可输出到独立缓冲区的 varying 数量
MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS当输出到单个缓冲区时可输出的 varying 总数
MAX_COMBINED_UNIFORM_BLOCKS 所有着色器中可使用的 uniform block 总数
MAX_VERTEX_UNIFORM_BLOCKS 顶点着色器中可用的 uniform block 数量
MAX_FRAGMENT_UNIFORM_BLOCKS 片段着色器中可用的 uniform block 数量
-## Depth Buffer resolution +## 深度缓冲区分辨率 + +一些非常老旧的移动设备使用 16 位深度缓冲区。除此之外,据我所知,99% 的设备都使用 24 位深度缓冲区, +所以你大概率无需担心这个问题。 -A few really old mobile devices use 16bit depth buffers. Otherwise, AFAICT 99% -of devices use a 24bit depth buffer so you probably don't have to worry about -this. +## readPixels 的 format/type 组合 -## readPixels format/type combos +只有某些格式/类型组合是强制支持的,其他组合是可选的。 +这个问题在[这篇文章](webgl-readpixels.html)中有详细介绍。 -Only certain format/type combos are guaranteed to work. Other combos are -optional. This is covered in [this article](webgl-readpixels.html). +## framebuffer 附件组合 -## framebuffer attachment combos +帧缓冲可以附加一个或多个纹理和渲染缓冲对象作为附件。 -Framebuffers can have 1 or more attachments of textures and renderbuffers. +在 WebGL1 中,只有以下三种附件组合是被保证支持的: -In WebGL1 only 3 combinations of attachments are guaranteed to work. +1. 将格式为RGBA、类型为UNSIGNED_BYTE的纹理附加为COLOR_ATTACHMENT0 +2. 将格式为RGBA、类型为UNSIGNED_BYTE的纹理附加为COLOR_ATTACHMENT0,同时将格式为DEPTH_COMPONENT的渲染缓冲区附加为DEPTH_ATTACHMENT +3. 将格式为RGBA、类型为UNSIGNED_BYTE的纹理附加为COLOR_ATTACHMENT0,同时将格式为DEPTH_STENCIL的渲染缓冲区附加为DEPTH_STENCIL_ATTACHMENT -1. a single format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` -2. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a - format = `DEPTH_COMPONENT` renderbuffer attached as `DEPTH_ATTACHMENT` -3. a format = `RGBA`, type = `UNSIGNED_BYTE` texture as `COLOR_ATTACHMENT0` and a - format = `DEPTH_STENCIL` renderbuffer attached as `DEPTH_STENCIL_ATTACHMENT` +所有其他组合都由具体实现决定是否支持。你可以通过调用 +`gl.checkFramebufferStatus` 并检查是否返回 `FRAMEBUFFER_COMPLETE` 来验证支持情况。 -All other combinations are up to the implementation which you check by calling -`gl.checkFramebufferStatus` and seeing if it returned `FRAMEBUFFER_COMPLETE`. +WebGL2 保证可以写入更多格式,但依然存在**任何组合都有可能失败!**的限制。 +如果你附加了多个颜色附件,最稳妥的方法是确保它们都使用相同的格式。 -WebGL2 guarantees to be able to write to many more formats but still has the -limit in that **any combination can fail!** Your best bet might be if all the -color attachments are the same format if you attach more than 1. +## 扩展(Extensions) -## Extensions +WebGL1 和 WebGL2 中的许多功能都是可选的。 +`getExtension` 这个 API 的意义就在于它可能失败(如果扩展不存在), +所以你应该检查它是否返回了有效扩展,而不是盲目假设它总能成功。 -Many features of WebGL1 and WebGL2 are optional. The entire point of having an -API called `getExtension` is that it can fail if the extension does not exist -and so you should be checking for that failure and not blindly assuming it will -succeed. +在 WebGL1 和 WebGL2 中,最常见缺失的扩展之一是 +`OES_texture_float_linear`,它允许对浮点纹理进行过滤, +也就是说可以把 `TEXTURE_MIN_FILTER` 和 `TEXTURE_MAG_FILTER` +设置为除 `NEAREST` 之外的值。很多移动设备并不支持这个扩展。 -Probably the most common missing extension on WebGL1 and WebGL2 is -`OES_texture_float_linear` which is the ability to filter a floating point -texture, meaning the ability to support setting `TEXTURE_MIN_FILTER` and -`TEXTURE_MAX_FILTER` to anything except `NEAREST`. Many mobile devices do not -support this. +在 WebGL1 中另一个常缺失的扩展是 `WEBGL_draw_buffers`, +这个扩展允许将多个颜色附件绑定到一个帧缓冲上。 +在桌面平台上支持率大约是 70%,而在智能手机上几乎没有支持(虽然这听起来不太对)。 +基本上,任何能运行 WebGL2 的设备应该也支持 WebGL1 的 `WEBGL_draw_buffers`, +但这显然仍然是个潜在问题。 +如果你需要一次性渲染到多个纹理,很可能你的网站就是为高端 GPU 设计的。 +不过你仍应检测用户设备是否支持,并在不支持时给出友好的提示说明。 -In WebGL1 another often missing extension is `WEBGL_draw_buffers` which is the -ability to attach more than 1 color attachment to a framebuffer is still at -around 70% for desktop and almost none for smartphones (that seems wrong). -Basically any device that can run WebGL2 should also support -`WEBGL_draw_buffers` in WebGL1 but still, it's apparently still an issue. If you -are needing to render to multiple textures at once it's likely your page needs a -high end GPU period. Still, you should check if the user device supports it and -if not provide a friendly explanation. +对于 WebGL1,以下 3 个扩展几乎被所有设备支持, +所以即使你希望在缺失时警告用户页面无法正常运行, +这些用户通常是极其老旧的设备,原本也跑不动你的页面: -For WebGL1 the following 3 extensions seem almost universally supported so while -you might want to warn the user your page is not going to work if they are -missing it's likely that user has an extremely old device that wasn't going to -run your page well anyway. +- `ANGLE_instanced_arrays`:支持[实例化绘制](webgl-instanced-drawing.html) +- `OES_vertex_array_object`:支持将所有 attribute 状态存入对象中, + 从而通过一次函数调用切换所有状态,见 [这里](webgl-attributes.html) +- `OES_element_index_uint`:允许使用 `UNSIGNED_INT` 类型的 32 位索引, + 与 [`drawElements`](webgl-indexed-vertices.html) 配合使用 -They are, `ANGLE_instance_arrays` (the ability to use [instanced drawing](webgl-instanced-drawing.html)), -`OES_vertex_array_object` (the ability to store all the attribute state in an object so you can swap all -that state with a single function call. See [this](webgl-attributes.html)), and `OES_element_index_uint` -(the ability to use `UNSIGNED_INT` 32 bit indices with [`drawElements`](webgl-indexed-vertices.html)). +## attribute 位置(attribute locations) -## attribute locations +一个较常见的 bug 是没有正确获取 attribute 的位置。 -A semi common bug is not looking up attribute locations. For example you have a vertex shader like +例如你有一个顶点着色器如下: ```glsl attribute vec4 position; @@ -251,17 +240,16 @@ void main() { } ``` -Your code assumes that `position` will be attribute 0 and `texcoord` will be -attribute 1 but that is not guaranteed. So it runs for you but fails for someone -else. Often this can be a bug in that you didn't do this intentionally but -through an error in the code things work when the locations are one way but not -another. +你的代码假设 `position` 是 attribute 0,`texcoord` 是 attribute 1, +但这是**没有保证的**。所以它在你这能运行,在别人那可能就失败了。 +这类问题往往是因为你没有明确这么指定位置, +但由于某些代码错误,恰好在你这里按预期的方式分配了位置。 -There are 3 solutions. +有三种解决方案: -1. Always look up the locations. -2. Assign locations by calling `gl.bindAttribLocation` before calling `gl.linkProgram` -3. WebGL2 only, set the locations in the shader as in +1. 始终使用 `gl.getAttribLocation` 显式查询位置 +2. 在调用 `gl.linkProgram` 之前,使用 `gl.bindAttribLocation` 显式绑定位置 +3. 仅限 WebGL2:可以直接在 shader 中设置 attribute 的位置,例如: ```glsl #version 300 es @@ -270,33 +258,32 @@ There are 3 solutions. ... ``` - Solution 2 seems the most [D.R.Y.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) where as solution 3 - seems the most [W.E.T.](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions) unless - you're generating your textures at runtime. - -## GLSL undefined behavior + 方案 2 看起来是最符合 [D.R.Y. 原则](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 的, + 而方案 3 则是最 [W.E.T.(重复)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself#DRY_vs_WET_solutions) 的—— + 除非你是在运行时生成 shader。 -Several GLSL functions have undefined behavior. For example `pow(x, y)` is -undefined if `x < 0`. There is a longer list at [the bottom of the article on -spot lighting](webgl-3d-lighting-spot.html). +## GLSL 未定义行为 -## Shader precision issues +一些 GLSL 函数具有未定义行为。例如,当 `x < 0` 时,`pow(x, y)` 的结果是未定义的。 +更详细的列表见[这篇关于聚光灯照明的文章底部](webgl-3d-lighting-spot.html)。 -In 2020 the biggest issue here is if you use `mediump` or `lowp` in your shaders -then on desktop the GPU will really use `highp` but on mobile they'll actually be -`mediump` and or `lowp` and so you won't notice any issues when developing on desktop. +## Shader 精度问题 -See [this article for more details](webgl-precision-issues.html). +截至 2020 年,这方面最大的问题是: +如果你在着色器中使用了 `mediump` 或 `lowp`,在桌面端 GPU 实际会使用 `highp`, +但在移动设备上它们真的就是 `mediump` 或 `lowp`。 +这意味着你在桌面开发时可能不会发现任何问题。 -## Points, Lines, Viewport, Scissor behavior +详细内容见[这篇文章](webgl-precision-issues.html)。 -`POINTS` and `LINES` in WebGL can have a max size of 1 and in fact for `LINES` -that is now the most common limit. Further whether points are clipped when their -center is outside the viewport is implementation defined. See the bottom of -[this article](webgl-drawing-without-data.html#pointissues). +## 点、线、视口和剪裁行为 -Similarly, whether or not the viewport clips vertices only or also pixels is -undefined. The scissor always clips pixels so turn on the scissor test and set -the scissor size if you set the viewport smaller than the thing you're drawing -to and you're drawing LINES or POINTS. +在 WebGL 中,`POINTS` 和 `LINES` 的最大尺寸可能就是 1, +实际上对于 `LINES` 来说,这已成为最常见的限制。 +另外,当点的中心在视口外时是否会被裁剪,是由实现决定的, +见[这篇文章底部](webgl-drawing-without-data.html#pointissues)。 +类似地,视口是否只裁剪顶点、还是同时裁剪像素,也是未定义的。 +但剪裁测试(scissor test)始终裁剪像素。 +因此,如果你设置了比目标区域更小的视口,并且正在绘制 LINES 或 POINTS, +你应该开启剪裁测试并设置合适的剪裁区域。 From 02844802c52d4295b2cb4905b070dd65d446b106 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Wed, 25 Jun 2025 00:14:48 +0800 Subject: [PATCH 18/22] Translate webgl-attributes.md into Chinese --- webgl/lessons/zh_cn/webgl-attributes.md | 269 ++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-attributes.md diff --git a/webgl/lessons/zh_cn/webgl-attributes.md b/webgl/lessons/zh_cn/webgl-attributes.md new file mode 100644 index 000000000..6b4df4041 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-attributes.md @@ -0,0 +1,269 @@ +Title: WebGL2 属性(Attributes) +Description: WebGL 中的 attributes 是什么? +TOC: 属性(Attributes) + +本文旨在帮助你建立对 WebGL 中属性状态是如何设置的一个直观理解。 +另有[关于纹理单元的类似文章](webgl-texture-units.html)以及[framebuffer 的文章](webgl-framebuffers.html)。 + +前置知识建议阅读:[WebGL 是如何工作的](webgl-how-it-works.html) 和 +[WebGL 着色器和 GLSL](https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html)。 + +## Attributes(属性) + +在 WebGL 中,attributes 是传入顶点着色器的输入,数据来自 buffer。 +每当调用 `gl.drawArrays` 或 `gl.drawElements` 时,WebGL 会执行用户提供的顶点着色器 N 次。 +每次迭代,attributes 定义了如何从绑定到它们的 buffer 中提取数据, +并将其传递给顶点着色器中的属性变量。 + +如果用 JavaScript 来模拟实现,它们可能像这样: + +```js +// pseudo code +const gl = { + arrayBuffer: null, + vertexArray: { + attributes: [ + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, divisor: 0 }, + ], + elementArrayBuffer: null, + }, +} +``` + +如上所示,总共有 16 个 attributes。 + +当你调用 `gl.enableVertexAttribArray(location)` 或 `gl.disableVertexAttribArray`,可以将其理解为如下操作: + +```js +// pseudo code +gl.enableVertexAttribArray = function(location) { + const attrib = gl.vertexArray.attributes[location]; + attrib.enable = true; +}; + +gl.disableVertexAttribArray = function(location) { + const attrib = gl.vertexArray.attributes[location]; + attrib.enable = false; +}; +``` + +换句话说,location 就是 attribute 的索引。 + +类似地,`gl.vertexAttribPointer` 用来设置 attribute 的几乎所有其他属性。 +实现可能如下所示: + +```js +// pseudo code +gl.vertexAttribPointer = function(location, size, type, normalize, stride, offset) { + const attrib = gl.vertexArray.attributes[location]; + attrib.size = size; + attrib.type = type; + attrib.normalize = normalize; + attrib.stride = stride ? stride : sizeof(type) * size; + attrib.offset = offset; + attrib.buffer = gl.arrayBuffer; // !!!! <----- +}; +``` + +注意,调用 `gl.vertexAttribPointer` 时,`attrib.buffer` 会设置为当前的 `gl.arrayBuffer`。 +`gl.arrayBuffer` 如上述伪代码所示,通过调用 `gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer)` 设置。 + + +```js +// pseudo code +gl.bindBuffer = function(target, buffer) { + switch (target) { + case ARRAY_BUFFER: + gl.arrayBuffer = buffer; + break; + case ELEMENT_ARRAY_BUFFER; + gl.vertexArray.elementArrayBuffer = buffer; + break; + ... +}; +``` + +接下来是顶点着色器。在顶点着色器中你声明属性,例如: + +```glsl +#version 300 es +in vec4 position; +in vec2 texcoord; +in vec3 normal; + +... + +void main() { + ... +} +``` + +当你使用 `gl.linkProgram(someProgram)` 将顶点着色器和片段着色器链接时, +WebGL(驱动/GPU/浏览器)会自行决定每个属性使用哪个索引(location)。 +除非你手动分配位置(见下文),否则你无法预知它们会选哪个索引。 +因此你需要通过 `gl.getAttribLocation` 查询它们: + +```js +const positionLoc = gl.getAttribLocation(program, 'position'); +const texcoordLoc = gl.getAttribLocation(program, 'texcoord'); +const normalLoc = gl.getAttribLocation(program, 'normal'); +``` + +假设 `positionLoc` = `5`,意味着在执行顶点着色器时(即调用 `gl.drawArrays` 或 `gl.drawElements`), +WebGL 期待你已经为 attribute 5 设置好了正确的 `type`、`size`、`offset`、`stride`、`buffer` 等。 + +注意:在调用 `gl.linkProgram` *之前*,你可以使用`gl.bindAttribLocation(program, location, nameOfAttribute) `指定位置,例如: + +```js +// Tell `gl.linkProgram` to assign `position` to use attribute #7 +gl.bindAttribLocation(program, 7, 'position'); +``` + +如果使用的是GLSL ES 3.00着色器,您也可以直接在着色器中指定要使用的location位置,例如: + +```glsl +layout(location = 0) in vec4 position; +layout(location = 1) in vec2 texcoord; +layout(location = 2) in vec3 normal; + +... +``` + +使用 `bindAttribLocation` 看起来更符合 [D.R.Y. 原则](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), +不过你可以根据个人偏好选择不同的方式。 + +## 完整的属性状态 + +上面的描述中省略了一点:每个 attribute 实际上都有一个默认值。 +这在实际应用中较少使用,所以之前没有提及。 + +```js +attributeValues: [ + [0, 0, 0, 1], + [0, 0, 0, 1], + ... +], +vertexArray: { + attributes: [ + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, +  divisor: 0, }, + { enable: ?, type: ?, size: ?, normalize: ?, stride: ?, offset: ?, buffer: ?, +  divisor: 0, }, + ... +``` + +你可以通过一系列 `gl.vertexAttribXXX` 函数设置每个 attribute 的值。 +当 `enable` 为 `false` 时,会使用这些值;当 `enable` 为 `true`,则会从分配的 `缓冲区buffer` 中读取数据。 + + +## 顶点数组对象(VAO) + +```js +const vao = gl.createVertexArray(); +``` + +这会创建一个如上伪代码中 `gl.vertexArray` 所示的对象。 +调用 `gl.bindVertexArray(vao)` 将你创建的 VAO 设为当前 VAO: + +```js +// pseudo code +gl.bindVertexArray = function(vao) { + gl.vertexArray = vao ? vao : defaultVAO; +}; +``` + +这样你就可以在当前 VAO 中设置所有 `attributes` 和 `ELEMENT_ARRAY_BUFFER`。 +当你要绘制某个图形时,只需调用一次 `gl.bindVertexArray` 即可设置所有属性。 +否则你可能需要为每个属性分别调用 `gl.bindBuffer`、`gl.vertexAttribPointer`、`gl.enableVertexAttribArray`。 + +由此可见,使用 VAO 是很有价值的。 +不过,要正确使用 VAO 需要更好的组织结构。 + +举个例子,假设你想用 `gl.TRIANGLES` 和一个着色器绘制一个立方体, +再用 `gl.LINES` 和另一个着色器重新绘制它。 + +假设用三角形绘制时要做光照处理,因此顶点着色器声明了这些属性: + +```glsl +#version 300 es +// lighting-shader +// 用于绘制三角形的着色器 + +in vec4 a_position; +in vec3 a_normal; +``` + +然后使用这些位置和法线向我在[第一篇光照文章](webgl-3d-lighting-directional.html)中做的那样。 + +对于不需要光照的线条,您需要使用纯色,可以参照本教程系列[第一页](webgl-fundamentals.html)中的基础着色器实现方式。 +声明一个uniform的颜色变量。 这意味着在顶点着色器中只需处理位置数据即可 + +```glsl +#version 300 es +// solid-shader +// shader for cube with lines + +in vec4 a_position; +``` + +我们并不知道 WebGL 为每个 shader 分配的 attribute 位置是多少。 +假设 lighting-shader 的分配结果是: + +``` +a_position location = 1 +a_normal location = 0 +``` + +solid-shader只有一个attribute属性。 + +``` +a_position location = 0 +``` + +显然,在切换着色器时需要重新设置属性。 +一个着色器期望 `a_position` 的数据出现在attribute 0,另一个着色器期望它出现在attribute 1。 + +重新设置属性是一件麻烦事。更糟的是,使用 VAO 的初衷就是避免这些重复操作。 +为了解决这个问题,我们需要在链接程序之前使用 bindAttribLocation 显式指定位置: + +重新设置属性是一件麻烦事。更糟的是,使用 VAO 的初衷就是避免这些重复操作。 +为了解决这个问题,我们需要在链接程序之前使用 `bindAttribLocation` 显式指定位置。 + +我们告诉 WebGL: + +```js +gl.bindAttribLocation(solidProgram, 0, 'a_position'); +gl.bindAttribLocation(lightingProgram, 0, 'a_position'); +gl.bindAttribLocation(lightingProgram, 1, 'a_normal'); +``` + +务必在**调用 `gl.linkProgram` 之前**执行以上操作。 +这样 WebGL 在链接着色器时就会使用我们指定的位置。 +现在这两个着色器就可以使用相同的 `VAO`。 + +## 最大属性数量 + +WebGL2 要求至少支持 16 个 attribute,但具体设备 / 浏览器 / 驱动可能支持更多。 +你可以通过下面的方式获取实际支持数量: + +```js +const maxAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); +``` + +如果你打算使用超过 16 个 attributes,建议检查支持数量, +并在设备不足时提示用户,或者降级使用更简单的着色器。 From 89171aa001ede6927aec29af26252f8435bd1dc8 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Wed, 25 Jun 2025 00:30:44 +0800 Subject: [PATCH 19/22] Translate webgl-framebuffers.md into Chinese --- webgl/lessons/zh_cn/webgl-framebuffers.md | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-framebuffers.md diff --git a/webgl/lessons/zh_cn/webgl-framebuffers.md b/webgl/lessons/zh_cn/webgl-framebuffers.md new file mode 100644 index 000000000..bd486a104 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-framebuffers.md @@ -0,0 +1,133 @@ +Title: WebGL2 帧缓冲区(Framebuffers) +Description: WebGL 中的 帧缓冲区(Framebuffers) 是什么? +TOC: 帧缓冲区(Framebuffers) + +本文旨在帮助你建立对 WebGL 中 framebuffer 的一个直观理解。 +framebuffer 之所以重要,是因为它们允许你[渲染到纹理](webgl-render-to-texture.html)。 + +一个 `Framebuffer` 其实就是一个**附件的集合(attachments)**。就是这样! +它的作用是让你可以将内容渲染到`纹理`或`渲染缓冲区`中。 + +你可以将一个 Framebuffer 对象想象成这样: + +``` +class Framebuffer { + constructor() { + this.attachments = new Map(); // attachments by attachment point + this.drawBuffers = [gl.BACK, gl.NONE, gl.NONE, gl.NONE, ...]; + this.readBuffer = gl.BACK, + } +} +``` + +而 `WebGL2RenderingContext`(也就是 `gl` 对象)可以理解为如下结构: + +```js +// pseudo code +gl = { + drawFramebufferBinding: defaultFramebufferForCanvas, + readFramebufferBinding: defaultFramebufferForCanvas, +} +``` + +这里有两个绑定点(binding point),设置方式如下: + +```js +gl.bindFramebuffer(target, framebuffer) { + framebuffer = framebuffer || defaultFramebufferForCanvas; // if null use canvas + switch (target) { + case: gl.DRAW_FRAMEBUFFER: + this.drawFramebufferBinding = framebuffer; + break; + case: gl.READ_FRAMEBUFFER: + this.readFramebufferBinding = framebuffer; + break; + case: gl.FRAMEBUFFER: + this.drawFramebufferBinding = framebuffer; + this.readFramebufferBinding = framebuffer; + break; + default: + ... error ... + } +} +``` + +`DRAW_FRAMEBUFFER`:用于向 `framebuffer` 绘制内容,如通过 `gl.clear`、`gl.draw???` 或 `gl.blitFramebuffer`。 +`READ_FRAMEBUFFER`:用于从 `framebuffer` 中读取内容,如通过 `gl.readPixels` 或 `gl.blitFramebuffer`。 + +你可以通过三个函数向 framebuffer 添加附件,`framebufferTexture2D`、 +`framebufferRenderbuffer` 和 `framebufferTextureLayer`。 + +它们的内部逻辑可以想象成如下实现: + +```js +// pseudo code +gl._getFramebufferByTarget(target) { + switch (target) { + case gl.FRAMEBUFFER: + case gl.DRAW_FRAMEBUFFER: + return this.drawFramebufferBinding; + case gl.READ_FRAMEBUFFER: + return this.readFramebufferBinding; + } +} +gl.framebufferTexture2D(target, attachmentPoint, texTarget, texture, mipLevel) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + texture, texTarget, mipLevel, + }); +} +gl.framebufferTextureLayer(target, attachmentPoint, texture, mipLevel, layer) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + texture, texTarget, mipLevel, layer + }); +} +gl.framebufferRenderbuffer(target, attachmentPoint, renderbufferTarget, renderbuffer) { + const framebuffer = this._getFramebufferByTarget(target); + framebuffer.attachments.set(attachmentPoint, { + renderbufferTarget, renderbuffer + }); +} +``` + +你可以使用 `gl.drawBuffers` 设置 `framebuffer` 的绘制目标数组,其内部实现如下所示: + + +```js +// pseudo code +gl.drawBuffers(drawBuffers) { + const framebuffer = this._getFramebufferByTarget(gl.DRAW_FRAMEBUFFER); + for (let i = 0; i < maxDrawBuffers; ++i) { + framebuffer.drawBuffers[i] = i < drawBuffers.length + ? drawBuffers[i] + : gl.NONE + } +} +``` + +`drawBuffers` 数组决定了哪些附件会被渲染。 + +合法值包括: + +* `gl.NONE`:不渲染到这个附件 +* `gl.COLOR_ATTACHMENTx`:其中 `x` 和附件索引一样 +* `gl.BACK`:仅在当前 `framebuffer` 为 `null` 时有效,表示渲染到默认 canvas 的 `backbuffer` + +你还可以使用 `gl.readBuffer` 设置读缓冲: + +```js +// pseudo code +gl.readBuffer(readBuffer) { + const framebuffer = this._getFramebufferByTarget(gl.READ_FRAMEBUFFER); + framebuffer.readBuffer = readBuffer; +} +``` + +readBuffer 决定在调用 `gl.readPixels` 时会从哪个附件读取。 + +重点总结:framebuffer 本质上只是一个附件的简单集合。 +真正复杂的是这些附件之间的限制与兼容性。 +例如:浮点纹理附件默认不能被渲染,除非通过扩展如 `EXT_color_buffer_float` 开启支持。 +此外,如果 framebuffer 包含多个附件,它们必须具有相同的尺寸。 + From 6b43b6e3071fcaf746c6cb914e4acdc16cf87892 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Wed, 25 Jun 2025 00:43:35 +0800 Subject: [PATCH 20/22] Translate webgl-texture-units.md into Chinese --- webgl/lessons/zh_cn/webgl-texture-units.md | 131 +++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-texture-units.md diff --git a/webgl/lessons/zh_cn/webgl-texture-units.md b/webgl/lessons/zh_cn/webgl-texture-units.md new file mode 100644 index 000000000..667cdee71 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-texture-units.md @@ -0,0 +1,131 @@ +Title: WebGL2 纹理单元(Texture Units) +Description: WebGL 中的纹理单元是什么? +TOC: 纹理单元(Texture Units) + +本文旨在帮助你直观理解 WebGL 中纹理单元是如何设置的。 +关于[属性(attributes)](webgl-attributes.html)的使用,另有一篇专门文章进行了详细讲解。 + +在阅读本文之前,你可能需要先阅读: +- [WebGL 是如何工作的](webgl-how-it-works.html) +- [WebGL 着色器与 GLSL](webgl-shaders-and-glsl.html) +- [WebGL 纹理](webgl-3d-textures.html) + +## 纹理单元(Texture Units) + +在 WebGL 中有纹理。纹理是可以传入着色器的 2D 数据数组。 +在着色器中你会这样声明一个 *uniform 采样器*: + +```glsl +uniform sampler2D someTexture; +``` + +但着色器如何知道 `someTexture` 对应的是哪一张纹理呢? + +这就是纹理单元(Texture Unit)登场的地方了。 +纹理单元是一个**全局数组**,保存着对纹理的引用。 +你可以想象,如果 WebGL 是用 JavaScript 编写的,它可能拥有如下的全局状态: + +```js +const gl = { + activeTextureUnit: 0, + textureUnits: [ + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + { TEXTURE_2D: null, TEXTURE_CUBE_MAP: null, TEXTURE_3D: null, TEXTURE_2D_ARRAY: null, }, + ]; +} +``` + +如上所示,`textureUnits` 是一个数组。你可以把纹理绑定到这个纹理单元数组中某个位置的 *绑定点(bind point)* 上。 +例如,将 `ourTexture` 绑定到纹理单元 5 上: + +```js +// at init time +const ourTexture = gl.createTexture(); +// insert code it init texture here. + +... + +// at render time +const indexOfTextureUnit = 5; +gl.activeTexture(gl.TEXTURE0 + indexOfTextureUnit); +gl.bindTexture(gl.TEXTURE_2D, ourTexture); +``` + +然后你需要告诉着色器这个纹理变量(uniform)使用的是哪一个纹理单元: + +```js +gl.uniform1i(someTextureUniformLocation, indexOfTextureUnit); +``` + +如果 `activeTexture` 和 `bindTexture` 函数是用 JavaScript 实现的,可能看起来像这样: + +```js +// PSEUDO CODE!!! +gl.activeTexture = function(unit) { + gl.activeTextureUnit = unit - gl.TEXTURE0; // convert to 0 based index +}; + +gl.bindTexture = function(target, texture) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + textureUnit[target] = texture; +}: +``` + +你甚至可以想象其它纹理相关函数是如何运作的。这些函数都接受一个 `target` 参数, +比如 `gl.texImage2D(target, ...)` 或 `gl.texParameteri(target)`,它们的实现可能像这样: + +```js +// PSEUDO CODE!!! +gl.texImage2D = function(target, level, internalFormat, width, height, border, format, type, data) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + const texture = textureUnit[target]; + texture.mips[level] = convertDataToInternalFormat(internalFormat, width, height, format, type, data); +} + +gl.texParameteri = function(target, pname, value) { + const textureUnit = gl.textureUnits[gl.activeTextureUnit]; + const texture = textureUnit[target]; + texture[pname] = value; +} +``` + +从以上示例可以清楚地看到,`gl.activeTexture` 会设置 WebGL 内部的一个全局变量, +表示当前使用哪个纹理单元(在纹理单元数组中的索引)。 + +从此之后,所有其它纹理函数中传入的 `target` 参数实际上就是当前激活的纹理单元中要操作的绑定点。 + +## 最大纹理单元数(Maximum Texture Units) + +WebGL 要求实现至少支持 32 个纹理单元。你可以通过如下方式查询实际支持的数量: + +```js +const maxTextureUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); +``` + +需要注意的是,顶点着色器 和 片段着色器 对可用纹理单元数可能有不同限制。 +你可以分别用如下方式查询: + + +```js +const maxVertexShaderTextureUnits = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); +const maxFragmentShaderTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS); +``` +它们都至少需要支持 16 个纹理单元。 + +例如: + +```js +maxTextureUnits = 32 +maxVertexShaderTextureUnits = 16 +maxFragmentShaderTextureUnits = 32 +``` + +这意味着,如果你在顶点着色器中使用了 2 个纹理单元,那么片段着色器最多只能再使用 30 个纹理单元, +因为两个着色器合起来总共不能超过 MAX_COMBINED_TEXTURE_IMAGE_UNITS 的限制(即 32)。 From 726eb8f78c82bb065b9fc50043698071e12b7777 Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Wed, 25 Jun 2025 00:51:58 +0800 Subject: [PATCH 21/22] Translate webgl-readpixels.md into Chinese --- webgl/lessons/zh_cn/webgl-readpixels.md | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-readpixels.md diff --git a/webgl/lessons/zh_cn/webgl-readpixels.md b/webgl/lessons/zh_cn/webgl-readpixels.md new file mode 100644 index 000000000..962d9610a --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-readpixels.md @@ -0,0 +1,33 @@ +Title: WebGL2 readPixels +Description: readPixels 的详细信息 +TOC: readPixels + + +在 WebGL 中,调用 `readPixels` 时需要传入一个 format/type(格式/类型)对。对于一个给定的纹理内部格式(附加到帧缓冲上的纹理),只有两种 format/type 组合是合法的。 + +来自规范的说明: + +> 对于标准化的定点渲染表面,接受格式是 `RGBA` 和类型 `UNSIGNED_BYTE`的组合。 +> 对于有符号整数渲染表面,接受的格式是 `RGBA_INTEGER` 和类型 `INT`的组合。 +> 对于无符号整数渲染表面,接受的格式是 `RGBA_INTEGER` 和类型 `UNSIGNED_INT`的组合。 + +第二种组合是实现定义的 +这很可能意味着,如果你想让你的代码具有可移植性,就不应该在 WebGL 中使用它。 +你可以通过查询以下内容来查看当前实现所支持的 format/type 组合: + +```js +// 假设已经绑定了一个附有要读取纹理的 framebuffer +const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); +const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); +``` + +还需要注意,哪些纹理格式是 可渲染 的(即你可以将它们附加到 framebuffer 并对其进行渲染) 在某种程度上也是实现定义的。 +WebGL2 列出了[许多格式](webgl-data-textures.html),但其中有些是可选的(例如 `LUMINANCE`), +而有些则默认不可渲染,除非通过扩展启用(例如 `RGBA32F`)。 + +**下表是实时的**。你可能会注意到,它在不同的设备、操作系统、GPU 甚至浏览器上可能会给出不同的结果。 +我在自己的机器上发现 Chrome 和 Firefox 在某些实现定义值上的表现是不同的。 + +
+ + From fbf07968c9ac8727526e01e404a41ef1b1bec10d Mon Sep 17 00:00:00 2001 From: colin3dmax Date: Wed, 25 Jun 2025 10:07:41 +0800 Subject: [PATCH 22/22] webgl-references.md --- webgl/lessons/zh_cn/webgl-references.md | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 webgl/lessons/zh_cn/webgl-references.md diff --git a/webgl/lessons/zh_cn/webgl-references.md b/webgl/lessons/zh_cn/webgl-references.md new file mode 100644 index 000000000..0cf7fb375 --- /dev/null +++ b/webgl/lessons/zh_cn/webgl-references.md @@ -0,0 +1,57 @@ +Title: 参考资料 +Description: 其他参考资料 +TOC: 参考资料 + +以下是可能会对你有用的一些资源链接: + +## 教程和课程 + +* [3d game shaders for beginners](https://lettier.github.io/3d-game-shaders-for-beginners/) + 提供了许多图形技术的精彩讲解。虽然是基于 OpenGL,但讲解图文并茂,因此应当可以较容易地将其适配为 WebGL。 + +* [Learn OpenGL](https://learnopengl.com/): 现代 OpenGL 教程 + + 这些内容可能有用,也可能不适用。尽管 API 相似,但 OpenGL 并不是 WebGL。首先,OpenGL 是一个基于 C 的库;其次,OpenGL 拥有比 WebGL 更多的特性,而且着色器语言也有许多差异。尽管如此,教程中展示的很多概念和技术在 WebGL 中依然同样适用。 + +## 工具 / 浏览器扩展 + +* [Spector](https://chrome.google.com/webstore/detail/spectorjs/denbgaamihkadbghdceggmchnflmhpmk?hl=en): 一个可以显示所有 WebGL 调用的浏览器扩展 + +* [Shader Editor](https://chrome.google.com/webstore/detail/shader-editor/ggeaidddejpbakgafapihjbgdlbbbpob?hl=en): 一个允许你在实时网页中查看和编辑着色器的浏览器扩展 + +* [WebGL Insight](https://chrome.google.com/webstore/detail/webgl-insight/djdcbmfacaaocoomokenoalbomllhnko?hl=en): 一个用于查看 WebGL 使用情况的扩展 + +* [webgl-helpers](https://greggman.github.io/webgl-helpers/): 用于辅助 WebGL 编程的脚本集合 + +## 库 + +* [twgl](https://twgljs.org): 一个帮助减少 WebGL 冗长代码的库 + +* [three.js](https://threejs.org): 最流行的 JavaScript 3D 图形库 + +* [PlayCanvas](https://playcanvas.com/): 一个带有游戏编辑器的 WebGL 游戏引擎 + +* [regl](https://regl.party/): 一个无状态函数式的 WebGL 库 + +## 规范 + +* [WebGL2](https://www.khronos.org/registry/webgl/specs/latest/2.0/): WebGL2 的规范 + +* [OpenGL ES 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/es_spec_3.0.pdf): WebGL2 所基于的 OpenGL ES 3.0 规范 + +* [GLSL ES 3.0](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf): WebGL2 使用的着色器语言规范 + +## 趣味网站 + +* [Shadertoy.com](https://shadertoy.com): 在极限条件下创作的令人惊叹的片段着色器 + + ⚠️ 注意:shadertoy.com 上的着色器通常并不是生产级 WebGL 应用中使用的那种。但其中仍蕴含许多可借鉴的技术与灵感。 + +* [glslsandbox.com](https://glslsandbox.com): 最早的片段着色器在线实验平台 + +* [vertexshaerart.com](https://vertexshaderart.com): glslsandbox 的顶点着色器版本 + +--- + +如果你还知道其他一些不错的参考资源,欢迎 +[提交一个 issue](https://github.com/gfxfundamentals/webgl-fundamentals/issues) 来补充。