1.思路
将 人物模型拆成各部分,再在换装时更换模型即可。
2.实现过程
1.三维场景及人物模型分别由各自类管理,使用时除了指定HTMLElement容器和保存Engine实例,还要保证三维模型在正确的路径上
import {
WebGLRenderer,
Scene,
PerspectiveCamera,
Mesh,
BoxGeometry,
MeshStandardMaterial,
Vector3,
AmbientLight,
AxesHelper,
GridHelper,
Object3D,
Group,
AnimationMixer,
TextureLoader,
LinearFilter,
SRGBColorSpace,
SpotLight,
PlaneGeometry,
MeshBasicMaterial,
MeshPhysicalMaterial,
MeshLambertMaterial,
CircleGeometry,
RepeatWrapping,
} from "three";
import Stats from "three/examples/jsm/libs/stats.module";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
export class TEngine {
private dom: HTMLElement;
private renderer: WebGLRenderer;
private scene: Scene;
private camera: PerspectiveCamera;
private model: ModelGroup;
constructor(dom: HTMLElement) {
this.dom = dom;
this.renderer = new WebGLRenderer({
antialias: true, //抗锯齿
alpha: true,
});
this.scene = new Scene();
this.camera = new PerspectiveCamera(
45,
dom.offsetWidth / dom.offsetHeight,
1,
100000
);
this.camera.position.set(
11.659653898465203,
19.439256121461874,
19.16799622219121
);
this.camera.lookAt(new Vector3(0, 10, 0));
// this.camera.up = new Vector3(0, 1, 0);
this.renderer.setSize(dom.offsetWidth, dom.offsetHeight, true);
this.renderer.setClearAlpha(0);
// 环境光
const ambientLight: AmbientLight = new AmbientLight("#fff", 1);
// 参考坐标轴
const axesHelper: AxesHelper = new AxesHelper(500);
// 矩阵线
const gridHelper: GridHelper = new GridHelper(500, 10, "#fff", "#999");
// 性能监视
// 轨道监控
const orbitControls: OrbitControls = new OrbitControls(
this.camera,
this.renderer.domElement
);
orbitControls.autoRotate = false;
orbitControls.target.set(0, 10, 0);
// 最简单的模型
const box: Mesh = new Mesh(
new BoxGeometry(10, 10, 10),
new MeshStandardMaterial({ color: "#f00" }) //物体颜色
);
// this.scene.add(box);
this.scene.add(ambientLight);
this.scene.add(axesHelper);
this.scene.add(gridHelper);
// this.renderer.setClearColor('rgb(255,255,255)')//场景颜色*canvas
// this.renderer.clearColor()//清空场景色
// console.log('TEngine init', this.dom, this);
// 动态渲染 request Animation Frame 让场景动起来
/**换装 开始 ↓*/
this.model = new ModelGroup();
this.scene.add(this.model.BODY);
const plane = this.addSpotlight();
/**换装 结束↑*/
const tick = () => {
orbitControls.update(); //轨道相机控制
if (this.model) this.model.update(0.01);
this.renderer.render(this.scene, this.camera);
plane.offset.y += 0.01;
requestAnimationFrame(tick);
};
tick();
dom.appendChild(this.renderer.domElement);
}
addSpotlight() {
const textload = new TextureLoader();
const texture = textload.load("/images/wave.jpg");
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.colorSpace = SRGBColorSpace;
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
const spotLight = new SpotLight(0xffffff, 10);
spotLight.position.set(0, 3, 0);
spotLight.angle = Math.PI / 2.2;
spotLight.penumbra = 1;
spotLight.decay = 0.8;
spotLight.distance = 1000;
spotLight.map = texture;
spotLight.castShadow = false;
spotLight.shadow.camera.near = 1;
spotLight.shadow.camera.far = 10;
spotLight.shadow.focus = 1;
this.scene.add(spotLight);
const plane = new Mesh(
new CircleGeometry(10, 32),
new MeshLambertMaterial({
color: 0xbcbcbc,
})
);
plane.rotation.set(-Math.PI / 2, 0, 0);
this.scene.add(plane);
return texture;
}
// 屏幕缩放后调整画布
resize(width: number, height: number) {
let { renderer } = this;
renderer.setSize(width, height, true);
}
// 调用model 类的方法换模型
setBody(part: "head" | "cloth" | "pants" | "shoes", url: string) {
console.log("TESETBODY");
if (!this.model) return;
this.model.setBody(part, url);
}
}
interface ModelParts {
model: Group | Mesh | null;
mixer: AnimationMixer | null;
}
interface ModelCombine {
head: ModelParts;
cloth: ModelParts;
pants: ModelParts;
shoes: ModelParts;
}
class ModelGroup {
// 三维场景中的模型
BODY: Group;
// 管理模型内的四个部分
body: ModelCombine = {
head: { model: null, mixer: null },
cloth: { model: null, mixer: null },
pants: { model: null, mixer: null },
shoes: { model: null, mixer: null },
};
constructor() {
this.BODY = new Group();
this.BODY.scale.set(0.1, 0.1, 0.1);
// 初始化四个部分 如果需要更多分块和更多逻辑判断
// 可将一下代码抽离为单独方法
this.setBody("head", "/testModel/head.FBX");
this.setBody("cloth", "/testModel/cloth.FBX");
this.setBody("pants", "/testModel/pants.FBX");
this.setBody("shoes", "/testModel/shoes.FBX");
}
// 人物模型分为四部分,但加载逻辑相同,可抽取为公共方法
setBody(part: "head" | "cloth" | "pants" | "shoes", url: string) {
if (!part) return;
// let { model, mixer } = curPart;
let loader = new FBXLoader();
loader.load(url, (fbx) => {
console.log("part:", { part, fbx });
if (this.body[part].model !== null) {
this.BODY.remove(this.body[part].model!);
this.body[part].model = null!;
}
if (this.body[part].mixer) {
this.body[part].mixer = null!;
}
this.body[part].model = fbx;
this.body[part].mixer = new AnimationMixer(fbx);
const action = this.body[part].mixer!.clipAction(fbx.animations[0]);
action.play();
this.BODY.add(this.body[part].model!);
this.resetAnimate();
});
}
resetAnimate(animatecode: number = 0) {
// 非必须,旨在加强稳定性
//涉及到单个模型更改,使得动画时序不同步,在每次更改后统一时序
if (this.body.head.mixer) this.body.head.mixer.setTime(animatecode);
if (this.body.cloth.mixer) this.body.cloth.mixer.setTime(animatecode);
if (this.body.pants.mixer) this.body.pants.mixer.setTime(animatecode);
if (this.body.shoes.mixer) this.body.shoes.mixer.setTime(animatecode);
}
update(delta: number) {
//在 requestAnimate中更新模型,播放动画
if (this.body.head.mixer) this.body.head.mixer.update(delta);
if (this.body.cloth.mixer) this.body.cloth.mixer.update(delta);
if (this.body.pants.mixer) this.body.pants.mixer.update(delta);
if (this.body.shoes.mixer) this.body.shoes.mixer.update(delta);
}
}
2.关于模型的管理
可以将各部分模型单独管理,定义部位,名称和风格等,下面的cover为封面url,旨在列表中展示模型的大致样子,这样可以很好的在页面对各部分进行分类
export interface bodiesComponent {
id: string;
name: string;
url: string;
cover: string | null;
part: "head" | "cloth" | "pants" | "shoes" ;
type: "cartoon" | "real";
sex: "male" | "female";
}
然后将不同模型组合可以组成完整的模型,这样可以组成预设的默认角色样式,通过id获取部件。同样的定义风格,性别以便分类
export interface Bodies {
id: string;
name: string;
cover: string | null;
type: "cartoon" | "real";
sex: "male" | "female";
head: string;
cloth: string;
pants: string;
shoes: string;
}
3.关于页面使用
在将模型json写好,三维场景初始化后,我们可以将各个部位的id进行记录,通过id的变化修改肠镜中人物的外观
// vue版本 当前模型
const currentBody = ref<any>({
head: "0",
cloth: "1",
pants: "2",
shoes: "3",
});
//修改全部与单个身体部件的方法
const changeBodyAll = (paths: any) => {
let { head, cloth, pants, shoes } = paths;
console.log("changeBodyAll", { head, cloth, pants, shoes });
if (te === null) return;
te.value?.setBody("head", head);
te.value?.setBody("cloth", cloth);
te.value?.setBody("pants", pants);
te.value?.setBody("shoes", shoes);
};
const changeBodyPart = (id: string, part: string) => {
if (!id) return;
currentBody.value[part] = id;
};
//检测数据变化更新模型
watch(
currentBody,
(val) => {
let paths = getBodyComponents(val);
console.log("currentBody change", {
currentBody: val,
paths,
});
changeBodyAll(paths);
// for(let k in path)
},
{
deep: true,
}
);
这样就完成了,由于留够了灵活度(封装,规范数据格式等),具体功能可根据需求再做修改。
3.演示
threejs换装