Threejs 是一个非常流行 Javascript 3d 引擎,我们可以在Threejs官网找到非常多酷炫的3d案例。 对于开发者而言,Threejs 对 webGL 优雅的封装让我们可以用面向对象的思路进行3d建模。 其特点可以概括为:
React 是一个非常灵活高效的模块化前端框架。 下面是一些React的主要特点:
这些特点对于组织复杂应用都是非常重要的。
对于复杂的3d应用场景,我们一方面需要Threejs专业的3d绘制能力,同时我们也需要React普适的模块组织能力,尤其是当canvas 3d场景和其他DOM元素有耦合关系的时候。
由于Threejs 和 React都是“框架型”js库(区别于“工具型”js库,如jquery, underscore),即开发者使用它们时,代码需要服从某种既定的大致结构。因此,怎样系统地将二者有机组合起来使用,并不是一件显而易见的事情。
本文将试图创造一个js “中间件”,用来连接 Threejs 和 React。 由于 Threejs 和 React 作为“重型”js库,各自已有足够多的“轮子”和“锁链”,我们希望这个“中间件” 是轻量的,即我们希望这个“中间件”对原有框架的底层和使用习惯不做任何改变,同时也尽量避免重复造轮子,因为我们希望使用这个“中间件”时,不会引入其他的复杂性或学习成本。
这里先预览一下采用“中间件”实现的一个简单 demo:
这个demo,对应的核心代码是
相信对于熟悉Threejs的开发者,这段jsx片段的含义是非常直观的。
Demo 是用该“中间件”实现的一个更复杂的案例。
联结 Threejs 和 React 的首要问题是怎样实现Threejs中对象的“组件化”, 前面我们提到 Threejs 中也可以通过Object3D的add方法实现简单的“组件化”结构,例如要绘制一个“行星系统”, Threejs中通常可以这样写
但是这种 “组件化” 和 React的组件不是一个体系,即无法像jsx那样用xml声明的形式更加直观地描述组件之间的嵌套与参数依赖关系,同时threejs的“组件”也缺少对其状态的描述和对其生命周期的管理,例如我们希望当某个3d模型被创建时,该模型可以被自动add进来,当不需要该模型时,我们希望该模型可以“自动”被remove掉。 这些在React 组件中是可以轻易实现的,因此我们首先构造一个 Three.Object3D 在React中的“代理”组件,它有一个obj属性,对应其被代理的Threejs.Object3D 实例。当该组件被“创建时” (componentDidMount),其被代理的obj 立刻被add 到其父组件代理的obj,同样当该组件被“销毁”时,该组件代理的obj自动从父组件代理的obj中被remove掉,具体代码如下:
import React from "react";
import {Object3D as ThreeObject3D} from "three"
class Object3D extends React.Component {
constructor(props) {
super(props);
this.obj = this.objContructor(props);
}
componentWillReceiveProps(nextProps) {
this.objWillReceiveProps(nextProps);
}
componentDidMount(){
const parent = this.context.parent;
parent.add(this.obj);
this.objDidMount();
}
componentWillUnmount(){
const parent = this.context.parent;
parent.remove(this.obj);
this.objWillUnmount();
}
render(){
return <span>{this.props.children}</span>;
}
getChildContext() {
return {
parent: this.obj
};
}
objContructor(props) {
const obj = new ThreeObject3D();
return obj
}
objWillReceiveProps(nextProps){}
objDidMount(){
}
objWillUnmount(){}
}
Object3D.setTypes = (obj, opt) => {
opt = opt || {};
if(opt.hasChild) {
obj.childContextTypes = {
parent: React.PropTypes.object
};
}
obj.contextTypes = {parent: React.PropTypes.object};
}
Object3D.setTypes(Object3D, {hasChild: true});
export default Object3D;
以后当我们需要创建一个3D对象的时候,我们只需要继承这个基础类,例如:
import {Object3D} from "react3d.jsx";
import {BoxGeometry, MeshBasicMaterial, Mesh} from "three"
const createCube = () => {
const geometry = new BoxGeometry( 15, 15, 15 );
var material = new MeshBasicMaterial( {color: 0xffffff, wireframe: true};
var cube = new Mesh( geometry, material );
cube.name = "cube";
return cube;
}
class Cube extends Object3D {
objContructor(props) {
this.id = "cube";
return createCube();
}
};
Object3D.setTypes(Cube, {hasChild: true});
export default Cube;
注意:这里我们创造Cube对象时,没有重新创造 material 或者 geometry 对应的React 组件,因为material 和geometry 是描述 Object3D 的,自身并没有模块化特征,因此没有必要为了“React化” 而 “React 化”。 相反,这里认为,直接在 objContructor方法内用js代码的方式创建 obj 是自然的,因为这样最大程度上保留了threejs开发者以前的代码习惯(只在需要的时候才“React化”),threejs开发者可以像以前一样在这个方法内专注于3d模型的定义。
(下一篇 part2)