您的位置:首页 > 其它

X3D:现代Web的声明式3D技术

2019-10-07 15:49 471 查看


作者|Adrian Sureshkumar
译者|夏夜
编辑|王文婧
现代 Web 技术使开发人员能够创建干净而视觉丰富的用户体验,这些体验被所有主流浏览器作为标准进行广泛支持。那么,如何为 Web 编写基于标准的可视化程序呢?对 3D 图形的支持到底又有哪些呢?让我们首先回顾 HTML 标准中支持的两种主要方法:SVG 和 Canvas。
SVG:可伸缩的矢量图形 

SVG 本身是基于 XML 的一种独立的数据格式,用于声明式的 2D 矢量图形。但是,它也可以嵌入到 HTML 文档中,这是所有主流浏览器都支持的。

让我们考虑一个例子,如何使用 SVG 绘制一个可调整大小的圆:
<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>

想要理解这段代码很容易!我们只是向浏览器描述了要绘制什么(与传统 HTML 文档非常相似)。它保留了这个描述,并负责如何在屏幕上绘制它。

当浏览器窗口调整大小或缩放时,它将重新缩放图像,而不会丢失图像的任何质量(因为图像是根据形状定义的,而不是根据像素定义的)。当 SVG 元素被 JavaScript 代码修改时,它还会自动重新绘制图像,这使得 SVG 特别适合与 JavaScript 库(如 D3)一起使用,D3 将数据绑定到 DOM 中的元素,从而能够创建从简单图表到更奇特的交互式数据可视化的任何内容。

这种声明性方法也称为保留模式图形绘制(retained-mode graphics rendering)。

 画  布 
canvas 元素只是在网页上提供了一个可以绘图的区域。使用 JavaScript 代码,首先从画布获取上下文,然后使用提供的 API,定义绘制图像的函数。
const canvas = document.getElementById(id);
const context = canvas.getContext(contextType);

// call some methods on context to draw onto the canvas

当脚本执行时,图像立即绘制成了底层位图的像素,浏览器不保留绘制方式的任何信息。为了更新绘图,需要再次执行脚本。重新缩放图像时,也会触发更新绘图,否则,浏览器只会拉伸原始位图,导致图像明显模糊或像素化。

这种函数式方法也称为即时模式图形绘制(immediate-mode graphics rendering)。

上下文:2D

首先让我们考虑 2D 绘制的上下文,它提供了一个用于在画布上绘制 2D 图形的高级 API。

让我们来看一个例子,看看如何使用它来绘制我们可调整大小的圆:
<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>

同样,这非常简单,但肯定比前面的示例更冗长!我们必须自己根据画布的当前大小,以像素为单位计算圆的半径和中心位置。这也意味着我们必须监听缩放的事件并相应地重新绘制。

那么,既然更加复杂,为什么还要使用这种方法而不是 SVG 呢?在大多数情况下,你可能不会使用该方法。然而,这给了你对渲染的内容更多的控制。对于要绘制更多对象的、更复杂的动态可视化,它可能比更新 DOM 中的大量元素,并让浏览器来决定何时呈现和呈现什么,带来更好的性能。

上下文:WebGL 

大多数现代浏览器也支持 webgl 上下文。这为您提供了使用 WebGL 标准绘制硬件加速图形的底层 API,尽管这需要 GPU 支持。它可以用来渲染 2D,更重要的是,也可以用来渲染本篇博客所说的 3D 图形。

现在让我们来看一个例子,看看如何使用 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>

复杂度升级得相当快!在我们渲染任何东西之前,要做很多设置。我们必须使用顶点列表,将圆定义为由小三角形组成的一个序列。我们还必须定义一个投影矩阵,将我们的 3D 模型(一个平面圆)投影到 2D 画布上。然后,我们必须编写“着色器”(用一种称为 GLSL 的语言),在 GPU 上编译并运行,以确定顶点的位置和颜色。

但是,额外的复杂性和较底层的 API,确实能够让我们更好地控制 2D 图形绘制的性能(如果我们真的需要的话)。它还为我们提供了渲染 3D 可视化的能力,即使我们还没有考虑过这样的例子。

面向声明式的 3D 图形 

现在我们已经了解了 WebGL,并了解了如何使用它来绘制一个圆。随着我们进入 3D 图形的世界,下一个步骤就是使用它来绘制一个球体。然而,这增加了另一层次的复杂性,因为我们将要思考,如何使用一组顶点来表示球面。我们还需要添加一些灯光效果,这样我们就可以看到一个球体的轮廓,而不是从任何角度都只能看到一个平坦的红色圆圈。

我们还看到,对于绝对性能并不重要的场景,SVG 等简单而简洁的声明式方法可以发挥多大的作用。它们还可以让我们使用 D3 这样的库,轻松地生成与数据连接起来的可视化。所以,如果我们能以类似的方式表示基于 Web 的 3D 图形,那不是更好吗?

遗憾的是,目前 HTML 中的标准还不支持这个操作。但也许还有另一种方法……

正如 Mike Bostock(D3 的创建者)在 POC(Proof of Concept)中所演示的,在 DOM 中定义 2D“素描”的定制化 XML 表示,并将其与一些 JavaScript 代码结合,使用 2D 上下文将其绘制到画布上,这相对来说会更加简单。

这意味着在所有主流浏览器上运行的声明式 3D 真正需要的是:
  • 基于 XML 格式的 3D 模型声明;
  • 使用 webgl 上下文将它们绘制到画布上的 JavaScript 代码。

 X3D ——拼图中缺失的这块? 

X3D 是表示 3D 模型的 ISO 标准,是虚拟现实建模语言(VRML)的后续标准。它可以表示为各种编码,包括 JSON 和 XML。后者特别适合嵌入到 HTML 文档中。它由 Web3D 联盟维护,他们希望它能像 SVG 一样在 HTML5 中得到原生支持。

目前有两种被 Web3D 联盟认可的 JavaScript 开源 X3D 实现:X3DOM 和 X_ite。

X3DOM 是由弗劳恩霍夫计算机图形研究所 IGD(The Fraunhofer Institute for Computer Graphics Research IGD)开发的,IGD 本身也是 Web3D 联盟的成员。为了使用它,您只需要在 HTML 页面中包含 X3DOM JavaScript 代码和样式表。

让我们来看看用 X3D 和 X3DOM 绘制圆圈的例子:
<html style="height: 100%; width: 100%">
  <head>
    <script type="text/javascript" src="http://www.x3dom.org/release/x3dom-full.js"></script>
    <link rel="stylesheet" type="text/css" href="http://www.x3dom.org/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 版本进行比较,您会注意到圆周看起来不那么光滑。这是因为 X3DOM 库对形状的近似只使用了 32 条线段。而我们的 WebGL 绘制中选择了 360 条线段。我们对要渲染什么有一个更简单的描述,但同时也会放弃对如何渲染的一些控制。

现在是时候走出我们的“平面”世界,渲染一些 3D 的东西了!如前所述,让我们来看看一个球体的绘制:
<html style="height: 100%; width: 100%">
  <head>
    <script type="text/javascript" src="http://www.x3dom.org/release/x3dom-full.js"></script>
    <link rel="stylesheet" type="text/css" href="http://www.x3dom.org/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 元素定义了一个球体,该元素具有单一属性:半径。为了看到球体的轮廓,我们还调整了光线,移除了与观察者头部对齐的默认光源,并用与我们视角成一定角度的定向光替换它。这不需要为球体的表面定义一个复杂的网格或者编写一个着色器来控制光照效果。

X3DOM 还提供了开箱即用的导航功能,允许您旋转、平移和缩放模型。根据您正在编写的应用程序的类型,还可以使用各种不同的控制方案和导航模式。

 结  论 

就是这样!我们已经看到可以使用 X3D 和 X3DOM 库来编写声明式的 3D 图形应用,这些图形将在大多数现代 Web 浏览器中运行。这是一种比直接深钻 WebGL 更简单的 Web 3D 图形入门方法,代价是增加对底层绘制的控制。如果您有兴趣了解这个库的更多信息,官方 X3DOM 文档中有一些教程。

在我的下一篇博客文章中,我将演示如何将 X3DOM 与 D3 结合起来生成动态 3D 图表。

英文原文:

https://blog.scottlogic.com/2019/08/27/declarative-3d-for-the-modern-web.html


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: