使用Babylon实现3D手机手表定制(uv贴图)

138 阅读5分钟

效果:life-app.vertu.com/test/mto/v2…

直接上代码实操 以下代码为核心代码没有业务逻辑

创建场景

// 相机
const camera  = useRef(Camera);
// 场景
const scene  = useRef();
// 引擎
const engine = useRef();
// cancas实例
const canvasRef = useRef();

// 创建场景
scene.current = new Scene(engine);
// 启用阴影
scene.shadowsEnabled = true;
// 设置场景背景色 参考rgba
scene.clearColor = new Color4(0,0,0,0);

创建轨道式相机

camera.current = new ArcRotateCamera()
  "camera", //name  名称
  -Math.PI / 2, //alpha 定义相机沿纵轴的旋转
  Math.PI / 2, // beta  定义相机沿横轴的旋转
  radius, // radius 定义摄影机与其目标的距离
  Vector3.Zero(), //target  定义摄影机目标 (Vector3.Zero():三维向量都是0点)
  scene.current // 添加到场景中
);
// 设置相机控制器
const cameraControl = new ArcRotateCameraPointersInput();
camera.current.inputs.add(cameraControl);
// 设置最小缩放
camera.current.lowerRadiusLimit = lowerRadiusLimit; 
// 设置最大缩放
camera.current.upperRadiusLimit = upperRadiusLimit; 
// 把相机附属在 画布上面, 可以通过 鼠标操作 
camera.current.attachControl(canvasRef.current, true);
// 设置滚轮的缩放精度
camera.current.wheelPrecision = 0.45;

移动端手指缩放模型

// 手势缩放精度调整系数
const scaleSpeed = 2;

// 监听触摸事件
const canvas = scene.current.getEngine().getRenderingCanvas();
canvas.current?.addEventListener("touchstart", handleTouchStart);
canvas.current?.addEventListener("touchmove", handleTouchMove);

let lastPinchDistance = 0;

function handleTouchStart(event: TouchEvent) {
if (event.touches.length === 2) {
  const touch1 = event.touches[0];
  const touch2 = event.touches[1];

  lastPinchDistance = getPinchDistance(touch1, touch2);
}
}

function handleTouchMove(event: TouchEvent) {
if (event.touches.length === 2) {
  const touch1 = event.touches[0];
  const touch2 = event.touches[1];

  const currentPinchDistance = getPinchDistance(touch1, touch2);

  // 根据手势缩放的距离变化调整相机的缩放
  const pinchDelta = currentPinchDistance - lastPinchDistance;
  camera.current.radius -= pinchDelta * scaleSpeed;

  lastPinchDistance = currentPinchDistance;
}
}

function getPinchDistance(touch1: Touch, touch2: Touch) {
return Math.sqrt(
  Math.pow(touch1.clientX - touch2.clientX, 2) +
    Math.pow(touch1.clientY - touch2.clientY, 2)
);
}

创建一个光源

var light = new DirectionalLight(
  "DirectionalLight",
  new Vector3(-0.8, 0.1, -1),
  scene.current
);
light.intensity = 1.5;

创建环境光

//创建并返回由IBL-Baker或Lys等工具根据预过滤数据创建的纹理。
const envTex = CubeTexture.CreateFromPrefilteredData(envPath, scene.current); 
//创建 环境纹理 (在所有pbr材质中用作反射纹理的纹理。 正如在大多数场景中一样,它们是相同的(多房间等除外), 这比从所有材料中引用更容易。)
scene.current.environmentTexture = envTex; 
scene.current.environmentIntensity = environmentIntensity;

加载模型 监听加载进度 添加阴影

const { meshes } = await SceneLoader.ImportMeshAsync(
      "",
      "",
      模型地址,
      scene.current,
      (event) => {
        // 监听加载进度
        if (event.lengthComputable) {
          var progress = (event.loaded * 100) / event.total;
          modelProgressCallback(Number(progress.toFixed(0)));
        }
      }
    );

    // 创建灯光
    var light = new DirectionalLight(
      "dir01",
      new Vector3(0.6, -1, 0.05),
      scene.current
    );
    light.intensity = 0.3;
    // 启用阴影
    scene.current.shadowsEnabled = true;
    light.shadowEnabled = true;
    light.shadowMinZ = -1100; // 调整阴影偏移的值

    // 创建阴影生成器
    var shadowGenerator = new ShadowGenerator(1024, light);
    shadowGenerator.useBlurExponentialShadowMap = true;
    // 设置阴影贴图的模糊参数
    shadowGenerator.useKernelBlur = true;
    shadowGenerator.blurKernel = 10;
    // 创建地面网格
    var ground = MeshBuilder.CreateGround(
      "ground",
      { width: 2000, height: 2000 },
      scene.current
    );
    // 创建地板材质
    var groundMaterial = new StandardMaterial("groundMaterial", scene.current);
    groundMaterial.emissiveColor = new Color3(0.72, 0.72, 0.72);
    // 禁用反射光
    groundMaterial.specularColor = new Color3(0, 0, 0);
    groundMaterial.specularPower = 0; // 将反射光的强度设置为零
    groundMaterial.diffuseColor = new Color3(
      0.9647058823529412,
      0.9647058823529412,
      0.9647058823529412
    );

    // 将材质应用到地板
    ground.material = groundMaterial;
    ground.position = new Vector3(0, -425, 0);
    ground.receiveShadows = true;
    // 循环设置模型每个网格的阴影属性
    for (var i = 0; i < meshes.length; i++) {
      meshes[i].receiveShadows = true;
      // 将模型网格添加到阴影生成器渲染列表中
      shadowGenerator!.getShadowMap()!.renderList!.push(meshes[i]);
    }

uv贴图 根据模型名称给对应模型部位贴模型师提供的贴图 定义json 按照模型所有的部位定义每个部位使用的贴图类型和贴图地址

背盖: {
    zh: "背盖",
    en: "tegmental",
    alias: ["D", "Mix", "N"],
    texture: [
      {
        alias: "N",
        path: `${baseUrl}/小牛皮_N.png`,
      },
      {
        alias: "Mix",
        path: `${baseUrl}/小牛皮_Mix.png`,
      },
      {
        alias: "D",
        path: `${baseUrl}/小牛皮_D.png`,
      },
    ],
  },

加载材质 并监听加载进度

export const getTexture = async (
  url: string,
  scene: Scene,
  callback: (v: BaseTexture) => void
) => {
  const newTexture = new Texture(
    url, 
    scene
  );
  newTexture.vScale = -1;
  var assetsManager = await new AssetsManager(scene);
  var assetsManager = new AssetsManager(scene);
  assetsManager.addTextureTask("textureTask", url);

  // 加载完成后的处理
  assetsManager.onFinish = function () {
    
  };
  // 开始加载任务
  assetsManager.load();
  return newTexture;
};

使用pbr根据自定义的类型完成对应的贴图

const material = new PBRMetallicRoughnessMaterial("material", scene.current);
texture.forEach(async (v: IDefaultText) => {
  const texture = await getTexture(v.path, scene.current, setCallback);
  if (v.alias === "D") {
    // 颜色 D
    material.baseTexture = texture;
  }
  if (v.alias === "N") {
    // 法线 N
    material.normalTexture = texture;
  }
  if (v.alias === "Mix") {
    // 金属&粗糙 混合贴图 mix
    material.metallicRoughnessTexture = texture;
  }
  if (v.alias === "R") {
    material.environmentTexture = texture;
  }
});
return material;

循环模型meshs贴对应材质

// 根据名称完成贴图
scene.cuurent.meshes.forEach((mesh: AbstractMesh) => {
 mesh.material = material
});

视频材质贴图 监听进度

let video = document.createElement("video");
  video.src = src;
  video.autoplay = true;
  video.loop = true;
  // 创建一个 VideoTexture 对象
  let videoTexture = new VideoTexture("videoTexture", video, scene.current, true, true);

  // 监听加载完成的事件
  videoTexture.onLoadObservable.add(function () {
    setCallback?.();
  });
  const mater = new StandardMaterial("videoMaterial", scene);
  mater.diffuseTexture = videoTexture;
  // 自发光
  mater.emissiveColor = new Color3(1, 1, 1);
  // 反转
  videoTexture.vScale = -1;
  return mater;

给模型添加点击事件

scene.current.onPointerUp = (e: Touch) => {
    const absX = Math.abs(e.clientX - mouseClient.x) <= 5;
    const absY = Math.abs(e.clientY - mouseClient.y) <= 5;
    // 点击事件
    if (absX && absY) {
      const ray = scene.createPickingRay(
        scene.pointerX, //定义原点的x坐标(屏幕上)
        scene.pointerY, //定义原点的y坐标(屏幕上)
        Matrix.Identity(), //创建新的单位矩阵
        camera
      );
      //使用给定的光线在场景中拾取网格
      const raycastHit = scene.pickWithRay(ray); 
      const pickedMesh = raycastHit?.pickedMesh;
      if (raycastHit && raycastHit.hit && pickedMesh) {
 
        // 根据点击的部件返回
        callback(pickedMesh.id);
      }
    }
  };
  scene.onPointerDown = (e: Touch) => {
    mouseClient = {
      x: e.clientX,
      y: e.clientY,
    };
  };

部件的变色动画 比如点击或者切换了材质以后让其变色凸显出当前改变的部位

  let startColor = new Color3(0.5, 0.5, 0.5);
  let endColor = new Color3(0, 0, 0);
  let animationDuration = 1500;
  let animation = new Animation(
    "emissiveColorAnimation",
    "material.emissiveColor",
    animationDuration,
    Animation.ANIMATIONTYPE_COLOR3,
    Animation.ANIMATIONLOOPMODE_CONSTANT
  );
  let keys = [
    { frame: 0, value: startColor },
    { frame: animationDuration, value: endColor },
  ];

  animation.setKeys(keys);

  let easingFunction = new SineEase();
  easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
  animation.setEasingFunction(easingFunction);

  mesh.animations.push(animation);
  scene.current.beginAnimation(mesh, 0, animationDuration, false);

创建动态纹理 给屏幕添加日期

//Set font
 let font_size = 75;
 let font = font_size + "px Akzidenz-Grotesk Pro";

 //Set height for plane
 let planeHeight = 100;

 //Set height for dynamic texture
 let DTHeight = 80; //or set as wished

 //Calcultae ratio
 let ratio = planeHeight / DTHeight;

 //Use a outputplaneTextureoray dynamic texture to calculate the length of the text on the dynamic texture canvas
 let outputplaneTexture = new DynamicTexture("DynamicTexture", 64, scene);
 outputplaneTexture.hasAlpha = true;
 let context2D = outputplaneTexture.getContext();
 context2D.font = font;
 let DTWidth = 220;

 //Calculate width the plane has to be
 let planeWidth = DTWidth * ratio;

 //Create dynamic texture and write the text
 let dynamicTexture = new DynamicTexture(
   "DynamicTexture",
   {
     width: DTWidth,
     height: DTHeight,
   },
   scene.current,
   false
 );
 dynamicTexture.hasAlpha = true;
 let mat = new StandardMaterial("mat", scene);
 mat.diffuseTexture = dynamicTexture;
 mat.emissiveColor = new Color3(1, 1, 1);
 context2D.globalAlpha = 0;

 let text = getTime();
 dynamicTexture.drawText(text, null, null, font, "#444", "transparent", true);

 // 更改时间
 let timer = setInterval(() => {
   const newText = getTime();
   if (newText !== text) {
     text = newText;
     clearInterval(timer);
     dynamicTexture.dispose();
     plane.dispose();
     createTime(scene.current);
   }
 }, 1000);

 //Create plane and set dynamic texture as material
 let plane = MeshBuilder.CreatePlane(
   "plane",
   { width: planeWidth, height: planeHeight },
   scene
 );

 plane.material = mat;
 plane.position = new Vector3(-10, 210, -17.3);
 plane.rotate(Axis.Y, 0.371, Space.LOCAL);

模型某个部位的显示和隐藏

const mesh = scene.getMeshByName('模型部件id');
mesh!.isVisible = true;

改变相机视角

 camera.current.position = new Vector3(3146, 382, -424);

** 销毁场景和所有资源**

// 销毁场景和资源
export const destroyModel = (engine: Engine, scene: Scene) => {
 // 销毁网格
 scene.meshes.forEach(function (mesh: AbstractMesh) {
   mesh.dispose();
 });

 // 销毁材质实例
 scene.materials.forEach(function (material: Material) {
   material.dispose();
 });

 // 销毁材质
 scene.textures.forEach(function (texture: BaseTexture) {
   texture.dispose();
 });

 // 销毁灯光
 scene.lights.forEach(function (light: Light) {
   light.dispose();
 });

 // 销毁场景
 scene.dispose();

 // 销毁引擎
 engine.stopRenderLoop();
 engine.dispose();
 console.log("销毁完成");
};