Threejs换装效果实现

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换装

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鸢_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值