这里我们来实现一个坦克,那么这个坦克的结构为
- 场景
- 坦克
- 坦克主体,也就是驾驶舱,我们把所有的配件都装到驾驶舱上
- 六个轮子
- 炮台
- 炮筒
- 炮筒上面放个摄像机
- 炮筒
- 坦克上面放个摄像机
- 坦克主体,也就是驾驶舱,我们把所有的配件都装到驾驶舱上
- 两个灯光
- 一个从顶部照射
- 一个从正Z方向照射来补光,要不然一个灯太暗了
- 地面和坦克运行路线
- 坦克的目标
- 坦克
开头老样子,咱们建场景对象、摄像机对象、渲染器对象、渲染器函数、响应函数、罗里吧嗦的,创建一个main函数,作为程序入口。
渲染器函数加了一个time参数,为当前渲染器从创建到现在的时间,会逐步增加的,自己测一下。
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 50;
let c = document.getElementById("c") as HTMLCanvasElement;
let renderer = new THREE.WebGLRenderer({
canvas: c,
});
renderer.setClearColor("#C4C4C4");
function ResetSize() {
let ratio = window.devicePixelRatio
let w = c.clientWidth * ratio;
let h = c.clientHeight * ratio;
let needReset = c.width !== w || c.height !== h;
if (needReset) renderer.setSize(w, h, false);
return needReset;
}
function render(time) {
requestAnimationFrame(render);
if (ResetSize()) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
}
function main() {
}
main();
requestAnimationFrame(render);
坦克壳子
这里的壳子也就是说是坦克外面包的Object3D,为的就是把坦克的东西给包起来,形成一个整体。
我懒,这里把坦克画成一个颜色的。所以我把TankMater提出来不用每次都创建了
let wireframe = false;
let TankMater = new THREE.MeshPhongMaterial({
color: "#68663D",
wireframe,
});
function createTank() {
let Tank = new THREE.Object3D();
// 这个Body其实就是一个板子我们稍候来看一下
let TankBody = new THREE.BoxGeometry(15, 1.5, 8);
let TankBodyMesh = new THREE.Mesh(TankBody, TankMater);
Tank.add(TankBodyMesh);
return Tank;
}
function main() {
scene.add(createTank());
}
创建灯光,要不然太黑了
记得一定要有创建Object3D的习惯,把你认为是一体的部分包起来,但是具体要不要包,还得看实际情况。
function createLight() {
let light = new THREE.Object3D();
let topLight = new THREE.DirectionalLight("", 1);
topLight.position.set(0, 50, 0);
light.add(topLight);
let zIndexLight = new THREE.DirectionalLight("", 1);
zIndexLight.position.set(5, 5, 5);
light.add(zIndexLight);
return light;
}
function main() {
scene.add(createTank(), createLight());
}
好了,我们现在创建完灯光了,我们可以看到一个野战军绿颜色的板子,我们拿他来当坦克的主体部分,作为坦克的基础结构。
创建地面
地面就是一个简简单单的正方形,比较矮的正方形。
function createGround() {
let Ground = new THREE.BoxGeometry(60, 1, 60);
let GroundMater = new THREE.MeshPhongMaterial({
color: "#67ABB8",
wireframe,
});
let GroundMesh = new THREE.Mesh(Ground, GroundMater);
GroundMesh.castShadow = true;
GroundMesh.receiveShadow = true;
return GroundMesh;
}
function main() {
scene.add(createTank(), createLight(), createGround());
}
castShadow和receiveShadow两个属性接下来创建轮子的时候会讲
什么?看着难受?板板整整的?那换个方向
// 把相机定位到X:-50,Y:40,Z:45的位置,这样你的视角是在远处的左侧向上一点的位置
camera.position.set(-50, 40, 45);
// 从相机当前位置看向0,0,0坐标点,也就是场景的正中心
camera.lookAt(0, 0, 0);
可以参照这个改动一下X,Y,Z的位置,可以有更多视角哦~
什么?坦克那个板子有一半到地面里去了?
我们来吧Tank对象抬上来
为什么抬板子厚度的两倍?我们是为了给轮子留点空间
Tank.position.y = 3;
创建6个轮子
我们用圆柱来做轮子,比较矮的圆柱,只不过圆柱的的段数比较少,所以我们看起来像方的,然后我们让圆柱旋转180°,让他从横着的变成竖着的。
因为我们要在render方法中让轮子们旋转起来,所以我们把wheel提出来成为一个数组,然后我们通过计算来让一侧三个轮子。
let wheels: Array<THREE.Mesh> = [];
function createWheels() {
let size = 2.5;
for (let i = 0; i < 6; i++) {
let wheel = new THREE.CylinderGeometry(size, size, 1.5, 8);
let wheelMesh = new THREE.Mesh(wheel, TankMater);
wheelMesh.rotation.x = Math.PI / 2;
wheelMesh.position.set(
(i % 3) * (size * 2) + -size * 2,
0,
i >= 3 ? -4 : 4
);
wheels.push(wheelMesh);
}
return wheels;
}
为了让轮子添加到tankBody内部,我们稍微改动一下createTank方法,并且为Tank和TankBody设置可以形成阴影和接收阴影。
function createTank(...child: Array<THREE.Object3D | Array<THREE.Object3D>>){
// ...
child.forEach((c) => {
if (Array.isArray(c)) {
c.forEach((cc) => {
cc.castShadow = true;
cc.receiveShadow = true;
TankBodyMesh.add(cc);
});
} else {
c.castShadow = true;
c.receiveShadow = true;
TankBodyMesh.add(c);
}
});
// ...
}
castShadow:可以形成阴影
receiveShadow:可以接收阴影,也就是说在其上方节点形成的阴影会不会显示在当前节点上面。
对于Array<THREE.Object3D | Array<THREE.Object3D>>这行代码来说,我们知道Mesh是继承与Object3D的,所以我们根据里氏替换原则,我们可以将父类Object3D定为形参,意味着它可以接收所有继承自Object3D的子类实例。
为了让场景可以显示阴影我们还需要进行以下操作
// createLight 方法
zIndexLight.castShadow = true; // 允许该灯光照射出阴影
zIndexLight.shadow.mapSize.width = 2048; // 一个二维矩阵定义阴影贴图的宽度
zIndexLight.shadow.mapSize.height = 2048;// 定义高度
const d = 50;
// 以下距离表示从0点看往哪个方向的距离,现在看的距离是50,你看的越短,那么就显示的越少
// 被裁剪的也越多
// 阴影摄像机左侧距离,如果为0你会发现左侧被裁掉了,也就是不显示了
zIndexLight.shadow.camera.left = -d;
// 右侧距离
zIndexLight.shadow.camera.right = d;
// 顶部距离
zIndexLight.shadow.camera.top = d;
// 底部距离
zIndexLight.shadow.camera.bottom = -d;
// 阴影相机近端面,即这个阴影相机对于灯光坐标的位置,现在就是站在灯光往前加1坐标
zIndexLight.shadow.camera.near = 1;
// 阴影相机远端面,即可以看多远
zIndexLight.shadow.camera.far = 50;
// 阴影贴图偏差,在比较复杂的地方绘制阴影时,比如曲面,从标准的平面深度,添加/减去多少
zIndexLight.shadow.bias = 0.001;
// 渲染器
renderer.shadowMap.enabled = true; // 允许在场景中使用阴影贴图
// 使用百分比-接近滤波(PCF)算法过滤阴影地图,具有更好的软阴影,
// 特别是低分辨率下阴影的渲染,将会有更好的效果
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
我们在render方法中添加代码让轮子转起来
function render(time) {
// ...
wheels.forEach((w) => (w.rotation.y += 0.1));
// ...
}
到现在main方法就变成了
function main() {
scene.add(
createTank(createWheels()),
createLight(),
createGround()
);
}
创建炮台
我们用球体形状,做一个半球体,扣在板子上,然后做成一个类似炮台一样的东西。
function createDome(...child: Array<THREE.Object3D>) {
let domeObj = new THREE.Object3D();
let dome = new THREE.SphereGeometry(5,20,20,0,Math.PI * 2,0,Math.PI / 2);
let domeMesh = new THREE.Mesh(dome, TankMater);
child.forEach((c) => {
c.castShadow = true;
c.receiveShadow = true;
domeMesh.add(c);
});
domeObj.add(domeMesh);
return domeObj;
}
发现对的不是太齐,咱们微调一下
// 方法createWheels
let y = size * 2 - 0.5;
wheelMesh.position.set(
(i % 3) * (size * 2) + -size * 2,
-1,
i >= 3 ? -y : y
);
// 方法createTank
Tank.position.y = 3.5;
修改main方法
scene.add(
createTank(createWheels(), createDome()),
createLight(),
createGround()
);
创建炮筒和炮台上的摄像头和坦克上的摄像头
创建炮筒,这只是个普通的长方体,只不过比较长,但是很矮,很低。
我们将BarrelObj对象定义在外面是因为我们一会要用到,在炮筒外面套一层壳子,是为了让炮筒元素向外平移一点,但是旋转还是按照原来的0,0,0坐标旋转。
可以自己将BarrelObj去掉,也就是不套壳子,然后尝试一下,你会发现旋转的会有偏差,但是现在先别在乎旋转,等一下用到BarrelObj对象时,你再尝试。
let BarrelObj: THREE.Object3D;
function createBarrel(...child: Array<THREE.Object3D>) {
BarrelObj = new THREE.Object3D();
let Barrel = new THREE.BoxGeometry(1.2, 1.2, 10);
let BarrelMesh = new THREE.Mesh(Barrel, TankMater);
BarrelMesh.position.set(0, 0, 8);
BarrelMesh.castShadow = true;
child.forEach((c) => {
BarrelObj.add(c);
});
BarrelObj.position.set(0, 2, 0);
BarrelObj.add(BarrelMesh);
return BarrelObj;
}
// main 方法
scene.add(
createTank(createWheels(), createDome(createBarrel()))
// ...
)
添加炮台摄像头,咱们以后还需要创建别的摄像头,所以我们创建一个createCamera方法,来生成摄像头,并且声明一个PerspectiveCamera类型的数组,来存储这些摄像头。该数组需要在camera变量上方声明,要不然就会出现cameras为undefined的情况。
let cameras: Array<THREE.PerspectiveCamera> = [];
let camera = createCamera();
// ...
function createCamera() {
let c = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
cameras.push(c);
return c;
}
创建炮台摄像头
function createDomeTrackingCamera() {
let DomeCamera = createCamera();
DomeCamera.position.set(0, 1, 3);
DomeCamera.rotation.y = Math.PI;
return DomeCamera;
}
// main 方法
scene.add(
createTank(
createWheels(),
createDome(createBarrel(createDomeTrackingCamera()))
)
// ...
)
创建坦克摄像头
function createTankCamera() {
let tankCamera = createCamera();
tankCamera.position.set(-15, 8, 0);
tankCamera.lookAt(0, 0, 0);
return tankCamera;
}
// main 方法
scene.add(
createTank(
createWheels(),
createDome(createBarrel(createDomeTrackingCamera())),
createTankCamera()
),
// ...
)
我们修改render方法来让这三个摄像头切换观看。
time为毫秒数,1000毫秒等于1秒,那么time / 2000就是2秒会切换一个摄像头。可以调试输出下看一下
function render(time) {
// ...
let currentCamera = cameras[Math.floor((time / 2000) % cameras.length)];
if (ResetSize()) {
const canvas = renderer.domElement;
cameras.forEach((c) => {
c.aspect = canvas.clientWidth / canvas.clientHeight;
c.updateProjectionMatrix();
});
}
// ...
renderer.render(scene, currentCamera);
}
我们先把currentCamera固定位0号机,我们还有别的东西没画呢,就先不让他切换了。
let currentCamera = cameras[0];
创建目标及目标上的摄像头
// 将这四个元素放外面,待会要用到
let targetOrbit: THREE.Object3D;
let targetObj: THREE.Object3D;
let targetMesh: THREE.Mesh;
let targetCamera: THREE.PerspectiveCamera;
function createTarget() {
// 该元素以场景的0,0,0为中心
targetOrbit = new THREE.Object3D();
// 该元素以 targetOrbit 的坐标再偏-20X,8Y为中心
targetObj = new THREE.Object3D();
targetObj.position.set(-20, 8, 0);
let target = new THREE.SphereGeometry(1, 6, 3);
let TargetMater = new THREE.MeshPhongMaterial({
color: "#EE6B3C",
wireframe,
});
targetMesh = new THREE.Mesh(target, TargetMater);
targetCamera = createCamera();
targetObj.add(targetCamera);
targetObj.add(targetMesh);
// 让相机看往场景中心
targetCamera.lookAt(0, 0, 0);
// 将相机定位到目标的后面偏上位置
targetCamera.position.set(-1.5, 2, 0);
targetOrbit.add(targetObj);
return targetOrbit;
}
以下内容对render方法进行修改
让目标围绕坦克旋转起来
targetOrbit.rotation.y += 0.02;
让目标上下左右动并且旋转起来
targetMesh.rotation.x += 0.1;
targetMesh.rotation.y += 0.1;
targetObj.position.y = Math.sin(time * 0.002) * 4 + 10;
targetObj.position.x = Math.sin(time * 0.002) * 4 - 20;
Math.sin方法是计算直角三角形中正弦比值的方法,传入一个数字他会返回一个-1到1之间的数字,比如返回-0.5,那么-0.5*4 = -2,也就是说这个值最大是4最小是-4,再加上/减去一定的数值,那么就固定了这个目标最大可以动到哪里,最小可以动到哪里,让他在一定范围内移动。
让炮台跟随目标移动
先在render方法外层声明targetPosition变量,必须初始化为0,0,0,否则将会出现无法设置X/Y/Z坐标的问题。
let targetPosition: THREE.Vector3 = new THREE.Vector3(0, 0, 0);
// render方法内部
// 先调用getWorldPosition方法获取到targetMesh对于整个场景中的坐标
targetMesh.getWorldPosition(targetPosition);
// 然后让炮筒看向他
BarrelObj.lookAt(targetPosition);