前言
最近在捣鼓 Three.js 相关知识,同时觉得微信小游戏是个不错的试错场呢。来个 2048 小游戏踩踩坑。
先来一张 场景图
乍一看,感觉就是 2D 的么,与 Three.js 没半毛线关系。请仔细看,有阴影,有透明度,有立体感,一切都在细节中,3D 的没错。
抢先体验请扫码
这里吐槽一哈,微信部分版本,进游戏部分机型可能会闪退,具体原因不详。
Three 开发前置条件
- 这里运行环境实在微信小游戏。如果浏览器环境,用谷歌准没错, 具体兼容性,请查看
https://caniuse.com
, - 假定你一定程度上掌握了 JavaScript,最好有一定三维图形学知识。
基本概念
- 场景:提供一个带三维坐标轴的空间
- 相机: 相当于人的眼睛,就是视角。相机分两种,正投影相机
OrthographicCamera
和透视投影相机PerspectiveCamera
。 针对不同应用的三维场景需要使用不同的投影方式,比如平面领域常常采用正投影(平行投影), 游戏场景往往采用透视投影(中心投影) - 光源 Threejs 虚拟光源是对自然界光照的模拟。光源分环境光(
AmbientLight
), 平行光(DirectionalLight
), 半球光(HemisphereLight
),点光源(PointLight
),平面光光源(RectAreaLight
),聚光灯(SpotLight
)等等 - 几何体:模型的几何形状,如:立方体,圆柱体,或是 自定义几何体之类。three.js 封装了一些几何模型 立方几何体(
BoxGeometry
),圆柱几何体(CylinderGeometry
)等 - 材质:几何体的外观。three.js 封装了一些材质,如具有镜面高光的光泽表面的材质(
MeshPhongMaterial
),没有镜面高光的网格材质(MeshLambertMaterial
)等等 - 纹理:材质表面的贴图, 可以是图片,也可以是
canvas
绘制出来的图形 - 渲染器 就是将场景相机光照以及模型以某方式结合并呈现出来
相信上面的概念,足够用了。
场景实例
- 先
new
场景,我这里是new
了两个场景,游戏界面需要立体感,采用中心投影,其余分数等对象,需要平铺在界面上,采用平行投影。中心投影场景中添加辅助线,灯光和背景色。
createScene() {
this.scene = new THREE.Scene();
this.hudScene = new THREE.Scene();
const axesHelper = new THREE.AxesHelper(1000);
const lights = this.createLight();
Object.keys(lights).forEach((value) => {
this.scene.add(lights[value]);
});
this.scene.add(axesHelper);
this.scene.background = new THREE.Color(0x3498db);
}
- 再
new
相机,也是两个,一个正投影相机, 一个透视相机。设置朝向,相机位置,以及聚焦位置。
createCamera() {
this.camera = new THREE.PerspectiveCamera(
45,
screenWidth / screenHeight,
0.1,
5000
);
this.camera.up.set(0, 1, 0);
this.camera.position.set(0,0,2000);
this.camera.lookAt(this.scene.position);
}
createHudCamera() {
this.hudCamera = new THREE.OrthographicCamera(
-screenWidth / 2,
screenWidth / 2,
screenHeight / 2,
-screenHeight / 2,
-1000,
1000
);
this.hudCamera.up.set(0, 1, 0);
this.hudCamera.position.set(0, 0, 1000);
this.hudCamera.lookAt(new THREE.Vector3(0, 0, 0));
}
new
光源,这里设置了环境光和平行光,其中平行光可以设置阴影。
createLight() {
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
directionalLight.position.set(400, 100, 500);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 1000;
directionalLight.shadow.camera.left = -500;
directionalLight.shadow.camera.right = 500;
directionalLight.shadow.camera.top = 500;
directionalLight.shadow.camera.bottom = -500;
directionalLight.shadow.mapSize.width = 3024;
directionalLight.shadow.mapSize.height = 3024;
return {
ambientLight: new THREE.AmbientLight(0xffffff, 0.7),
directionalLight,
};
}
new
渲染器
createRenderer() {
let { devicePixelRatio, innerWidth, innerHeight } = window;
this.renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
precision: "highp",
});
this.renderer.setClearColor(0xffffff, 1);
this.renderer.setPixelRatio(devicePixelRatio);
this.renderer.setSize(innerWidth, innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.autoClear = false;
}
- 渲染
animate() {
this.renderer.render(this.scene, this.camera);
this.renderer.render(this.hudScene, this.hudCamera);
requestAnimationFrame(() => {
this.animate();
});
}
创建模型,这里纹理运用 canvas 绘制,
// 这里创建平面模型
createPlane() {
const geometry = new THREE.PlaneGeometry(width, length);
const material = new THREE.MeshPhongMaterial({
color: 0xdddddd,
specular: 0xdddddd,
map: this.texture(),
});
this.instance = new THREE.Mesh(geometry, material);
this.instance.name = "plane";
// 设置阴影
this.instance.castShadow = true;
this.instance.receiveShadow = true;
}
// 创建纹理
texture() {
let w = width;
const canvas = wx.createCanvas();
canvas.width = canvas.height = w;
const context = canvas.getContext("2d");
...
let map = new THREE.CanvasTexture(canvas);
map.minFilter = THREE.LinearFilter;
return map;
}
添加事件
- 点击事件,将需要添加点击事件的对象放在一个
group
里,用户点击屏幕的时候,threejs
提供Raycaster
实例,会从触碰点会发射一条“激光”,激光扫到的所有对象记录在返回的数组里。
getEventObject(event, camera = null, group = []) {
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
mouse.x = (event.clientX / screenWidth) * 2 - 1;
mouse.y = -(event.clientY / screenHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(group, true);
return intersects;
}
- 滑动事件,通过微信提供的
onTouchStart
和onTouchEnd
, 获取起始坐标点和结束坐标点,然后通过滑动距离和角度来判断滑动方向。
fn(callback) {
if (this.endId === this.startId) {
let w = this.xt - this.x0;
let h = this.yt - this.y0;
let k = h / w;
if (k > 2 || k < -2) {
if (h < -20) callback(0); /*向上*/
if (h > 20) callback(1); /*向下*/
}
if (k < 0.5 && k > -0.5) {
if (w < -20) callback(2); /*向左*/
if (w > 20) callback(3); /*向右*/
}
}
}
优化性能
- 几何体,材质,纹理等尽量共用,考虑使用记忆函数
- 移除模型时,及时调用
dispose()
, 包括几何体,材质及纹理等
总结
通过 Three.js 来构建一个小游戏场景, 过程比较简单的,大概四步走。
- 新建场景,打好灯光,设置摄像机位置
- 将几何图形和材质的组合,构建出不同的模型
- 在场景中将模型渲染出来。一个 3D 界面就出来了
- 根据业务需求添加事件,调整场景
体验扫码