杂事2:Three.js 试水和其他小事情

Three.js 尝试修改两个示例,翻译黑历史

昨天(10日)凌晨试了一下 Three.js。搜索的时候看到一篇文章原文地址已经失效),其中除了官方示例的链接外,还有一位大神的示例库

友情提示:运行 Three.js 的预览页面的时候,请保证是在服务器上运行的,而不是直接从桌面上用浏览器打开的。还是同源策略的锅吧。

Texture from Canvas 示例看来很适合作为文字绘制的测试。

Texture with Canvas

不过原先的结果有点不符合我们的需求。

首先,明显文字的周围出现了模糊。其实联想一下就能知道,这是透视投影(perspective projection)导致的。因此我们要改为正交投影(orthographic projection)。

//camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
camera = new THREE.OrthographicCamera(-SCREEN_WIDTH / 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, -SCREEN_HEIGHT / 2, NEAR, FAR);

现在就能按照“平面”的视角来投影 <canvas> 的内容了,和普通的2D绘图没什么差别。

第二还要测试一个“动态”的特征。我们知道如果创建了一个 CommentField,那么随着其文字内容的变化,所绘制的内容也要发生变化。

// canvas contents will be used for a texture
var texture1 = new THREE.Texture(canvas1)
texture1.needsUpdate = true;

var material1 = new THREE.MeshBasicMaterial({map: texture1, side: THREE.DoubleSide});
material1.transparent = true;

var mesh1 = new THREE.Mesh(
        new THREE.PlaneGeometry(canvas1.width, canvas1.height),
        material1
    );

从源代码中我们可以看到,<canvas> 被用来创建了一个贴图(texture),然后这个贴图被用来生成了一个材质(material)。(抱歉,在和 Source Engine 干架的时候我也经常弄混这两个词,不过V社的用语总是正确的……)

Texture 有一个有趣的东西,Texture.needsUpdate。看名字应该都知道是干什么的吧。大概能猜到会做出什么吧。我们针对此进行修改。

var textList = ["Hello, world!", "你好,世界!", "こんにちは、世界!"];
var index = 0;

document.onkeydown = function (ev) {
    if (ev.keyCode == "Z".charCodeAt(0)) {
        index = (++index) % 3;
        context1.font = "Bold 40px SimHei";
        context1.fillStyle = "rgba(255,0,0,0.95)";
        var c = context1.measureText(textList[index]);
        canvas1.width = c.width;
        //context.clearRect(0, 0, canvas1.width, canvas1.height);
        context1.font = "Bold 40px SimHei";
        context1.fillStyle = "rgba(255,0,0,0.95)";
        context1.fillText(textList[index], 0, 50);
        texture1.needsUpdate = true;
    }
};

现在试试按下Z键,发现了什么?

你可以在这里看到修改后的示例。

代码修正2见本文最后。


另外一个示例 Lines and Dashed Lines 则是对图元的挑战。做过一点三维编程的同学都知道,在 Three 里画图元(primitives)不是那么简单的。还好 Three.js 帮我们做了不错的封装,有一个 Line。但是看官网的示例:

var material = new THREE.LineBasicMaterial({
    color: 0x0000ff
});

var geometry = new THREE.Geometry();
geometry.vertices.push(
    new THREE.Vector3( -10, 0, 0 ),
    new THREE.Vector3( 0, 10, 0 ),
    new THREE.Vector3( 10, 0, 0 )
);

var line = new THREE.Line( geometry, material );
scene.add( line );

没错,画出的线就属于 Geometry,为了渲染出来我们需要指定一个 Material。翻一下文档就知道,最接近我们所需的无非是 LineBasicMaterialLineDashedMaterial。而且,在 LineBasicMaterial 的属性说明里,可以看到这么一条:

.linewidth

Controls line thickness. Default is 1.

Due to limitations in the ANGLE layer, on Windows platforms linewidth will always be 1 regardless of the set value.

ANGLE(Almost Native Graphics Layer Engine)就是一个 Windows 上的 OpenGL 兼容层,调用 DirectX 实现具体功能。(有点类似 Cygwin。)

题外话时间。ANGLE 里确实存在两个 issue(#119#334)请求开发者解决这个bug,但是此bug一直存活,到现在还在影响着 Chrome 和 Firefox。有牛人想出了用 Shader 模拟的折线渲染的方法或者另一种用 Vertex Shader 实现的通杀,但是对于我们的需求来说使用比较困难……如何检测呢?这里查看“ANGLE”一项的值即可。强制 Chrome 和 Firefox 启用原生 OpenGL 的方法也有(--use-gl=desktopwebgl.disable-angle),但是不能让用户来干这事吧(要假设他们不懂这些)。

回到正题。对于这个示例,我们只改变其中的一点点创建部分的代码就可以了。

//var lineMaterial = new THREE.LineBasicMaterial({color: 0xcc00ff});
var lineMaterial = new THREE.LineBasicMaterial({color: 0xcc00ff, linewidth: 4});

在启用 ANGLE 和使用原生 OpenGL 的浏览器中,对比是很明显的:

Lines under ANGLE

Lines under Native OpenGL

对于某种要批量改变样式的调用(抱歉脑子抽了,就当是假想的好了),我们可以尝试这样的东西:

var lm;

// ...

function init() {
    // ...
    var lineMaterial = new THREE.LineBasicMaterial({color: 0xcc00ff, linewidth: 4});
    lm = lineMaterial;
    // ...
}

// ...

document.addEventListener('keypress', function (ev) {
    if (ev.keyCode == 'z'.charCodeAt(0)) {
        //console.log(lm.color);
        //console.log(lm.color.getHex().toString(16));
        // getHex() 返回的总是正数,所以不能用大于/小于零来判断
        lm.setValues({'color': (lm.color.getHex() < 0x800000 ? 0xff0000 : 0x00ff00)});
        lm.needsUpdate = true;
    }
}, false);

你可以在这里看到修改后的示例。


正在我为此次翻译质量比第一次(黑历史)好的时候,突然发现歌词原文出现了多出错误。起因推断是网易从其他地方挖歌词的时候,在放到数据库之前某一步(无论是网易自己还是歌词源)错误地应用了繁体转简体的过程。

例如第一句,现在在网易上是这样的:

“キマグレ”“自分胜手”“強がり”

实际上,最初的情况是这样的:

“キマグレ”“自分胜手”“强がり”

在翻译的时候就觉得哪里不对,改了一下。但是,正确的应该是这个呢:

“キマグレ”“自分勝手”“強がり”

勝手(かって)。读的时候倒没问题,估计是我脑子里又做了自动转换吧……

完了,本来想洗刷黑历史的,结果成了新的黑历史。


恋选的情节并不是催泪向的。这点和 Key 社、FAVORITE 社的一堆作品不一样。为什么突然想到了《放晴后必定是油菜花绽放的好天气》(「晴のちきっと菜の花びより」,昵称“晴菜花”)……总之这些都是虐人用的。如果要我玩一个游戏还要被这个游戏摧毁心理防线,这算啥嘛!因此我主要玩的都是白糖系的,要是结局不给个 HE 我就要给你打低分。——最近两个游戏,GTA 5(选择了三人结局)和 Splinter Cell: Blacklist,结局都是大家乐呵呵(反派就不那么想了),尽管有着很强的个人英雄主义在。


今天继续降温,现在不到20度。


代码修正2:

你也许会发现,按照上面对 Texture from Canvas 的修改,字体会出现长宽比不同的现象。那是因为我们忘记设置了网格(Mesh),网格是按照固定大小的 PlaneGeometry 建立的,如果渲染所用材质大小小于其大小则会被拉伸。

PlaneGeometry 有四个属性:widthheightwidthSegmentsheightSegments。但是文档也说了:

Each of the contructor parameters is accessible as a property of the same name. Any modification of these properties after instantiation does not change the geometry.

因此不要想着直接设置其属性,还是乖乖建立新的吧。修改之后核心如下:

// canvas contents will be used for a texture
var texture1 = new THREE.Texture(canvas1)
texture1.needsUpdate = true;

var material1 = new THREE.MeshBasicMaterial({map: texture1, side: THREE.DoubleSide});
material1.transparent = true;
var plane = new THREE.PlaneGeometry(canvas1.width, canvas1.height);

var mesh1 = new THREE.Mesh(
        plane,
        material1
);
mesh1.position.set(0, 50, 0);
scene.add(mesh1);

var textList = ["Hello, world!", "你好,世界!", "こんにちは、世界!"];
var index = 0;

document.onkeydown = function (ev) {
    if (ev.keyCode == "Z".charCodeAt(0)) {
        index = (++index) % 3;
        context1.font = "Bold 40px SimHei";
        context1.fillStyle = "rgba(255,0,0,0.95)";
        var c = context1.measureText(textList[index]);
        canvas1.width = c.width;
        plane.dispose();
        plane = new THREE.PlaneGeometry(canvas1.width, canvas1.height);
        mesh1.geometry = plane;
        //context.clearRect(0, 0, canvas1.width, canvas1.height);
        context1.font = "Bold 40px SimHei";
        context1.fillStyle = "rgba(255,0,0,0.95)";
        context1.fillText(textList[index], 0, 50);
        texture1.needsUpdate = true;
    }
};

现在就正常了。这里是第二次修改的演示。

我们添加了一个引用 plane 来管理文字所在的 PlaneGeometry

问题来了:同学们应该很清楚,如果大资源没有释放,就会造成内存泄露。CLR 里经常会遇到这种事,要很仔细实现 IDisposable 范式,也要注意使用 using,之类的东西。在这个例子中,如果更新函数是放在了 requestFrameAnimation() 中,那就蛋疼了,内存估计哗哗哗就开始消耗;且不说 JavaScript 对象的引用没断,其后的资源也没释放呢。

所以我搜索了一下。两个回答(1/2)提到了 dispose(),以及数组元素请 delete(且不保持其他引用)这个细节。

内存压力测试在 Three.js 的官方示例中有。测试1测试2这里是内存测试2的代码。

分享到 评论