現(xiàn)代 Web 技術(shù)使開發(fā)人員能夠創(chuàng)建干凈而視覺豐富的用戶體驗,這些體驗被所有主流瀏覽器作為標(biāo)準(zhǔn)進(jìn)行廣泛支持。那么,如何為 Web 編寫基于標(biāo)準(zhǔn)的可視化程序呢?對 3D 圖形的支持到底又有哪些呢?讓我們首先回顧 HTML 標(biāo)準(zhǔn)中支持的兩種主要方法:SVG 和 Canvas。 SVG 本身是基于 XML 的一種獨立的數(shù)據(jù)格式,用于聲明式的 2D 矢量圖形。但是,它也可以嵌入到 HTML 文檔中,這是所有主流瀏覽器都支持的。
讓我們考慮一個例子,如何使用 SVG 繪制一個可調(diào)整大小的圓:<html style='height: 100%; width: 100%'>
<body style='height: 100%; width: 100%; margin: 0px'>
<svg style='height: 100%; width: 100%; display: block' viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='25' fill='red' stroke='black'
vector-effect='non-scaling-stroke' />
</svg>
</body>
</html>
想要理解這段代碼很容易!我們只是向瀏覽器描述了要繪制什么(與傳統(tǒng) HTML 文檔非常相似)。它保留了這個描述,并負(fù)責(zé)如何在屏幕上繪制它。
當(dāng)瀏覽器窗口調(diào)整大小或縮放時,它將重新縮放圖像,而不會丟失圖像的任何質(zhì)量(因為圖像是根據(jù)形狀定義的,而不是根據(jù)像素定義的)。當(dāng) SVG 元素被 JavaScript 代碼修改時,它還會自動重新繪制圖像,這使得 SVG 特別適合與 JavaScript 庫(如 D3)一起使用,D3 將數(shù)據(jù)綁定到 DOM 中的元素,從而能夠創(chuàng)建從簡單圖表到更奇特的交互式數(shù)據(jù)可視化的任何內(nèi)容。
這種聲明性方法也稱為保留模式圖形繪制(retained-mode graphics rendering)。
canvas 元素只是在網(wǎng)頁上提供了一個可以繪圖的區(qū)域。使用 JavaScript 代碼,首先從畫布獲取上下文,然后使用提供的 API,定義繪制圖像的函數(shù)。const canvas = document.getElementById(id);
const context = canvas.getContext(contextType);
// call some methods on context to draw onto the canvas
當(dāng)腳本執(zhí)行時,圖像立即繪制成了底層位圖的像素,瀏覽器不保留繪制方式的任何信息。為了更新繪圖,需要再次執(zhí)行腳本。重新縮放圖像時,也會觸發(fā)更新繪圖,否則,瀏覽器只會拉伸原始位圖,導(dǎo)致圖像明顯模糊或像素化。
這種函數(shù)式方法也稱為即時模式圖形繪制(immediate-mode graphics rendering)。
首先讓我們考慮 2D 繪制的上下文,它提供了一個用于在畫布上繪制 2D 圖形的高級 API。
讓我們來看一個例子,看看如何使用它來繪制我們可調(diào)整大小的圓:<html style='height: 100%; width: 100%'>
<body style='height: 100%; width: 100%; margin: 0px'>
<canvas id='my-canvas' style='height: 100%; width: 100%; display: block'></canvas>
<script>
const canvas = document.getElementById('my-canvas');
const context = canvas.getContext('2d');
function render() {
// Size the drawing surface to match the actual element (no stretch).
canvas.height = canvas.clientHeight;
canvas.width = canvas.clientWidth;
context.beginPath();
// Calculate relative size and position of circle in pixels.
const x = 0.5 * canvas.width;
const y = 0.5 * canvas.height;
const radius = 0.25 * Math.min(canvas.height, canvas.width);
context.arc(x, y, radius, 0, 2 * Math.PI);
context.fillStyle = 'red';
context.fill();
context.strokeStyle = 'black';
context.stroke();
}
render();
addEventListener('resize', render);
</script>
</body>
</html>
同樣,這非常簡單,但肯定比前面的示例更冗長!我們必須自己根據(jù)畫布的當(dāng)前大小,以像素為單位計算圓的半徑和中心位置。這也意味著我們必須監(jiān)聽縮放的事件并相應(yīng)地重新繪制。
那么,既然更加復(fù)雜,為什么還要使用這種方法而不是 SVG 呢?在大多數(shù)情況下,你可能不會使用該方法。然而,這給了你對渲染的內(nèi)容更多的控制。對于要繪制更多對象的、更復(fù)雜的動態(tài)可視化,它可能比更新 DOM 中的大量元素,并讓瀏覽器來決定何時呈現(xiàn)和呈現(xiàn)什么,帶來更好的性能。
大多數(shù)現(xiàn)代瀏覽器也支持 webgl 上下文。這為您提供了使用 WebGL 標(biāo)準(zhǔn)繪制硬件加速圖形的底層 API,盡管這需要 GPU 支持。它可以用來渲染 2D,更重要的是,也可以用來渲染本篇博客所說的 3D 圖形。
現(xiàn)在讓我們來看一個例子,看看如何使用 WebGL 渲染我們的圓圈:<html style='height: 100%; width: 100%'>
<body style='height: 100%; width: 100%; margin: 0px'>
<canvas id='my-canvas' style='height: 100%; width: 100%; display: block'></canvas>
<script>
const canvas = document.getElementById('my-canvas');
const context = canvas.getContext('webgl');
const redColor = new Float32Array([1.0, 0.0, 0.0, 1.0]);
const blackColor = new Float32Array([0.0, 0.0, 0.0, 1.0]);
// Use an orthogonal projection matrix as we're rendering in 2D.
const projectionMatrix = new Float32Array([
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]);
// Define positions of the vertices of the circle (in clip space).
const radius = 0.5;
const segmentCount = 360;
const positions = [0.0, 0.0];
for (let i = 0; i < segmentCount + 1; i++) {
positions.push(radius * Math.sin(2 * Math.PI * i / segmentCount));
positions.push(radius * Math.cos(2 * Math.PI * i / segmentCount));
}
const positionBuffer = context.createBuffer();
context.bindBuffer(context.ARRAY_BUFFER, positionBuffer);
context.bufferData(context.ARRAY_BUFFER, new Float32Array(positions), context.STATIC_DRAW);
// Create shaders and program.
const vertexShader = context.createShader(context.VERTEX_SHADER);
context.shaderSource(vertexShader, `
attribute vec4 position;
uniform mat4 projection;
void main() {
gl_Position = projection * position;
}
`);
context.compileShader(vertexShader);
const fragmentShader = context.createShader(context.FRAGMENT_SHADER);
context.shaderSource(fragmentShader, `
uniform lowp vec4 color;
void main() {
gl_FragColor = color;
}
`);
context.compileShader(fragmentShader);
const program = context.createProgram();
context.attachShader(program, vertexShader);
context.attachShader(program, fragmentShader);
context.linkProgram(program);
const positionAttribute = context.getAttribLocation(program, 'position');
const colorUniform = context.getUniformLocation(program, 'color');
const projectionUniform = context.getUniformLocation(program, 'projection');
function render() {
// Size the drawing surface to match the actual element (no stretch).
canvas.height = canvas.clientHeight;
canvas.width = canvas.clientWidth;
context.viewport(0, 0, canvas.width, canvas.height);
context.useProgram(program);
// Scale projection to maintain 1:1 ratio between height and width on canvas.
projectionMatrix[0] = canvas.width > canvas.height ? canvas.height / canvas.width : 1.0;
projectionMatrix[5] = canvas.height > canvas.width ? canvas.width / canvas.height : 1.0;
context.uniformMatrix4fv(projectionUniform, false, projectionMatrix);
const vertexSize = 2;
const vertexCount = positions.length / vertexSize;
context.bindBuffer(context.ARRAY_BUFFER, positionBuffer);
context.vertexAttribPointer(positionAttribute, vertexSize, context.FLOAT, false, 0, 0);
context.enableVertexAttribArray(positionAttribute);
context.uniform4fv(colorUniform, redColor);
context.drawArrays(context.TRIANGLE_FAN, 0, vertexCount);
context.uniform4fv(colorUniform, blackColor);
context.drawArrays(context.LINE_STRIP, 1, vertexCount - 1);
}
render();
addEventListener('resize', render);
</script>
</body>
</html>
復(fù)雜度升級得相當(dāng)快!在我們渲染任何東西之前,要做很多設(shè)置。我們必須使用頂點列表,將圓定義為由小三角形組成的一個序列。我們還必須定義一個投影矩陣,將我們的 3D 模型(一個平面圓)投影到 2D 畫布上。然后,我們必須編寫“著色器”(用一種稱為 GLSL 的語言),在 GPU 上編譯并運行,以確定頂點的位置和顏色。
但是,額外的復(fù)雜性和較底層的 API,確實能夠讓我們更好地控制 2D 圖形繪制的性能(如果我們真的需要的話)。它還為我們提供了渲染 3D 可視化的能力,即使我們還沒有考慮過這樣的例子。
現(xiàn)在我們已經(jīng)了解了 WebGL,并了解了如何使用它來繪制一個圓。隨著我們進(jìn)入 3D 圖形的世界,下一個步驟就是使用它來繪制一個球體。然而,這增加了另一層次的復(fù)雜性,因為我們將要思考,如何使用一組頂點來表示球面。我們還需要添加一些燈光效果,這樣我們就可以看到一個球體的輪廓,而不是從任何角度都只能看到一個平坦的紅色圓圈。
我們還看到,對于絕對性能并不重要的場景,SVG 等簡單而簡潔的聲明式方法可以發(fā)揮多大的作用。它們還可以讓我們使用 D3 這樣的庫,輕松地生成與數(shù)據(jù)連接起來的可視化。所以,如果我們能以類似的方式表示基于 Web 的 3D 圖形,那不是更好嗎?
遺憾的是,目前 HTML 中的標(biāo)準(zhǔn)還不支持這個操作。但也許還有另一種方法……
正如 Mike Bostock(D3 的創(chuàng)建者)在 POC(Proof of Concept)中所演示的,在 DOM 中定義 2D“素描”的定制化 XML 表示,并將其與一些 JavaScript 代碼結(jié)合,使用 2D 上下文將其繪制到畫布上,這相對來說會更加簡單。
這意味著在所有主流瀏覽器上運行的聲明式 3D 真正需要的是:X3D 是表示 3D 模型的 ISO 標(biāo)準(zhǔn),是虛擬現(xiàn)實建模語言(VRML)的后續(xù)標(biāo)準(zhǔn)。它可以表示為各種編碼,包括 JSON 和 XML。后者特別適合嵌入到 HTML 文檔中。它由 Web3D 聯(lián)盟維護(hù),他們希望它能像 SVG 一樣在 HTML5 中得到原生支持。
目前有兩種被 Web3D 聯(lián)盟認(rèn)可的 JavaScript 開源 X3D 實現(xiàn):X3DOM 和 X_ite。
X3DOM 是由弗勞恩霍夫計算機圖形研究所 IGD(The Fraunhofer Institute for Computer Graphics Research IGD)開發(fā)的,IGD 本身也是 Web3D 聯(lián)盟的成員。為了使用它,您只需要在 HTML 頁面中包含 X3DOM JavaScript 代碼和樣式表。
讓我們來看看用 X3D 和 X3DOM 繪制圓圈的例子:<html style='height: 100%; width: 100%'>
<head>
<script type='text/javascript' src='http://www./release/x3dom-full.js'></script>
<link rel='stylesheet' type='text/css' href='http://www./release/x3dom.css'>
<style>x3d > canvas { display: block; }</style>
</head>
<body style='height: 100%; width: 100%; margin: 0px'>
<x3d style='height: 100%; width: 100%'>
<scene>
<orthoviewpoint></orthoviewpoint>
<shape>
<appearance>
<material diffuseColor='1 0 0'></material>
</appearance>
<disk2d outerRadius='0.5'></disk2d>
</shape>
<shape>
<appearance>
<material emissiveColor='0 0 0'></material>
</appearance>
<circle2d radius='0.5'></circle2d>
</shape>
</scene>
</x3d>
</body>
</html>
這比 WebGL 示例更容易接受一些!但是,如果您將 X3DOM 圓與我們的 WebGL 版本進(jìn)行比較,您會注意到圓周看起來不那么光滑。這是因為 X3DOM 庫對形狀的近似只使用了 32 條線段。而我們的 WebGL 繪制中選擇了 360 條線段。我們對要渲染什么有一個更簡單的描述,但同時也會放棄對如何渲染的一些控制。
現(xiàn)在是時候走出我們的“平面”世界,渲染一些 3D 的東西了!如前所述,讓我們來看看一個球體的繪制:<html style='height: 100%; width: 100%'>
<head>
<script type='text/javascript' src='http://www./release/x3dom-full.js'></script>
<link rel='stylesheet' type='text/css' href='http://www./release/x3dom.css'>
<style>x3d > canvas { display: block; }</style>
</head>
<body style='height: 100%; width: 100%; margin: 0px'>
<x3d style='height: 100%; width: 100%'>
<scene>
<orthoviewpoint></orthoviewpoint>
<navigationinfo headlight='false'></navigationinfo>
<directionallight direction='1 -1 -1' on='true' intensity='1.0'></directionallight>
<shape>
<appearance>
<material diffuseColor='1 0 0'></material>
</appearance>
<sphere radius='0.5'></sphere>
</shape>
</scene>
</x3d>
</body>
</html>
這又是很直接的。我們使用一個 XML 元素定義了一個球體,該元素具有單一屬性:半徑。為了看到球體的輪廓,我們還調(diào)整了光線,移除了與觀察者頭部對齊的默認(rèn)光源,并用與我們視角成一定角度的定向光替換它。這不需要為球體的表面定義一個復(fù)雜的網(wǎng)格或者編寫一個著色器來控制光照效果。
X3DOM 還提供了開箱即用的導(dǎo)航功能,允許您旋轉(zhuǎn)、平移和縮放模型。根據(jù)您正在編寫的應(yīng)用程序的類型,還可以使用各種不同的控制方案和導(dǎo)航模式。
就是這樣!我們已經(jīng)看到可以使用 X3D 和 X3DOM 庫來編寫聲明式的 3D 圖形應(yīng)用,這些圖形將在大多數(shù)現(xiàn)代 Web 瀏覽器中運行。這是一種比直接深鉆 WebGL 更簡單的 Web 3D 圖形入門方法,代價是增加對底層繪制的控制。如果您有興趣了解這個庫的更多信息,官方 X3DOM 文檔中有一些教程。
在我的下一篇博客文章中,我將演示如何將 X3DOM 與 D3 結(jié)合起來生成動態(tài) 3D 圖表。
英文原文:
https://blog./2019/08/27/declarative-3d-for-the-modern-web.html