效果: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("销毁完成");
};