(上一篇 part1)
在编写react3d库的其他核心组件之前,我们再啰嗦几句。 part1 中比较特别的地方是getChildContext的使用。
我们知道React 中的数据流由父元素向子元素进行单向传递。这种传递一般是通过“属性”(attributes)进行“显式”的传递。但是在某些场合下,我们希望数据传递是“隐式”的,从而把代码封装的更好。 例如在part1的讨论中, 一个组件始终需要向其子元素传递它所代理的threejs对象,因此我们希望将这种反复出现的传递pattern,以“隐式”的方式进行。 getChildContext 正是为了实现这种数据“隐式”传递的需要。关于React Context,这篇文章的介绍比较详细。
好,言归正传。 我们介绍完 react3d.Object3D
实现的思想后,下面我们将以 react3d.Object3D
作为基础类,构建其他核心组件
首先是整个3d场景,Threejs 中称为 Scene。 出于两点考虑: (1) Scene 是整个场景的根元素 (2) 为了保持简洁性,我们希望 react3d.Scene
也承载创建 canvas 的作用。由于这些特殊性,我们把 Scene 当做独立于 react3d.Object3D
以外的另一个基础类:
import React from "react";
import {Scene as ThreeScene} from "three"
class Scene extends React.Component {
constructor(props){
super(props);
const {width, height, style} = props;
this.obj = new ThreeScene();
this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;
this.canvas.style = style;
}
componentDidMount(){
const box = this.refs.container3d;
box.appendChild(this.canvas);
}
componentWillUnmount(){
const box = this.refs.container3d;
box.removeChild(this.canvas);
}
render(){
const {width, height, style} = this.props;
return <div ref="container3d">{this.props.children}</div>
}
getChildContext() {
return {
parent: this.obj
};
}
}
Scene.childContextTypes = {
parent: React.PropTypes.object
};
export default Scene
3d场景的另一个核心组件是Camera,可以直接继承 react3d.Object3D
:
import {PerspectiveCamera} from "three";
import Object3D from "Object3D.jsx";
class Camera extends Object3D {
objContructor(props){
const {fov, aspect, near, far, x, y, z} = props;
const camera = new PerspectiveCamera(fov, aspect, near, far);
return camera;
}
}
export default Camera;
渲染3d场景的另一个重要概念是灯光,我们以 PointLight 为例,显然它也可以直接继承react3d.Ojbect3D
:
import Object3D from "Object3D.jsx";
import {PointLight as ThreePointLight} from "three";
class PointLight extends Object3D {
objContructor(props){
const {color, intensity, distance, decay, x, y, z} = props;
const light = new ThreePointLight( color, intensity, distance, decay);
light.position.set( x, y, z);
return light;
}
}
export default PointLight;
我们可以顺着这个思路,定义其他常见的3d组件,例如 Box,Sphere 等。 so far, so good!
前面的组件中缺少了重要的一环,就是渲染器 renderer。 当然这是故意为之的,因为这里有点麻烦……
在 Threejs 中,渲染器(Renderer)用来直接绘制 canvas,出于惯性思维,我们很自然想把 renderer 当做某种最外层的概念,包住整个Scene (毕竟我们是要渲染整个Scene嘛!)。我们知道 Threejs 中的Renderer定义依赖于scene 和 camera,而camera 和 scene相对于renderer是”内层元素”。 注意React 中对象实体的传递只能从父元素传向子元素,想要逆向传播的话,我们需要使用ref来进行“显式”的引用。这么表述是有点拗口,我们直接上代码来体会一下,
import React from "react"
import {WebGLRenderer} from "three"
import Scene from "Scene.jsx"
import Camera from "Camera.jsx"
class Space extends React.Component {
constructor(props){
super(props)
this.frameId = null
this.renderFrames = this.renderFrames.bind(this)
}
renderFrames(){
const scene = this.scene;
const camera = this.camera;
const webGLRenderer = this.webGLRenderer;
webGLRenderer.render(scene, camera);
this.frameId = requestAnimationFrame(this.renderFrames)
}
componentDidMount(){
this.webGLRenderer = new WebGLRenderer({antialias: true, canvas: this.canvas});
this.renderFrames();
}
componentWillUnmount(){
cancelAnimationFrame(this.frameId);
}
render(){
const {width, height} = this.props
return <Scene ref={ref => { if(ref){this.scene = ref.obj; this.canvas = ref.refs.canvas;} }}
width={width} height={height} style=>
<Camera ref={ref => { if(ref){this.camera = ref.obj} }} fov={80} aspect={width/height} near={0.5} far={250} z={150}/>
</Scene>
}
}
把Renderer当做某种最外层的概念,我们很可能写出类似上面的代码,注意这里,为了使renderer能够获得的scene和camera(当然也包括canvas),我们使用ref把他们从子组件中提取出来,然后在父组件的componentDidMount阶段执行渲染操作。显然,ref的存在让代码显得非常啰嗦,封装做得很糟糕,并且我们还要自己重写 render loop 等常规逻辑(通过 render loop不断重绘canvas,实现动画是threejs中的一个常识)
怎样跳出这个坑?
只要打破惯性思维,办法就很简单:那就是不要把 renderer 当做外层的概念,恰恰相反,我们应该把 renderer 放到 camera的层级甚至更里面。这样scene、camera 就可以通过 React 向下的数据流自然传递给 renderer。 于是我们将上面的Scene和Camera组件分别改造为:
Scene 组件
import React from "react";
import {Scene as ThreeScene} from "three"
class Scene extends React.Component {
constructor(props){
super(props);
const {width, height, style} = props;
this.obj = new ThreeScene();
this.canvas = document.createElement("canvas");
this.canvas.width = width;
this.canvas.height = height;
this.canvas.style = style;
}
componentDidMount(){
const box = this.refs.container3d;
box.appendChild(this.canvas);
}
componentWillUnmount(){
const box = this.refs.container3d;
box.removeChild(this.canvas);
}
render(){
const {width, height, style} = this.props;
return <div ref="container3d">{this.props.children}</div>
}
getChildContext() {
return {
parent: this.obj,
canvas: this.canvas
};
}
}
Scene.childContextTypes = {
parent: React.PropTypes.object,
canvas: React.PropTypes.object
};
export default Scene
Camera 组件
import {PerspectiveCamera, WebGLRenderer} from "three";
import Object3D from "./Object3D.jsx";
import React from "react";
class Camera extends Object3D {
objContructor(props){
const {fov, aspect, near, far, x, y, z} = props;
const camera = new PerspectiveCamera(fov, aspect, near, far);
camera.position.x = x;
camera.position.y = y;
camera.position.z = z;
this.frameId = null;
this.renderFrames = this.renderFrames.bind(this);
return camera;
}
objDidMount(){
const canvas = this.context.canvas;
this.scene = this.context.parent;
this.webGLRenderer = new WebGLRenderer({antialias: true, canvas});
this.renderFrames();
}
objWillUnmount(){
cancelAnimationFrame(this.frameId);
}
renderFrames(){
const camera = this.obj;
const scene = this.scene;
const webGLRenderer = this.webGLRenderer;
webGLRenderer.render(scene, camera);
this.frameId = requestAnimationFrame(this.renderFrames)
}
}
Camera.childContextTypes = {
parent: React.PropTypes.object
};
Camera.contextTypes = {
parent: React.PropTypes.object,
canvas: React.PropTypes.object
};
export default Camera;
也就是说改造以后,Scene 将 canvas 实例以 context 形式传递给子元素; 而Camera 不仅承担 Threejs中的类似作用,同时还承担了渲染画布实现render loop的作用。
有了上述核心组件后,一个典型的3d场景,可以描写为以下形式:
<Scene>
<Camera/>
<PointLight/>
<Object3D />
</Scene>
到目前为止,react3d
已通过隐式传递的方式帮我们实现了3d模型的组装、render loop、以及状态描述以及生命周期的管理。
(下一篇 part3)