Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
out
package-lock.json
webgl2fundamentals.check.json
.idea
177 changes: 177 additions & 0 deletions webgl/lessons/zh_cn/webgl-2d-vs-3d-library.md
Original file line number Diff line number Diff line change
@@ -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 来显示这个代码。

<pre class="prettyprint showlinemods">{{#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}}</pre>

它显示如下。

{{{example url="resources/three-js-cube-with-lights.html" }}}

以下是在 OpenGL(非 ES 版本)中显示一个带有两个光源的立方体的类似代码。

<pre class="prettyprint showlinemods">{{#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}}</pre>

请注意,在这两个示例中我们几乎不需要任何 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 工作原理背后的许多神秘面纱。
97 changes: 97 additions & 0 deletions webgl/lessons/zh_cn/webgl-and-alpha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
Title: WebGL2 和 Alpha
Description: WebGL 中的 Alpha 与 OpenGL 中的 Alpha 有何不同
TOC: WebGL2 和 Alpha

我注意到一些 OpenGL 开发者在使用 WebGL 时遇到了关于后缓冲区(即画布)中 alpha 的问题,所以我觉得有必要讲一下 WebGL 和 OpenGL 在 alpha 处理上的一些差异。

OpenGL 和 WebGL 最大的区别是,OpenGL 渲染到一个不会被任何东西合成的后缓冲区,或者说操作系统的窗口管理器实际上不会对它进行合成,所以无论 alpha 怎么设置都无所谓。

而 WebGL 是由浏览器与网页内容合成的,默认使用预乘 alpha(premultiplied alpha),这和带透明通道的 PNG `<img>` 标签以及 2D canvas 标签的行为相同。

WebGL 有几种方式可以使其行为更像 OpenGL。

### #1) 告诉 WebGL 你希望使用非预乘 alpha 合成

gl = canvas.getContext("webgl2", {
premultipliedAlpha: false // Ask for non-premultiplied alpha
});

默认值是 true。

当然,结果仍会与画布下方的背景颜色合成(画布背景色、画布容器背景色、页面背景色,或者当画布 z-index 大于 0 时背后的内容),
换句话说,是网页该区域 CSS 定义的颜色。

判断是否存在 alpha 问题的一个好方法是将画布背景设置为鲜艳颜色,例如红色。你能立刻看到效果:

<canvas style="background: red;"><canvas>

你也可以设置成黑色,黑色会掩盖任何 alpha 问题。

### #2) 告诉 WebGL 你不需要后缓冲区的 alpha

gl = canvas.getContext("webgl", { alpha: false }};

这样它的行为更像 OpenGL,因为后缓冲区只会有 RGB。 这可能是最好的选择,因为优秀的浏览器能检测到你不需要 alpha, 从而优化 WebGL 的合成方式。 但这也意味着后缓冲区实际上没有 alpha,如果你确实依赖它可能会不适用。 我所知道的应用中很少使用后缓冲区 alpha。 从某种角度看,我认为这应该是默认行为。

### #3) 在渲染结束时清除 alpha 通道

..
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);

清除操作通常非常快,因为大多数硬件对此有特殊优化。 我在许多早期 WebGL 示例中都这么做过。 如果聪明的话,应该用上面方法 #2。 也许我发完这条就去改。 大多数 WebGL 库也应该默认采用这种方法。 真正使用 alpha 进行合成效果的开发者可以主动开启。 其余人则能获得最佳性能和最少意外。


### #4) 清除 Alpha 通道一次,之后不再渲染到该通道

// 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);

当然如果你在渲染自定义的帧缓冲区时, 可能需要重新开启 alpha 渲染, 然后切回渲染画布时再关闭。

### #5) 处理带 alpha 的图片

默认情况下,加载带 alpha 的图片到 WebGL,WebGL 会提供文件中的原始颜色值, 且颜色值未做预乘。 这一般符合我对 OpenGL 程序的使用习惯, 因为未预乘是无损的,而预乘会有损失。

1, 0.5, 0.5, 0 // RGBA

这是一个可能的未预乘值, 但预乘情况下不可能出现这种值, 因为 alpha = 0,r、g、b 必须都是 0。

加载图像时,如果需要,可以让 WebGL 对 Alpha 进行预乘。
你可以通过如下方式将 UNPACK_PREMULTIPLY_ALPHA_WEBGL 设置为 true 来实现。

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

默认情况下是不进行预乘的。

请注意,大多数(如果不是全部的话)Canvas 2D 实现都使用预乘 alpha。 这意味着当你将它们传输到 WebGL 并且 UNPACK_PREMULTIPLY_ALPHA_WEBGL 设置为 false 时,WebGL 会将它们转换回非预乘状态。

### #6) 使用与预乘 alpha 兼容的混合方程

我写过或参与过的几乎所有 OpenGL 应用, 默认都是这样设置混合函数。

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

这适用于非预乘 alpha 的纹理。

如果你确实想使用预乘 alpha 纹理,那么你可能需要使用以下设置

gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

这些是我所知道的方法。如果你了解更多,请在下方分享。



98 changes: 98 additions & 0 deletions webgl/lessons/zh_cn/webgl-animation.md
Original file line number Diff line number Diff line change
@@ -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)。

<div class="webgl_bottombar">
<h3>请勿使用 setInterval 或 setTimeout !</h3>
<p>若您曾用JavaScript实现动画,可能习惯使用<code>setInterval</code>或<code>setTimeout</code>调用绘制函数。</p>
<p> 这类方式存在双重缺陷:首先,<code>setInterval</code>和<code>setTimeout</code>与浏览器渲染流程无关,它们无法与屏幕刷新同步,最终将导致与用户设备不同步。
若您假设60FPS使用它们,而实际设备以其他帧率运行,就会出现同步问题。</p>
<p> 其次,浏览器无法识别您调用<code>setInterval</code>或<code>setTimeout</code>的意图。即使页面处于后台标签页(不可见状态),浏览器仍会执行这些代码。虽然这对每分钟检查邮件/Tweet的任务无碍,但若用于WebGL渲染上千个对象,将导致用户设备资源被不可见页面的渲染任务无意义消耗。</p>
<p>
<code>requestAnimationFrame</code>能完美解决这些问题。
它在屏幕刷新最佳时机触发回调,且仅在标签页可见时执行。
</p>
</div>



Loading
Loading