我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
由于霸王龙模型比较大(20M+),需要等待一段时间。
1.引子
古有后羿射日,后裔成为英雄后,上天奖励不死药,然后被嫦娥所偷。固有“嫦娥应悔偷灵药,碧海青天夜夜心”的诗句。
今日,小明追随偶像后裔的脚步,准备了三级火力机枪,在霸王龙的威压下,勇敢射月,聊解嫦娥寂寞。
2.画个草图
打开画图软件构思一下,大概就长这个样子
3.元素分析
- 两个人物
- 两个球体
- 一只恐龙
- 子弹飞出去的弹道
渲染引擎使用three.js
,为了写起来更方便,使用 simple-scene-react 库(该库在# 基于three.js的太、地、月三体运动有介绍)。
球体使用贴图即可,人物为了简单使用精灵图。
小明
嫦娥
对于霸王龙,我在模型库中找到了.glb
文件,
攻击弹道使用 fly-line(fly-line
是封装笔者的一个简单飞线效果)。
4.fly-line介绍
跟simple-scene-react一样,飞线效果用的比较多,所以就打包发了一个npm包
。
示例
3D场景应用
2D场景应用
地图中的应用
小明射击练习
代码见/example
5.构造类
5.1.天体类
class Star {
name: string;
image: any;
highImage: any;
raduis: number;
position: number[];
cita: number;
mesh: any
constructor(
name: string,
image: any,
highImage: any,
raduis: number,
position: number[],
cita: number,
) {
this.name = name;
this.image = image;
this.highImage = highImage;
this.raduis = raduis;
this.position = position;
this.cita = cita;
}
}
属性 | 说明 |
---|---|
name | 天体名称 |
image | 贴图图片 |
highImage | 置换贴图图片 |
raduis | 天体半径 |
position | 天体位置 |
cita | 公转角度 |
5.2.Human类
class Human {
name: string;
image: any;
position: number[];
mesh: any
constructor(
name: string,
image: any,
position: number[],
) {
this.name = name;
this.image = image;
this.position = position;
}
}
属性 | 说明 |
---|---|
name | 名字 |
image | 精灵图 |
position | 位置坐标 |
霸王龙比较独立,就懒得写类了。
6.渲染
6.1.天体渲染
定义:
const stars = [
new Star(
'Earth',
require('./texture/earth.jpg'),
require('./texture/earth-high.jpg'),
300,
[0, -300, 600],
0
),
new Star(
'Moon',
require('./texture/moon.jpg'),
require('./texture/moon-high.jpg'),
150,
[600, 400, -1000],
0
),
];
因为想要有更好的显示,这里采用了置换贴图。我在文章# Echarts 3D Earth "冰山一角"揭秘 | 创作者训练营第二期比较了置换贴图、凹凸贴图、法相贴图
的优劣。
const addStars = async (scene: THREE.Scene) => {
for (let i = 0; i < stars.length; i++) {
let star = stars[i];
let sphereGeo = new THREE.SphereGeometry(star.raduis, 50, 50); //创建一个球体几何对象
let img = await ImageLoader.load(star.image);
let texture = new THREE.Texture(img);
texture.needsUpdate = true;
let imgH = await ImageLoader.load(star.highImage);
let highTexture = new THREE.Texture(imgH);
let material = new THREE.MeshStandardMaterial({
map: texture,
displacementMap: highTexture, // 置换贴图纹理
displacementScale: i === 0 ? 40 : 5,
displacementBias: 10,
});
(material.displacementMap as any).needsUpdate = true;
let sphereMesh = new THREE.Mesh(sphereGeo, material);
// 设置位置
sphereMesh.position.set(
star.position[0],
star.position[1],
star.position[2]
);
sphereMesh.name = star.name;
star.mesh = sphereMesh;
if (i === 0) {
sphereMesh.rotation.z = Math.PI / 6;
sphereMesh.rotation.y = (3 * Math.PI) / 2;
}
scene.add(sphereMesh);
}
};
这样可以是球体表面有凹凸性。
6.2.人物渲染
const addHumans = async (scene: THREE.Scene) => {
for (let i = 0; i < humans.length; i++) {
let human = humans[i];
let img = await ImageLoader.load(human.image);
let texture = new THREE.Texture(img);
texture.needsUpdate = true;
let spriteMaterial = new THREE.SpriteMaterial({
rotation: i === 0 ? Math.PI / 6 : 2 * Math.PI,
map: texture,
});
let mesh = new THREE.Sprite(spriteMaterial);
mesh.position.set(human.position[0], human.position[1], human.position[2]); //几何体中心位置
mesh.name = human.name;
human.mesh = mesh;
mesh.scale.set(100, 100, 1);
scene.add(mesh);
}
};
精灵图其实就是始终面向摄像头的平面。可见Three.js电子书进行参考。效果如下:
不好意思,这个角度只让小明露了个头,没有办法,他可是要躲避霸王龙的攻击的。
6.3.霸王龙渲染
首选需要一个GLTFLoader
。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const GLBLoader = new GLTFLoader();
然后进行load
const addRampaging = async (scene: THREE.Scene) => {
GLBLoader.load(rampaging, gltf => {
let modal = gltf.scene;
modal.scale.set(50, 50, 50);
mixer = new THREE.AnimationMixer(modal); // mixer 为全局变量
animations = gltf.animations; // animations 为全局变量
mixer.clipAction(animations[1]).play(); // 默认播放第二个动画片段
modal.rotation.y = Math.PI / 2;
modal.position.set(-80, 0, 600);
scene.add(modal);
});
};
需要注意glb
模型有再带的animations
,我将其存到了全局变量 animations
中,且默认使用第二个animation
片段,见注释。其余的动作我将其入口放到了左上角第二行。
{['run', 'bit', 'roar', 'tail', 'idke'].map((item, index) => (
<button
key={item}
onClick={() => {
if (mixer) {
mixer.stopAllAction();
mixer.clipAction(animations[index]).play();
}
}}
>
{item}
</button>
))}
可以访问 在线预览 进行尝试。
动画的执行使用mixer = new THREE.AnimationMixer(modal)
对象,在使用clipAction().play()
之后,在animate
(动画循环)函数使用mixer.update(delta)
即可。
6.4.弹道攻击渲染
如果只用直线渲染,小明会觉得很单调,按小明的想法,他想给嫦娥一个如烟花般的惊喜。
这种曲线如果用贝塞尔曲线来实现,那么其重点在于控制点的计算,即THREE.QuadraticBezierCurve3
的第二个参数。
于是小明拿出了纸和笔。
- 已知起点
A
与终点B
; - 可以计算出
A
与B
的中点C
; - 过
C
点做垂直于向量AB
的平面; - 这个时候可以得到平面方程;
- 问题等价于已知平面
P
,和平面P
上的点C
,做一个半径为r
的圆,求圆的参数方程。
小明算了几张纸发现这并不好算,于是改变思路。
1.构造一个XY
平面的圆;
2.将其平移到C
;
3.然后做一个绕轴旋转就可以得到答案。
也就是先作用一个平移变换矩阵,然后再作用一个旋转矩阵即可。
但是非X,Y,Z
轴旋转矩阵小明不知道长啥样,小明陷入了沉思。
后来他发现THREE.Mesh
有绕轴旋转的APIsetRotationFromAxisAngle
于是他写下了如下code
来调试他的机关枪。
// 生成一个XY平面圆,圆心[0,0],半径200
let arc = new THREE.ArcCurve(0, 0, 200, 0, 2 * Math.PI, true);
// 拿到圆上50个点
let points = arc.getPoints(50);
// 根据点拿到几何体
let geometry = new THREE.BufferGeometry().setFromPoints(points);
// 让顶点数据更新
geometry.attributes.position.needsUpdate = true;
// 材质
let material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
// Mesh对象
let circle = new THREE.Line(geometry, material);
// 平移到点C
circle.translateX(C.x);
circle.translateY(C.y);
circle.translateZ(C.z);
// 绕轴旋转
// AB方向向量p
let p = A.clone().sub(B); // 不加clone会改变A向量
// XY平面圆的向量为 [0,0,1]
let _p = new THREE.Vector3(0, 0, 1);
// 相当于从法向量 p 旋转到了 _p
// 向量夹角
let cita = p.angleTo(_p)
// 旋转轴应该垂直于 p _p 构成的平面
let axis = p.clone().cross(_p).normalize(); // 再标准化一下
// 进行旋转
circle.setRotationFromAxisAngle(axis, cita)
将circle
添加到scene
之后,成功的在指定位置上渲染了。
离目标只差一步,从Mesh
中拿到顶点数据就可以了。
尝试了各种办法,如geometry.attributes.position.needsUpdate = true
,解析circle.geometry.getAttribute('position').array
得到points
,然后都失败的发现其不是最新值。
于是小明转了一个弯,为啥要操作Mesh
呢,直接操作Vector
不就没有中间商赚差价了吗?Vector
上也是有绕轴旋转方法applyAxisAngle
的。
/**
*
* @param axis 指定坐标轴
* @param angle 旋转的角度
* @param offset 平移的向量
* @param r 半径
* @param num 点的个数
* @returns points
*/
const generatePoints = (
axis: THREE.Vector3,
angle: number,
offset: THREE.Vector3,
r: number,
num: number
) => {
let arr: THREE.Vector3[] = [];
for (let i = 0; i <= num; i++) {
let cita = (2 * Math.PI * i) / num;
let p = new THREE.Vector3(r * Math.cos(cita), r * Math.sin(cita), 0);
p.applyAxisAngle(axis, angle);
p.add(offset);
arr.push(p);
}
return arr;
};
小明成功了,他给他的机关枪安装了三级火力,一级比一级强,最后猛烈的朝月球射去。
7.动画
animate
比较简单,直接贴一发代码,line_1,line_2,line_3
代表了三级火力。
const animate = (target: any, clock: THREE.Clock) => {
let delta = clock.getDelta();
if (mixer) {
mixer.update(delta);
}
lines_1.forEach(flyLine => {
flyLine.animate();
});
lines_2.forEach(flyLine => {
flyLine.animate();
});
lines_3.forEach(flyLine => {
flyLine.animate();
});
// 自转
stars.forEach(star => {
if (star.mesh) {
star.mesh.rotation.y += delta;
}
});
};
8.结语
转换视角,盖以嫦娥妹妹的角度来看,小明正在与霸王龙进行精彩的搏斗呢?
欢迎来玩,欢迎使用fly-line
。