現(xiàn)代 Web 技術(shù)使開(kāi)發(fā)人員能夠創(chuàng)建干凈而視覺(jué)豐富的用戶(hù)體驗(yàn),這些體驗(yàn)被所有主流瀏覽器作為標(biāo)準(zhǔn)進(jìn)行廣泛支持。那么,,如何為 Web 編寫(xiě)基于標(biāo)準(zhǔn)的可視化程序呢,?對(duì) 3D 圖形的支持到底又有哪些呢,?讓我們首先回顧 HTML 標(biāo)準(zhǔn)中支持的兩種主要方法:SVG 和 Canvas,。 SVG 本身是基于 XML 的一種獨(dú)立的數(shù)據(jù)格式,,用于聲明式的 2D 矢量圖形,。但是,它也可以嵌入到 HTML 文檔中,,這是所有主流瀏覽器都支持的,。
讓我們考慮一個(gè)例子,如何使用 SVG 繪制一個(gè)可調(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>
想要理解這段代碼很容易,!我們只是向?yàn)g覽器描述了要繪制什么(與傳統(tǒng) HTML 文檔非常相似),。它保留了這個(gè)描述,并負(fù)責(zé)如何在屏幕上繪制它,。
當(dāng)瀏覽器窗口調(diào)整大小或縮放時(shí),,它將重新縮放圖像,而不會(huì)丟失圖像的任何質(zhì)量(因?yàn)閳D像是根據(jù)形狀定義的,,而不是根據(jù)像素定義的),。當(dāng) SVG 元素被 JavaScript 代碼修改時(shí),它還會(huì)自動(dòng)重新繪制圖像,,這使得 SVG 特別適合與 JavaScript 庫(kù)(如 D3)一起使用,,D3 將數(shù)據(jù)綁定到 DOM 中的元素,,從而能夠創(chuàng)建從簡(jiǎn)單圖表到更奇特的交互式數(shù)據(jù)可視化的任何內(nèi)容。
這種聲明性方法也稱(chēng)為保留模式圖形繪制(retained-mode graphics rendering),。
canvas 元素只是在網(wǎng)頁(yè)上提供了一個(gè)可以繪圖的區(qū)域,。使用 JavaScript 代碼,首先從畫(huà)布獲取上下文,,然后使用提供的 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í)行時(shí),,圖像立即繪制成了底層位圖的像素,瀏覽器不保留繪制方式的任何信息,。為了更新繪圖,,需要再次執(zhí)行腳本。重新縮放圖像時(shí),,也會(huì)觸發(fā)更新繪圖,,否則,瀏覽器只會(huì)拉伸原始位圖,,導(dǎo)致圖像明顯模糊或像素化,。
這種函數(shù)式方法也稱(chēng)為即時(shí)模式圖形繪制(immediate-mode graphics rendering)。
首先讓我們考慮 2D 繪制的上下文,,它提供了一個(gè)用于在畫(huà)布上繪制 2D 圖形的高級(jí) API,。
讓我們來(lái)看一個(gè)例子,看看如何使用它來(lái)繪制我們可調(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>
同樣,,這非常簡(jiǎn)單,,但肯定比前面的示例更冗長(zhǎng)!我們必須自己根據(jù)畫(huà)布的當(dāng)前大小,,以像素為單位計(jì)算圓的半徑和中心位置,。這也意味著我們必須監(jiān)聽(tīng)縮放的事件并相應(yīng)地重新繪制。
那么,,既然更加復(fù)雜,,為什么還要使用這種方法而不是 SVG 呢?在大多數(shù)情況下,,你可能不會(huì)使用該方法,。然而,這給了你對(duì)渲染的內(nèi)容更多的控制,。對(duì)于要繪制更多對(duì)象的,、更復(fù)雜的動(dòng)態(tài)可視化,它可能比更新 DOM 中的大量元素,,并讓瀏覽器來(lái)決定何時(shí)呈現(xiàn)和呈現(xiàn)什么,,帶來(lái)更好的性能,。
大多數(shù)現(xiàn)代瀏覽器也支持 webgl 上下文。這為您提供了使用 WebGL 標(biāo)準(zhǔn)繪制硬件加速圖形的底層 API,,盡管這需要 GPU 支持,。它可以用來(lái)渲染 2D,更重要的是,,也可以用來(lái)渲染本篇博客所說(shuō)的 3D 圖形,。
現(xiàn)在讓我們來(lái)看一個(gè)例子,看看如何使用 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ù)雜度升級(jí)得相當(dāng)快,!在我們渲染任何東西之前,,要做很多設(shè)置。我們必須使用頂點(diǎn)列表,,將圓定義為由小三角形組成的一個(gè)序列,。我們還必須定義一個(gè)投影矩陣,將我們的 3D 模型(一個(gè)平面圓)投影到 2D 畫(huà)布上,。然后,,我們必須編寫(xiě)“著色器”(用一種稱(chēng)為 GLSL 的語(yǔ)言),在 GPU 上編譯并運(yùn)行,,以確定頂點(diǎn)的位置和顏色,。
但是,額外的復(fù)雜性和較底層的 API,,確實(shí)能夠讓我們更好地控制 2D 圖形繪制的性能(如果我們真的需要的話),。它還為我們提供了渲染 3D 可視化的能力,即使我們還沒(méi)有考慮過(guò)這樣的例子,。
現(xiàn)在我們已經(jīng)了解了 WebGL,,并了解了如何使用它來(lái)繪制一個(gè)圓。隨著我們進(jìn)入 3D 圖形的世界,,下一個(gè)步驟就是使用它來(lái)繪制一個(gè)球體,。然而,這增加了另一層次的復(fù)雜性,,因?yàn)槲覀儗⒁伎?,如何使用一組頂點(diǎn)來(lái)表示球面。我們還需要添加一些燈光效果,,這樣我們就可以看到一個(gè)球體的輪廓,,而不是從任何角度都只能看到一個(gè)平坦的紅色圓圈。
我們還看到,,對(duì)于絕對(duì)性能并不重要的場(chǎng)景,,SVG 等簡(jiǎn)單而簡(jiǎn)潔的聲明式方法可以發(fā)揮多大的作用。它們還可以讓我們使用 D3 這樣的庫(kù),輕松地生成與數(shù)據(jù)連接起來(lái)的可視化,。所以,,如果我們能以類(lèi)似的方式表示基于 Web 的 3D 圖形,那不是更好嗎?
遺憾的是,,目前 HTML 中的標(biāo)準(zhǔn)還不支持這個(gè)操作,。但也許還有另一種方法……
正如 Mike Bostock(D3 的創(chuàng)建者)在 POC(Proof of Concept)中所演示的,在 DOM 中定義 2D“素描”的定制化 XML 表示,,并將其與一些 JavaScript 代碼結(jié)合,,使用 2D 上下文將其繪制到畫(huà)布上,這相對(duì)來(lái)說(shuō)會(huì)更加簡(jiǎn)單,。
這意味著在所有主流瀏覽器上運(yùn)行的聲明式 3D 真正需要的是:X3D 是表示 3D 模型的 ISO 標(biāo)準(zhǔn),,是虛擬現(xiàn)實(shí)建模語(yǔ)言(VRML)的后續(xù)標(biāo)準(zhǔn),。它可以表示為各種編碼,,包括 JSON 和 XML。后者特別適合嵌入到 HTML 文檔中,。它由 Web3D 聯(lián)盟維護(hù),,他們希望它能像 SVG 一樣在 HTML5 中得到原生支持。
目前有兩種被 Web3D 聯(lián)盟認(rèn)可的 JavaScript 開(kāi)源 X3D 實(shí)現(xiàn):X3DOM 和 X_ite,。
X3DOM 是由弗勞恩霍夫計(jì)算機(jī)圖形研究所 IGD(The Fraunhofer Institute for Computer Graphics Research IGD)開(kāi)發(fā)的,,IGD 本身也是 Web3D 聯(lián)盟的成員。為了使用它,,您只需要在 HTML 頁(yè)面中包含 X3DOM JavaScript 代碼和樣式表,。
讓我們來(lái)看看用 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)行比較,,您會(huì)注意到圓周看起來(lái)不那么光滑。這是因?yàn)?X3DOM 庫(kù)對(duì)形狀的近似只使用了 32 條線段,。而我們的 WebGL 繪制中選擇了 360 條線段,。我們對(duì)要渲染什么有一個(gè)更簡(jiǎn)單的描述,但同時(shí)也會(huì)放棄對(duì)如何渲染的一些控制,。
現(xiàn)在是時(shí)候走出我們的“平面”世界,,渲染一些 3D 的東西了!如前所述,,讓我們來(lái)看看一個(gè)球體的繪制:<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>
這又是很直接的,。我們使用一個(gè) XML 元素定義了一個(gè)球體,該元素具有單一屬性:半徑。為了看到球體的輪廓,,我們還調(diào)整了光線,,移除了與觀察者頭部對(duì)齊的默認(rèn)光源,并用與我們視角成一定角度的定向光替換它,。這不需要為球體的表面定義一個(gè)復(fù)雜的網(wǎng)格或者編寫(xiě)一個(gè)著色器來(lái)控制光照效果,。
X3DOM 還提供了開(kāi)箱即用的導(dǎo)航功能,允許您旋轉(zhuǎn),、平移和縮放模型,。根據(jù)您正在編寫(xiě)的應(yīng)用程序的類(lèi)型,還可以使用各種不同的控制方案和導(dǎo)航模式,。
就是這樣,!我們已經(jīng)看到可以使用 X3D 和 X3DOM 庫(kù)來(lái)編寫(xiě)聲明式的 3D 圖形應(yīng)用,這些圖形將在大多數(shù)現(xiàn)代 Web 瀏覽器中運(yùn)行,。這是一種比直接深鉆 WebGL 更簡(jiǎn)單的 Web 3D 圖形入門(mén)方法,,代價(jià)是增加對(duì)底層繪制的控制。如果您有興趣了解這個(gè)庫(kù)的更多信息,,官方 X3DOM 文檔中有一些教程,。
在我的下一篇博客文章中,我將演示如何將 X3DOM 與 D3 結(jié)合起來(lái)生成動(dòng)態(tài) 3D 圖表,。
英文原文:
https://blog./2019/08/27/declarative-3d-for-the-modern-web.html