经过前面的内容,我们几乎可以从任何视角渲染任何场景,但生成的图像看起来很简单,以线框的形式渲染物体,给人印象是正在查看一组物体的蓝图,而不是物体本身
想让物体看起来像实体时,首先想到的是使用前面开发的DrawFilledTriangle函数,使用随机颜色绘制物体的每个三角形。
立方体的一部分背面三角形被绘制在正面三角形的前面,这是因为盲目地按照随机顺序在画布上绘制2D三角形,或者更准确地说,按照它们碰巧在模型的三角形列表Triangles中定义的顺序绘制,而没有考虑它们之间的空间关系。
第一个解决方案被称为画家算法 painter’s algorithm,现实生活中画家首先绘制背景,然后用前景物体覆盖背景的一部分,我们可以通过将场景中的所有三角形从后向前绘制来达到相同的效果。我们要应用模型变换和相机变换,并根据三角形与相机的距离对三角形进行排序。
上面提到的”没有单独的正确顺序“的问题,因为现在我们正在寻找物体和相机的特定相对位置的正确顺序。有点不切实际 人类已知的最有效的排序算法时间复杂度为nlogn,意味着三角形数量增加一倍,运行时间将会增加一倍以上。这不切实际。
其次,它要求我们一次性知道整个三角形列表,这需要大量内存,并且阻止了我们使用类似数据流的方法进行渲染。我们希望渲染器像一个管线pipeline,模型三角形从一段进入,像素从另一端出来,但是这个算法会在每个三角形被转换和排序后才开始绘制像素。
即使我们愿意接受这些限制,也有一些情况下,三角形的正确顺序根本不存在,如下图永远无法用正确的方法对这些三角形进行排序。
我们无法在三角形级别解决排序问题,所以在尝试在像素级别解决它。
对于画布上的每个像素,我们希望用 正确 的颜色绘制它,其中正确的颜色是离相机最近的物体的颜色。
在渲染过程中的任何时候,画布上的每个像素都代表场景中的一个点,在绘制任何东西之前它都代表一个无限远的点。假设对于画布上的每个像素,记录它当前所代表的点的z坐标,当需要决定是否用物体颜色绘制一个像素时,只需要在我们将要绘制的点的z坐标小于已经存在的点的z坐标时才这样做。
在实现方面,我们需要一个缓冲区来存储画布上每个像素的z坐标,我们称这个缓冲区为深度缓冲(depth buffer),它与画布具有相同的尺寸,其元素表示深度值的实数。
z值从何而来?这些应该是点变换之后但是在透视投影之前的z值,然而,这只给了我们顶点的z值,我们需要每个三角形的每个元素的z值。
可以利用前面的学过的属性映射算法。使用z作为属性,并在三角形的表面上对它进行插值计算,就像之前对颜色强度值所做的那样。
取z0,z1,z2的值,计算z01、z02、z012,将它们组合起来得到z_left和z_right,对于每个水平段,计算z_segment。
= z_segment[x - x1]
z if (z < depth_buffer[x][y])
{.PutPixel(x, y, color)
canvas= z
depth_buffer[x][y] }
为了使其正常工作,depth_buffer中的每个条目都应初始化为正无穷。
顶点的z值是正确的(毕竟它们来自模型数据),但在大多数情况下,其余像素的z值的线性插值是不正确的。
A(-1,0,2)到B(1,0,10)的线段的简单情况,其中点M(0,0,6)
Mz = (Az+Bz)/2 =6
计算d=1时这些点的投影,应用透视投影方程得到
A’ B’ 时视口上的水平线段,我们直到Az和Bz的值,如果用线性插值计算Mz的值会发生什么。
Mz - Az Bz - Az
------- = ---------
Mx' - A'x B'x - A'x
将M’z表达式解出来
将值代入Mz的算出来为8.666,但我们直到它实际上是6。 错误隐藏在我们使用线性插值时所做的隐含假设中,我们一开始就认为我们要插值的函数是线性的。
假设是线性关系则可以写为
z = Ax' + By' + C
线性关系则可以推出
对于包含我们正在研究的线段的平面,它们的方程如下
深度缓冲可产生我们所需的结果,但是我们能让事情变得更快吗?
回到立方体,即使每个像素最终都具有正确的颜色,但其中许多像素会被多次绘制。例如,立方体的背面先于正面渲染,则许多像素将被绘制两次,这种性能开销可能是很大的,到目前为止我们已经为每个像素计算了1/z,但后面还要加更多属性,如光照等。
在我们进行这些计算之前,我们可以更早地丢弃像素吗,事实证明,我们甚至可以在开始渲染之前丢弃整个三角形。
假设每个三角形都有两个不同的面,同时看到三角形的两个面是不可能的。为了区分这两个面,我们将在每个三角形上贴一个假想的箭头,垂直于其表面。然后我们观察由带有箭头的三角形所组成的立方体,确保每个箭头都指向外面。
如果视线向量和这个箭头(实际上是三角形的法向量)形成的角度小于90°,则三角形是正面的;否则,它是背面的。
我们需要对我们的3D模型施加一个限制条件:它们必须是封闭的(closed)。
封闭物体有一个有趣的特性,即无论模型或相机的方位如何,物体正向表面的集合完全覆盖背向表面的集合。这意味着我们根本不需要绘制背向的表面,可以节省宝贵的计算时间。
由于我们可以丢弃(剔除)所有的背向表面,因此该算法称为背面剔除(back face culling)算法。
CullBackFaces(object, camera)
{for T in object.triangles
{if T is back-faceing
{from object.triangles
remove T
}
} }
利用三角形顶点到相机的向量和三角形法向量夹角进行分类。
三角形的法向量从哪里来
确定法向量的方向有一个非常简单的规则:如果相机看三角形ABC的顶点是顺时针方向的,那么前文计算的法向量将指向相机,也就是说相机正在看三角形的正面。
我们只需要在手动设计3D模型时记住,这个规则,并在查看其正面时按顺时针依次列出每个三角形的顶点,以便当我们以这种方式计算法线时它们指向外部。
<!--
!!html_title Hidden surface removal demo - Computer Graphics from scratch
-->
# Hidden surface removal demo
This demo implements [Depth Buffering](../12-hidden-surface-removal.html#depth-buffering) and
[Back Face Culling](../12-hidden-surface-removal.html#back-face-culling). You can turn them on and off
to see the effect they have on the output.
With Depth Buffering and Back Face Culling disabled, the output depends on the order of the triangles
within the model. To show this, you can shuffle the triangles; and to make the output clearer, you can
choose to draw the outlines of the individual triangles.
<div class="centered">
<canvas id="canvas" width=600 height=600 style="border: 1px grey solid"></canvas>
<table class="cgfs-demo-controls">
<tr>
<td><b>Depth buffering &<br>Backface culling</b></td>
<td class="text-left">
<input type="radio" id="depth-off" name="depth-enabled" onClick="SetDepthEnabled(false);">
<label for="depth-off">Disabled</label><br>
<input type="radio" id="depth-on" name="depth-enabled" onClick="SetDepthEnabled(true);" checked>
<label for="depth-on">Enabled</label>
</td>
</tr>
<tr>
<td><b>Triangle outlines</b></td>
<td class="text-left">
<input type="radio" id="outlines-off" name="outlines-enabled" onClick="SetOutlinesEnabled(false);" checked>
<label for="outlines-off">Disabled</label><br>
<input type="radio" id="outlines-on" name="outlines-enabled" onClick="SetOutlinesEnabled(true);">
<label for="outlines-on">Enabled</label><br>
</td>
</tr>
<tr>
<td colspan=2 class="text-center"><button onClick="ShuffleCubeTriangles();">Shuffle triangles</button>
</tr>
</table>
</div>
<script>
"use strict";
// ======================================================================
// Low-level canvas access.
// ======================================================================
let canvas = document.getElementById("canvas");
let canvas_context = canvas.getContext("2d");
let canvas_buffer = canvas_context.getImageData(0, 0, canvas.width, canvas.height);
// A color.
function Color(r, g, b) {
return {
, g, b,
rmul: function(n) { return new Color(this.r*n, this.g*n, this.b*n); },
;
}
}
// The PutPixel() function.
function PutPixel(x, y, color) {
= canvas.width/2 + (x | 0);
x = canvas.height/2 - (y | 0) - 1;
y
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return;
}
let offset = 4*(x + canvas_buffer.width*y);
.data[offset++] = color.r;
canvas_buffer.data[offset++] = color.g;
canvas_buffer.data[offset++] = color.b;
canvas_buffer.data[offset++] = 255; // Alpha = 255 (full opacity)
canvas_buffer
}
// Displays the contents of the offscreen buffer into the canvas.
function UpdateCanvas() {
.putImageData(canvas_buffer, 0, 0);
canvas_context
}
// ======================================================================
// Depth buffer.
// ======================================================================
let depth_buffer = Array();
.length = canvas.width * canvas.height;
depth_buffer
function UpdateDepthBufferIfCloser(x, y, inv_z) {
= canvas.width/2 + (x | 0);
x = canvas.height/2 - (y | 0) - 1;
y
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) {
return false;
}
let offset = x + canvas.width*y;
if (depth_buffer[offset] == undefined || depth_buffer[offset] < inv_z) {
= inv_z;
depth_buffer[offset] return true;
}return false;
}
function ClearAll() {
.width = canvas.width;
canvas= Array();
depth_buffer .length = canvas.width * canvas.height;
depth_buffer
}
// ======================================================================
// Data model.
// ======================================================================
// A Point.
function Pt(x, y, h) {
return {x, y, h};
}
// A 3D vertex.
function Vertex(x, y, z) {
return {
, y, z,
xadd: function(v) { return new Vertex(this.x + v.x, this.y + v.y, this.z + v.z); },
mul: function(n) { return new Vertex(this.x*n, this.y*n, this.z*n); },
dot: function(vec) { return this.x*vec.x + this.y*vec.y + this.z*vec.z; },
}
}
// A 4D vertex (a 3D vertex in homogeneous coordinates).
function Vertex4(arg1, y, z, w) {
if (y == undefined) {
this.x = arg1.x;
this.y = arg1.y;
this.z = arg1.z;
this.w = arg1.w | 1;
else {
} this.x = arg1;
this.y = y;
this.z = z;
this.w = w;
}this.sub = function(v) { return new Vertex4(this.x - v.x, this.y - v.y, this.z - v.z, this.w - v.w); };
this.mul = function(n) { return new Vertex4(this.x*n, this.y*n, this.z*n, this.w); };
this.dot = function(vec) { return this.x*vec.x + this.y*vec.y + this.z*vec.z; };
this.cross = function(v2) { return new Vertex4(this.y*v2.z - this.z*v2.y, this.z*v2.x - this.x*v2.z, this.x*v2.y - this.y*v2.x); };
}
// A 4x4 matrix.
function Mat4x4(data) {
return {data};
}
const Identity4x4 = new Mat4x4([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]);
// A Triangle.
function Triangle(indexes, color) {
return {indexes, color}
}
// A Model.
function Model(vertices, triangles, bounds_center, bounds_radius) {
return {vertices, triangles, bounds_center, bounds_radius};
}
// An Instance.
function Instance(model, position, orientation, scale) {
this.model = model;
this.position = position;
this.orientation = orientation || Identity4x4;
this.scale = scale || 1.0;
this.transform = MultiplyMM4(MakeTranslationMatrix(this.position), MultiplyMM4(this.orientation, MakeScalingMatrix(this.scale)));
}
// The Camera.
function Camera(position, orientation) {
this.position = position;
this.orientation = orientation;
this.clipping_planes = [];
}
// A Clipping Plane.
function Plane(normal, distance) {
return {normal, distance};
}
// ======================================================================
// Linear algebra and helpers.
// ======================================================================
// Makes a transform matrix for a rotation around the OY axis.
function MakeOYRotationMatrix(degrees) {
let cos = Math.cos(degrees*Math.PI/180.0);
let sin = Math.sin(degrees*Math.PI/180.0);
return new Mat4x4([[cos, 0, -sin, 0],
0, 1, 0, 0],
[ , 0, cos, 0],
[sin0, 0, 0, 1]])
[
}
// Makes a transform matrix for a translation.
function MakeTranslationMatrix(translation) {
return new Mat4x4([[1, 0, 0, translation.x],
0, 1, 0, translation.y],
[0, 0, 1, translation.z],
[0, 0, 0, 1]]);
[
}
// Makes a transform matrix for a scaling.
function MakeScalingMatrix(scale) {
return new Mat4x4([[scale, 0, 0, 0],
0, scale, 0, 0],
[ 0, 0, scale, 0],
[ 0, 0, 0, 1]]);
[
}
// Multiplies a 4x4 matrix and a 4D vector.
function MultiplyMV(mat4x4, vec4) {
let result = [0, 0, 0, 0];
let vec = [vec4.x, vec4.y, vec4.z, vec4.w];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
+= mat4x4.data[i][j]*vec[j];
result[i]
}
}
return new Vertex4(result[0], result[1], result[2], result[3]);
}
// Multiplies two 4x4 matrices.
function MultiplyMM4(matA, matB) {
let result = new Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
for (let k = 0; k < 4; k++) {
.data[i][j] += matA.data[i][k]*matB.data[k][j];
result
}
}
}
return result;
}
// Transposes a 4x4 matrix.
function Transposed(mat) {
let result = new Mat4x4([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
.data[i][j] = mat.data[j][i];
result
}
}return result;
}
function Shuffle(vec){
for (let i = vec.length - 1; i > 0; --i) {
let rand = Math.floor(Math.random() * (i + 1));
, vec[rand]] = [vec[rand], vec[i]];
[vec[i]
}
}
// ======================================================================
// Rasterization code.
// ======================================================================
// Scene setup.
let viewport_size = 1;
let projection_plane_z = 1;
function Interpolate(i0, d0, i1, d1) {
if (i0 == i1) {
return [d0];
}
let values = [];
let a = (d1 - d0) / (i1 - i0);
let d = d0;
for (let i = i0; i <= i1; i++) {
.push(d);
values+= a;
d
}
return values;
}
function DrawLine(p0, p1, color) {
let dx = p1.x - p0.x, dy = p1.y - p0.y;
if (Math.abs(dx) > Math.abs(dy)) {
// The line is horizontal-ish. Make sure it's left to right.
if (dx < 0) { let swap = p0; p0 = p1; p1 = swap; }
// Compute the Y values and draw.
let ys = Interpolate(p0.x, p0.y, p1.x, p1.y);
for (let x = p0.x; x <= p1.x; x++) {
PutPixel(x, ys[(x - p0.x) | 0], color);
}else {
} // The line is verical-ish. Make sure it's bottom to top.
if (dy < 0) { let swap = p0; p0 = p1; p1 = swap; }
// Compute the X values and draw.
let xs = Interpolate(p0.y, p0.x, p1.y, p1.x);
for (let y = p0.y; y <= p1.y; y++) {
PutPixel(xs[(y - p0.y) | 0], y, color);
}
}
}
function DrawWireframeTriangle(p0, p1, p2, color) {
DrawLine(p0, p1, color);
DrawLine(p1, p2, color);
DrawLine(p0, p2, color);
}
// Converts 2D viewport coordinates to 2D canvas coordinates.
function ViewportToCanvas(p2d) {
return new Pt(
.x * canvas.width / viewport_size | 0,
p2d.y * canvas.height / viewport_size | 0);
p2d
}
function ProjectVertex(v) {
return ViewportToCanvas(new Pt(
.x * projection_plane_z / v.z,
v.y * projection_plane_z / v.z));
v
}
// Sort the points from bottom to top.
// Technically, sort the indexes to the vertex indexes in the triangle from bottom to top.
function SortedVertexIndexes(vertex_indexes, projected) {
let indexes = [0, 1, 2];
if (projected[vertex_indexes[indexes[1]]].y < projected[vertex_indexes[indexes[0]]].y) { let swap = indexes[0]; indexes[0] = indexes[1]; indexes[1] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[0]]].y) { let swap = indexes[0]; indexes[0] = indexes[2]; indexes[2] = swap; }
if (projected[vertex_indexes[indexes[2]]].y < projected[vertex_indexes[indexes[1]]].y) { let swap = indexes[1]; indexes[1] = indexes[2]; indexes[2] = swap; }
return indexes;
}
function ComputeTriangleNormal(v0, v1, v2) {
let v0v1 = v1.sub(v0);
let v0v2 = v2.sub(v0);
return v0v1.cross(v0v2);
}
function EdgeInterpolate(y0, v0, y1, v1, y2, v2) {
let v01 = Interpolate(y0, v0, y1, v1);
let v12 = Interpolate(y1, v1, y2, v2);
let v02 = Interpolate(y0, v0, y2, v2);
.pop();
v01let v012 = v01.concat(v12);
return [v02, v012];
}
// Controls depth buffering and backface culling.
let depthBufferingEnabled = true;
let backfaceCullingEnabled = true;
let drawOutlines = false;
function RenderTriangle(triangle, vertices, projected) {
// Sort by projected point Y.
let indexes = SortedVertexIndexes(triangle.indexes, projected);
let [i0, i1, i2] = indexes;
let v0 = vertices[triangle.indexes[i0]];
let v1 = vertices[triangle.indexes[i1]];
let v2 = vertices[triangle.indexes[i2]];
// Compute triangle normal. Use the unsorted vertices, otherwise the winding of the points may change.
let normal = ComputeTriangleNormal(vertices[triangle.indexes[0]], vertices[triangle.indexes[1]], vertices[triangle.indexes[2]]);
// Backface culling.
if (backfaceCullingEnabled) {
let vertex_to_camera = vertices[triangle.indexes[0]].mul(-1); // Should be Subtract(camera.position, vertices[triangle.indexes[0]])
if (vertex_to_camera.dot(normal) <= 0) {
return;
}
}
// Get attribute values (X, 1/Z) at the vertices.
let p0 = projected[triangle.indexes[i0]];
let p1 = projected[triangle.indexes[i1]];
let p2 = projected[triangle.indexes[i2]];
// Compute attribute values at the edges.
let [x02, x012] = EdgeInterpolate(p0.y, p0.x, p1.y, p1.x, p2.y, p2.x);
let [iz02, iz012] = EdgeInterpolate(p0.y, 1.0/v0.z, p1.y, 1.0/v1.z, p2.y, 1.0/v2.z);
// Determine which is left and which is right.
let m = (x02.length/2) | 0;
if (x02[m] < x012[m]) {
var [x_left, x_right] = [x02, x012];
var [iz_left, iz_right] = [iz02, iz012];
else {
} var [x_left, x_right] = [x012, x02];
var [iz_left, iz_right] = [iz012, iz02];
}
// Draw horizontal segments.
for (let y = p0.y; y <= p2.y; y++) {
let [xl, xr] = [x_left[y - p0.y] | 0, x_right[y - p0.y] | 0];
// Interpolate attributes for this scanline.
let [zl, zr] = [iz_left[y - p0.y], iz_right[y - p0.y]];
let zscan = Interpolate(xl, zl, xr, zr);
for (let x = xl; x <= xr; x++) {
if (!depthBufferingEnabled || UpdateDepthBufferIfCloser(x, y, zscan[x - xl])) {
PutPixel(x, y, triangle.color);
}
}
}
if (drawOutlines) {
let outline_color = triangle.color.mul(0.75);
DrawLine(p0, p1, outline_color);
DrawLine(p0, p2, outline_color);
DrawLine(p2, p1, outline_color);
}
}
// Clips a triangle against a plane. Adds output to triangles and vertices.
function ClipTriangle(triangle, plane, triangles, vertices) {
let v0 = vertices[triangle.indexes[0]];
let v1 = vertices[triangle.indexes[1]];
let v2 = vertices[triangle.indexes[2]];
let in0 = plane.normal.dot(v0) + plane.distance > 0;
let in1 = plane.normal.dot(v1) + plane.distance > 0;
let in2 = plane.normal.dot(v2) + plane.distance > 0;
let in_count = in0 + in1 + in2;
if (in_count == 0) {
// Nothing to do - the triangle is fully clipped out.
else if (in_count == 3) {
} // The triangle is fully in front of the plane.
.push(triangle);
triangleselse if (in_count == 1) {
} // The triangle has one vertex in. Output is one clipped triangle.
else if (in_count == 2) {
} // The triangle has two vertices in. Output is two clipped triangles.
}
}
function TransformAndClip(clipping_planes, model, scale, transform) {
// Transform the bounding sphere, and attempt early discard.
let center = MultiplyMV(transform, new Vertex4(model.bounds_center));
let radius = model.bounds_radius*scale;
for (let p = 0; p < clipping_planes.length; p++) {
let distance = clipping_planes[p].normal.dot(center) + clipping_planes[p].distance;
if (distance < -radius) {
return null;
}
}
// Apply modelview transform.
let vertices = [];
for (let i = 0; i < model.vertices.length; i++) {
.push(MultiplyMV(transform, new Vertex4(model.vertices[i])));
vertices
}
// Clip the entire model against each successive plane.
let triangles = model.triangles.slice();
for (let p = 0; p < clipping_planes.length; p++) {
let new_triangles = []
for (let i = 0; i < triangles.length; i++) {
ClipTriangle(triangles[i], clipping_planes[p], new_triangles, vertices);
}= new_triangles;
triangles
}
return Model(vertices, triangles, center, model.bounds_radius);
}
function RenderModel(model) {
let projected = [];
for (let i = 0; i < model.vertices.length; i++) {
.push(ProjectVertex(new Vertex4(model.vertices[i])));
projected
}for (let i = 0; i < model.triangles.length; i++) {
RenderTriangle(model.triangles[i], model.vertices, projected);
}
}
function RenderScene(camera, instances) {
let cameraMatrix = MultiplyMM4(Transposed(camera.orientation), MakeTranslationMatrix(camera.position.mul(-1)));
for (let i = 0; i < instances.length; i++) {
let transform = MultiplyMM4(cameraMatrix, instances[i].transform);
let clipped = TransformAndClip(camera.clipping_planes, instances[i].model, instances[i].scale, transform);
if (clipped != null) {
RenderModel(clipped);
}
}
}
const vertices = [
new Vertex(1, 1, 1),
new Vertex(-1, 1, 1),
new Vertex(-1, -1, 1),
new Vertex(1, -1, 1),
new Vertex(1, 1, -1),
new Vertex(-1, 1, -1),
new Vertex(-1, -1, -1),
new Vertex(1, -1, -1)
;
]
const RED = new Color(255, 0, 0);
const GREEN = new Color(0, 255, 0);
const BLUE = new Color(0, 0, 255);
const YELLOW = new Color(255, 255, 0);
const PURPLE = new Color(255, 0, 255);
const CYAN = new Color(0, 255, 255);
const triangles = [
new Triangle([0, 1, 2], RED),
new Triangle([0, 2, 3], RED),
new Triangle([1, 5, 6], YELLOW),
new Triangle([1, 6, 2], YELLOW),
new Triangle([2, 6, 7], CYAN),
new Triangle([2, 7, 3], CYAN),
new Triangle([4, 0, 3], GREEN),
new Triangle([4, 1, 0], PURPLE),
new Triangle([4, 3, 7], GREEN),
new Triangle([4, 5, 1], PURPLE),
new Triangle([5, 4, 7], BLUE),
new Triangle([5, 7, 6], BLUE),
;
]
let cube = new Model(vertices, triangles, new Vertex(0, 0, 0), Math.sqrt(3));
let instances = [
new Instance(cube, new Vertex(-1.5, 0, 7), Identity4x4, 0.75),
new Instance(cube, new Vertex(1.25, 2.5, 7.5), MakeOYRotationMatrix(195)),
;
]
let camera = new Camera(new Vertex(-3, 1, 2), MakeOYRotationMatrix(-30));
let s2 = 1.0 / Math.sqrt(2);
.clipping_planes = [
cameranew Plane(new Vertex( 0, 0, 1), -1), // Near
new Plane(new Vertex( s2, 0, s2), 0), // Left
new Plane(new Vertex(-s2, 0, s2), 0), // Right
new Plane(new Vertex( 0, -s2, s2), 0), // Top
new Plane(new Vertex( 0, s2, s2), 0), // Bottom
;
]
function ShuffleCubeTriangles() {
Shuffle(cube.triangles);
Render();
}
function SetDepthEnabled(enabled) {
= enabled;
depthBufferingEnabled = enabled;
backfaceCullingEnabled Render();
}
function SetOutlinesEnabled(enabled) {
= enabled;
drawOutlines Render();
}
function Render() {
ClearAll();
// This lets the browser clear the canvas before blocking to render the scene.
setTimeout(function(){
RenderScene(camera, instances);
UpdateCanvas();
, 0);
}
}
Render();
</script>