下载本文的 PDF 版本 PDF

DHTML 茶壶的故事

用 HTML 和 CSS 做惊人的事情很容易,例如渲染经典的茶壶。


Brian Beckman 和 Erik Meijer,感谢 Jeff Lau


在 SVG(可缩放矢量图形)、WebGL(Web 图形库)、Canvas 或浏览器中用于图形的大部分技术出现之前,有可能做到的远比最初想象的要多。为了证明这一点,我们创建了一个 JavaScript 程序,它仅使用 HTML 和 CSS 渲染多边形 3D 图形。我们的概念验证速度足够快,可以支持基于物理的小型游戏内容,但我们从标志性的 3D “犹他茶壶”(图 1)开始,因为它在一张图片中讲述了整个故事。(有关茶壶的背景,请参阅 http://www.sjbaker.org/wiki/index.php?title=The_History_of_The_Teapot。)使用常规的<div>元素、CSS 样式和少量 JavaScript 代码(图 2)来渲染这个经典对象是可行的。这个微小的图形管道是使用极少资源完成大量工作的永恒演示。

这个项目的灵感来自 Web 开发者 Jeff Lau,他在他的博客 UselessPickles 上用手写的 JavaScript 实现了教科书式的图形管道 (http://www.uselesspickles.com/triangles/)。Lau 的演示体现了一种高效渲染 HTML 三角形的辉煌技巧,如图 3 所示。

辉煌的技巧将在下一节中解释,但是,为了揭晓谜底,一旦你拥有了任意三角形,你就可以轻松渲染任意多边形,从而渲染任意基于多边形的模型。对于胜任游戏的 3D 图形来说,唯一剩下的问题是纹理映射、凹凸贴图、反射映射和性能。这些各种类型的映射都需要基于像素的图元:高效渲染单个像素的能力。虽然可能仅使用<div>元素和 CSS 样式将元素缩小到像素大小来渲染单个像素,但这显然无法为 3D 模型的经典扫描转换提供足够的性能。渲染单个像素的工作量与 2D 图形的线性尺寸呈二次方关系,这意味着将图形尺寸加倍大约需要四倍的工作量。

然而,<div>元素也勉强提供了一种绘制垂直和水平线的方法,即使仅仅是为了文本周围的边框。其他几位博主(包括 David Betz 和已故的 Walter Zorn)注意到,通过将图形分解为平行的“栅格线”而不是像素,工作量与图形的线性尺寸呈线性关系,这意味着将尺寸加倍只会使工作量加倍。他们通过在<div>s 中在必要时绘制像素在可能时绘制线条相结合,创建了具有合理性能的 JavaScript 2D 图形库。

以下是一个 HTML 页面,通过绘制一个由八条高度线性增加的垂直栅格线组成的小直角三角形来说明线性方法


<style>div{
    background:Black; position:absolute;
    width:9px;
}</style>
<div style="left:10px; height:10px;"></div>
<div style="left:20px; height:20px;"></div>
<div style="left:30px; height:30px;"></div>
...
<div style="left:80px; height:80px;"></div>

CSS 样式表和内联位置和高度声明创建了八个<div>实例,这些实例具有线性增加的左侧坐标和线性增加的高度。此 HTML 页面渲染如图 4 所示。

HTML 中坐标和高度值的线性模式应该是显而易见的。如何编写程序来生成类似的 HTML 页面也应该是显而易见的,该页面不仅可以渲染直角三角形,还可以渲染具有如下方案的图形:只需安排坐标和尺寸值以适当的方式线性增加或减少。以下程序动态生成<div>元素,与静态标记完全相同


<script>
    for(var i = 1; i < 9; i++) with(document)with(body.appendChild(createElement("div")).style)
    {
        left = i * 10; height = i * 10;
    }
</script>

诸如 Bresenham 算法之类的标准光栅化算法允许绘制各种图形。任何上过计算机图形学 101 课程的程序员现在都应该已经了解足够多的知识,可以在 HTML 中创建一个完整的可用的 2D 图形库。

对数性能

从像素中获得二次性能,从栅格中获得线性性能是可能的。我们能做得比线性更好吗?Lau 发现了对数性能,这意味着将图形的线性尺寸加倍仅需要恒定数量的额外工作,通常只是一到两次对图元的调用。对数效率远高于线性效率。这种差异与二分查找和线性查找之间的差异相同。Lau 注意到<div>+ 如果你知道在哪里看,HTML + CSS 有一个微妙隐藏的图元直角三角形。然后他提出了一种将任意三角形分解为对数数量的直角三角形的绝妙方法:他的辉煌技巧。

请注意图 5,HTML 允许通过设置<div>border-XXX颜色来完全独立地渲染的四个边框。将<div>的宽度设置为零会删除文本,只留下四个三角形,如图 6 所示。



是否有可能去掉其中两个三角形?这很简单:将右侧(黄色)边框的宽度设置为零,如图 7 中的“动画”所示,并类似地缩小底部(蓝色)边框使其也消失,如图 8 所示;这只留下一个绿色和一个红色的直角三角形。



现在,将剩余边框颜色之一设置为“透明”会在底层浏览器的渲染引擎的原生效率下渲染单个直角三角形,据推测效率非常高(图 9)。

将左侧和底部边框的宽度设置为零,并将其他适当的边框设置为透明——Lau 方法的直接扩展——为所有四种类型的直角三角形生成 HTML 图元,如图 10 所示。

假设此时有一个 JavaScript 函数drawRightTriangle(P1, P2, P3)可以渲染这些直角三角形中的任何一个,给定(坐标)三个顶点。细节是乏味且毫无启发的,但足以说明实现中的四个分支中的每一个都必须设置底层<div>标记的正确属性,可以直接设置也可以通过 CSS 样式类设置。

现在我们有了真正快速的直角三角形,但是任意三角形的对数性能在哪里呢?将三角形尺寸加倍意味着只需要额外调用一到两次直角三角形图元?Lau 的原始代码在风格上是迭代的,但底层的递归描述是优雅的,如下所示

考虑一个任意三角形;根据三角形的定义,三个顶点并非都在同一条线上。只有两种情况需要考虑:要么有一条水平边,要么没有(图 11)。如果有一条水平边,则跳到下一段。如果没有水平边,则使用一条水平线将三角形切割成两个三角形,每个三角形都有一条水平边。切割三角形意味着计算一个新点的坐标,P4,如图 12 所示。



然而,新点P4y新点坐标与新点中间P2的坐标相同——即,P4.y == P2.y——并且新点的x——并且新点的坐标与底点的坐标的距离成正比,就像新点的新点坐标与底点的新点坐标的距离成正比

P4.x == P3.x+(P1.x-P3.x)((P4.y-P3.y)/(P1.y-P3.y))

伪代码,假设P1, P2,P3按向下,递增-y顺序排列,如下


function drawTriangleWithoutHorizontalLeg(P1, P2, P3)
{ ... 根据上述公式计算 P4 ... ;
    drawTriangleWithOneHorizontalLeg(P1, P2, P4);
    drawTriangleWithOneHorizontalLeg(P3, P2, P4);
}

还有一个最终函数要编写drawTriangleWithOneHorizontalLeg。回想一下,具有一条水平边的三角形有两种类型:向下悬挂和向上站立。它们完全对称,因此让我们仅针对向上站立的三角形研究最后步骤。有三种可能的情况

* 尖端位于两个底顶点之间——一个锐角三角形。

* 尖端正好位于两个底顶点之一的正上方——一个直角三角形。

* 尖端位于底边段的右侧或左侧——一个钝角三角形。

如果是锐角,则将三角形垂直切割成两个直角三角形,并宣告完成!如果是直角,那么,它是一个直角三角形,完成!如果是钝角,则将三角形垂直切割成一个直角三角形(完成)和一个钝角三角形(递归调用drawTriangle)。太棒了!(见图 13。)

为了避免第三种情况下的无限递归,如果三角形太小——例如,小于一个像素,你也必须停止。从这个描述来看,所有角落情况都已涵盖,任何程序员都应该能够编写一个正确且性能良好的实现。当所有递归都触底时,三角形的分解如图 14 所示,这就是 Lau 最初绘制的内容。

他的算法将任意三角形分解为一个或两个带有水平边的三角形;我们称这些三角形为对齐三角形。它将任何锐角对齐子三角形分解为两个对齐的直角三角形,并将任何对齐的钝角子三角形分解为递归数量的对齐直角三角形。在数学上,这个递归数量是无限的。在计算上,因为你只能在具有有限像素尺寸的屏幕上渲染数学结构的有限近似值,所以这个数量与图形的大小呈对数关系。

请注意以下小改进:可以通过将与<div>的对齐边相对的边框宽度设置为零,并将目标三角形两侧的边框颜色设置为透明颜色来直接渲染锐角对齐三角形,如图 15 所示。

另请注意,似乎不可能将钝角对齐三角形分解为有限数量的锐角对齐三角形。Marc Levy 提供了以下论证草图:如果存在有限数量,那么你可以通过按大小对其进行排序来找到最小的一个。考虑最小的一个,并从其离原始三角形底边最远的顶点绘制一条水平切割线。残余部分是一个类似于原始三角形的三角形,从而留下一个更小版本的原始问题,其中存在更小的锐角对齐三角形。然而,我们假设我们拥有最小的一个,因此该前提一定是错误的:不存在最小的一个;因此,不存在有限数量的它们。

从茶壶到三角形再到返回

既然你知道如何在屏幕上的任何地方绘制任何三角形,你如何获得茶壶呢?首先,从公共领域以一个有据可查的格式 OFF (http://segeval.cs.princeton.edu/public/off_format.html) 获取一些数据,如表 1 所示。

使用免费的图形工具来抽取数据(将其裁剪到可管理的大小),以获取表 2 中的数据。

然后通过编辑器宏或简单的脚本将数据转换为 JavaScript。图形管道的最终成分是背面剔除、Z 排序、四元数定向和一个用于旋转对象的运动学库。添加一点弹簧-阻尼器物理效果,你可以让你的茶壶弹跳、变形、破碎和重组。所有这些示例的代码都可以在 https://github.com/gousiosg/teapots 中找到。

在正确的支点施加正确的杠杆,相对容易做到令人惊叹的事情,例如在 HTML 和 CSS 中渲染经典的茶壶。或者,用黑客词典的话来说,我们可以从将可编程系统的能力扩展到超出其设计者的原始意图之外获得极大的乐趣。

喜欢它,讨厌它?请告诉我们

[email protected]

Brian Beckman 现在在 Bing 的地图和信号部门工作,自 1992 年以来在微软担任过许多职位,从 Crypto (SET) 到 Biztalk 再到函数式编程研究。他在 1984-1989 年在 Caltech Hypercube 上编写了第一个版本的 Time Warp 操作系统。他拥有普林斯顿大学天体物理学博士学位(1982 年),并已申请 80 多项专利,其中 30 项已获授权。

Erik Meijer ([email protected]) 在过去 15 年中一直致力于“云民主化”。他最出名的工作可能是他在 Haskell、C# 和 Visual Basic 语言方面的工作,以 Javascript 作为汇编语言,以及他对 LINQ 和 Rx(响应式框架)的贡献。他是 TUDelft 的兼职云编程教授,并在微软运营云可编程团队。

© 2013 1542-7730/13/0100 $10.00

acmqueue

最初发表于 Queue vol. 11, no. 2
数字图书馆 中评论本文





更多相关文章

Shylaja Nukala, Vivek Rau - 为什么 SRE 文档很重要
SRE(站点可靠性工程)是一种职位职能、一种思维模式以及一组工程方法,用于使 Web 产品和服务可靠运行。SRE 在软件开发和系统工程的交叉点运作,以解决运营问题并设计解决方案,以可扩展、可靠和高效的方式设计、构建和运行大规模分布式系统。成熟的 SRE 团队可能拥有与许多 SRE 职能相关的定义明确的文档体系。


Taylor Savage - Web 组件化
在当今的软件工程中,没有哪项任务能像 Web 开发那样艰巨。一个典型的 Web 应用程序规范可能如下所示:该应用程序必须跨各种浏览器工作。它必须以 60 fps 的速度运行动画。它必须立即响应触摸。它必须符合一组特定的设计原则和规范。它必须在几乎所有可以想象的屏幕尺寸上工作,从电视和 30 英寸显示器到手机和手表表面。它必须在长期内得到良好的工程设计和可维护性。


Arie van Deursen - 超越页面对象:使用状态对象测试 Web 应用程序
Web 应用程序的端到端测试通常涉及使用诸如 Selenium WebDriver 之类的框架与 Web 页面进行棘手的交互。隐藏此类 Web 页面复杂性的推荐方法是使用页面对象,但是首先需要回答一些问题:在测试 Web 应用程序时应该创建哪些页面对象?您应该在页面对象中包含哪些操作?给定您的页面对象,您应该指定哪些测试场景?


Rich Harris - 拆除准入门槛
一场战争正在 Web 开发领域展开。一方是工具制造者和工具使用者的先锋,他们以破坏糟糕的旧观念(在这个环境中,“旧”意味着任何在一个多月前在 Hacker News 上首次亮相的东西)以及关于转译器和类似物的喧嚣辩论而蓬勃发展。





© 保留所有权利。

© . All rights reserved.