Three.js 搭建3D隧道监测

news2024/12/25 0:23:01

Three.js 搭建3D隧道监测

  • Three.js 基础元素
    • 场景scene
    • 相机carema
    • 网络模型Mesh
    • 光源light
    • 渲染器renderer
    • 控制器controls
  • 实现3d隧道监测基础
    • 实现道路
    • 实现隧道
    • 实现多个摄像头
    • 点击模型进行属性操作
    • 实现点击模型发光效果
  • 性能监视器stats
    • 引入
    • 使用
  • 总结
  • 完整代码

我们将通过three.js技术打造3d隧道监测可视化项目,隧道监测项目将涵盖照明,风机的运行情况,控制车道指示灯关闭,情报板、火灾报警告警、消防安全、车行横洞、风向仪、隧道紧急逃生出口的控制以及事故模拟等!那先来看看我们的初步成果!因为作者也是在边学习边做的情况,效果有些丑陋,希望不要见笑!!!three.js基础知识还是基本涵盖了,入门还是很有参考价值的!

在这里插入图片描述

Three.js 基础元素

我们将通过一个基本的three.js模板代码更好的概况我们的基础元素

import React, { useEffect } from 'react';
import * as THREE from 'three';
// eslint-disable-next-line import/extensions
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

export default function ThreeVisual() {
    // 场景
    let scene;
    // 相机
    let camera;
    // 控制器
    let controls;
    // 网络模型
    let mesh;
    // 渲染器
    let renderer;

    // debugger属性
    const debugObject = {
        light: {
            amlight: {
                color: 0xffffff,
            },
            directionalLight: {
                color: 0xffffff,
                position: {
                    x: 0,
                    y: 400,
                    z: 1800,
                },
            },
            pointLight: {
                color: 0xff0000,
                position: {
                    x: 0,
                    y: 400,
                    z: 1800,
                },
            },
        },
    };


    const sizes = {
        width: window.innerWidth,
        height: window.innerHeight,
    };

    useEffect(() => {
        // eslint-disable-next-line no-use-before-define
        threeStart();
    }, []);

    const initThree = () => {
        const width = document.getElementById('threeMain').clientWidth;
        const height = document.getElementById('threeMain').clientHeight;
        renderer = new THREE.WebGLRenderer({
            antialias: true,
            logarithmicDepthBuffer: true,
        });
        renderer.shadowMap.enabled = true;
        renderer.setSize(width, height);

        document.getElementById('threeMain').appendChild(renderer.domElement);
    };

    const initCamera = (width, height) => {
        camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);
        camera.position.x = 0;
        camera.position.y = 500;
        camera.position.z = 1300;
        camera.up.x = 0;
        camera.up.y = 1;
        camera.up.z = 0;
        camera.lookAt({
            x: 0,
            y: 0,
            z: 0,
        });

        // 创建相机视锥体辅助对象
        // const cameraPerspectiveHelper = new THREE.CameraHelper(camera);
        // scene.add(cameraPerspectiveHelper);
    };

    const initScene = () => {
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xbfd1e5);
    };

    const initLight = () => {
        // 环境光
        const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);
        amlight.position.set(1000, 1000, 1000);
        scene.add(amlight);
    };

    const initObject = () => {
        const geometry = new THREE.BoxGeometry(3000, 6, 2400);
        const material = new THREE.MeshBasicMaterial({color: 0xcccccc});
        geometry.position = new THREE.Vector3(0, 0, 0);
        mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);
        mesh.receiveShadow = true; // cast投射,方块投射阴影
        scene.add(mesh);
    }

    const initControl = () => {
        // 将renderer关联到container,这个过程类似于获取canvas元素
        const pcanvas = document.getElementById('threeMain');

        controls = new OrbitControls(camera, pcanvas);

        // 如果使用animate方法时,将此函数删除
        // controls.addEventListener( 'change', render );
        // 使动画循环使用时阻尼或自转 意思是否有惯性
        controls.enableDamping = true;
        // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
        // controls.dampingFactor = 0.25;
        // 是否可以缩放
        controls.enableZoom = true;
        // 是否自动旋转
        // controls.autoRotate = true;
        controls.autoRotateSpeed = 0.5;
        // 设置相机距离原点的最近距离
        // controls.minDistance  = 10;
        // 设置相机距离原点的最远距离
        controls.maxDistance = 10000;
        // 是否开启右键拖拽
        controls.enablePan = true;
    };



    function animation() {
        renderer.render(scene, camera);
        // mesh.rotateY(0.01);
        requestAnimationFrame(animation);
    }

    function initHelper() {
        const axesHelper = new THREE.AxesHelper(3000);
        scene.add(axesHelper);
    }


    function threeStart() {
        initThree();
        initScene();
        initCamera(sizes.width, sizes.height);
        initHelper();
        initObject();
        initLight();
        initControl();
        animation();
    }

    return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;
}

在这里插入图片描述

场景scene

是一个三维空间,相当于我们html中的body,所有节点的容器,相当于一个空房间,承载所有的物品!所以我们定义一个全局变量scene。

初始化我们可以这样:

  const initScene = () => {
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xbfd1e5);
    };

相机carema

打个比方,就是你买了一个1万元的相机出门拍风景,你总是想要抓住最美的风景,那你便要调好相机最精确的位置、角度、焦距等,相机看到的内容就是我们最终在屏幕上看到的内容。在这个例子中我们用的是像我们眼睛的透视相机PerspectiveCamera。
在这里插入图片描述
还有一个常用的相机是正交相机OrthographicCamera,它看到的范围不会受距离影响!
在这里插入图片描述
我们也定义了一个全局变量camera,

初始化我们可以这样:

 const initCamera = (width, height) => {
        camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);
        camera.position.x = 0;
        camera.position.y = 500;
        camera.position.z = 1300;
        camera.up.x = 0;
        camera.up.y = 1;
        camera.up.z = 0;
        camera.lookAt({
            x: 0,
            y: 0,
            z: 0,
        });

        // 创建相机视锥体辅助对象
        // const cameraPerspectiveHelper = new THREE.CameraHelper(camera);
        // scene.add(cameraPerspectiveHelper);
    };

网络模型Mesh

在介绍它之前我们需要先了解点模型Points、线模型Line。点线面,面就是Mesh模型。点模型Points、线模型Line、网格网格模型Mesh都是由几何体Geometry和材质Material构成。在这里就不过多研究点线面了,我们最重要的知道的是一个网络模型就是一个物体穿上了衣服,没有穿衣服的皇帝不会让别人揭穿和笑话,但是我们的老板才是皇帝,所以尽量给我们的模型套件衣服吧!

同理定义一个全局变量mesh
初始化我们可以这样:

const geometry = new THREE.BoxGeometry(3000, 6, 2400);
const material = new THREE.MeshBasicMaterial({color: 0xcccccc});
geometry.position = new THREE.Vector3(0, 0, 0);
mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);
mesh.receiveShadow = true; // cast投射,方块投射阴影
scene.add(mesh);

光源light

没有光世界便是黑暗的!同理假如没有光,摄像机看不到任何东西。所以我们需要为我们的场景加上不同光照效果。我们先从最基础的环境光AmbientLight开始。环境光意思就是哪个角度、哪个位置的光照亮度强度都一样。因为光不需要重复使用,所以我们没必要定义全局变量,所以我们初始化可以这样:

const initLight = () => {
    // 环境光
    const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);
    amlight.position.set(1000, 1000, 1000);
    scene.add(amlight);
};

渲染器renderer

就相当于现实生活中你带着相机,现在去了一个美丽的地方,你需要一个相片承载下这个美丽的景色,对于threejs而言,如果你需要这张相片,就需要一个新的对象,也就是WebGL渲染器WebGLRenderer,把这些承载。
在这里插入图片描述
同理我们定义一个全局变量renderer,初始化我们可以这样:

renderer = new THREE.WebGLRenderer({
   ... //属性配置
});

渲染器还需要补充几点,就是如何和我们的dom节点关联起来:

渲染器WebGLRenderer通过属性domElement可以获得渲染方法render()生成的Canvas画布,domElement本质上就是一个HTML元素:Canvas画布。我们也可以通过setSize()来设置尺寸。

定义一个html元素

return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;

html元素和渲染器关联,那就给div增加一个子节点(canvas)
在这里插入图片描述

const initThree = () => {
    const width = document.getElementById('threeMain').clientWidth;
    const height = document.getElementById('threeMain').clientHeight;
    renderer = new THREE.WebGLRenderer({
       ... //属性配置
    });
    renderer.setSize(width, height);  //设置画布宽高
    document.getElementById('threeMain').appendChild(renderer.domElement);  // 把画布加入dom节点
};

渲染器和我们的threejs元素关联, 那渲染器渲染方法.render(),把我们的场景和相机记录进来了!

renderer.render(scene, camera);

控制器controls

就是相当于可以通过我们的键盘和鼠标来控制我们的场景,使其有了交互功能!控制器种类有很多,但这里我们只说轨道控制器OrbitControls。它可以使得相机围绕目标进行轨道运动。打个比方(地球围绕太阳一样运动)。

同理我们定义一个全局变量controls,初始化我们可以这样:

controls = new OrbitControls(camera, pcanvas);

关联操作和属性介绍:

const initControl = () => {
    // 将renderer关联到container,这个过程类似于获取canvas元素
    const pcanvas = document.getElementById('threeMain');
    controls = new OrbitControls(camera, pcanvas);
    // 如果使用animate方法时,将此函数删除
    // controls.addEventListener( 'change', render );
    // 使动画循环使用时阻尼或自转 意思是否有惯性
    controls.enableDamping = true;
    // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
    // controls.dampingFactor = 0.25;
    // 是否可以缩放
    controls.enableZoom = true;
    // 是否自动旋转
    // controls.autoRotate = true;
    controls.autoRotateSpeed = 0.5;
    // 设置相机距离原点的最近距离
    // controls.minDistance  = 10;
    // 设置相机距离原点的最远距离
    controls.maxDistance = 10000;
    // 是否开启右键拖拽
    controls.enablePan = true;
};

到此,我们已经把threejs基础元素介绍的差不多了,在这里还需要补充一些很容易遗漏的地方!

动画和及时更新

function animation() {
    controls.update()
    renderer.render(scene, camera);
    // mesh.rotateY(0.01);
    requestAnimationFrame(animation);
}

补充一个知识点:
requestAnimationFrame
在这里插入图片描述

实现3d隧道监测基础

实现道路

如图,我们首先实现发光这部分。
在这里插入图片描述
在这里插入图片描述
这部分主要涉及的知识是给一个平面(plane)贴图,具体的知识我在代码块相应位置已经标注。

// 图加载器
const loader = new THREE.TextureLoader();
// 加载
const texture = loader.load('/model/route.png', function(t) {
  // eslint-disable-next-line no-param-reassign,no-multi-assign
  t.wrapS = t.wrapT = THREE.RepeatWrapping; //是否重复渲染和css中的背景属性渲染方式很像
  t.repeat.set(1, 1);
});

// 平面
const geometryRoute = new THREE.PlaneGeometry(1024, 2400);
const materialRoute = new THREE.MeshStandardMaterial({
  map: texture, // 使用纹理贴图
  side: THREE.BackSide, // 背面渲染
});
const plane = new THREE.Mesh(geometryRoute, materialRoute);
plane.receiveShadow = true;
plane.position.set(0, 8, 0);
plane.rotateX(Math.PI / 2);
scene.add(plane);

实现隧道

现在我们实现发光这部分
在这里插入图片描述
这部分主要涉及的知识是引入一个obj模型,并给模型贴上贴图(这里的材质是一个mtl)

补充知识点:

  • OBJ是一种3D模型文件,因此不包含动画、材质特性、贴图路径、动力学、粒子等信息 我们拿到一个隧道obj模型的文件打开看看,里面是什么
    在这里插入图片描述
  • mtl文件(Material Library File)是材质库文件,描述的是物体的材质信息,ASCII存储,任何文本编辑器可以将其打开和编辑。同理我们也可以打开看看,是个什么东西
    在这里插入图片描述
  • 从obj文件看出我们需要tunnelWall.mtl材质,从mtl文件,看出我们需要suidao.jpg图片(需要和模型放在同一级),其实到这里我们还是回到了引入道路的那部分,模型+贴图环节。

但是还是有一些不同的地方的,首先使用的加载器不同

const mtlLoader = new MTLLoader();
const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型

其次我们的模型是属于建模自己构造的,可能你引入进来很大可能是加载不出来的!所以你需要打印对象,从中分析具体原因。

// 模型对象公共变量
const modelsObj = {
  tunnelWall: {
    mtl: '/model/tunnelWall.mtl',
    obj: '/model/tunnelWall.obj',
    mesh: null,
  },

  camera: {
    mtl: '/model/camera/摄像头方.mtl',
    obj: '/model/camera/摄像头方.obj',
    mesh: null,
  },
};
mtlLoader.load(modelsObj.tunnelWall.mtl, material => {
    material.preload();
    // 设置材质的透明度
    // mtl文件中的材质设置到obj加载器
    loader.setMaterials(material);
    loader.load(modelsObj.tunnelWall.obj, object => {
        // 设置模型大小和中心点
        object.children[0].geometry.computeBoundingBox();
        object.children[0].geometry.center();
        modelsObj.tunnelWall.mesh = object;
        scene.add(object);
    });
});

实现多个摄像头

现在我们实现摄像头部分
在这里插入图片描述
这里其实和实现隧道大相径庭,只不过我们是多个,而隧道是单个。所以我们需要引入组(group)和克隆(clone)的概念。

知识点补充:

  • 组对象group:相当于一个身体有胳膊、头、腿,组成一个组。每个人组合可以再次分一个组。
  • 克隆clone:字面意思就是克隆一个一模一样的你。但是需要和copy分开。
// 加载摄像头模型
const loadCameraModel = () => {
  const mtlLoader = new MTLLoader();
  const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型
  mtlLoader.load(modelsObj.camera.mtl, material => {
    material.preload();
    // 设置材质的透明度
    // mtl文件中的材质设置到obj加载器
    loader.setMaterials(material);
    loader.load(modelsObj.camera.obj, object => {
      console.log(object);
      // 设置模型大小
      object.children[0].geometry.computeBoundingBox();
      object.children[0].geometry.center();
      modelsObj.camera.mesh = object;
      cloneCameraModel(4, 60, 180);
      cloneCameraModel(4, -200, 180);
    });
  });
};
// 克隆摄像头模型
const cloneCameraModel = (cameraSize, lrInterval, baInterval) => {
  const group = new THREE.Group();
  for (let i = 0; i <= cameraSize; i += 1) {
    modelsObj[`camera${i}`] = modelsObj.camera.mesh.clone();
    modelsObj[`camera${i}`].position.set(lrInterval, 180, baInterval * (i % 2 === 0 ? -i : i));
    modelsObj[`camera${i}`].scale.set(1, 1, 1);
    group.add(modelsObj[`camera${i}`])
  }
  scene.add(group);
};

点击模型进行属性操作

这块我们需要涉及的知识点是点击操作(Raycaster)、发光部分(效果合成器,shader渲染使用)、debugger模式(gui)

首先我们实现对模型进行的点击,我们需要使用raycaster 定义全局变量mouse初始化鼠标,光线追踪。可以这样定义:

// 获取鼠标坐标 处理点击某个模型的事件
const mouse = new THREE.Vector2(); // 初始化一个2D坐标用于存储鼠标位置
const raycaster = new THREE.Raycaster(); // 初始化光线追踪

知识点补充:

光线投射raycaster:可以向特定方向投射光线,并测试哪些对象与其相交,由鼠标点击转为世界坐标的过程。就是把一个2d坐标转变成3d坐标的强大类!

我们监听屏幕点击事件

const pcanvas = document.getElementById('threeMain');
// 监听点击事件,pcanvas
pcanvas.addEventListener('click', e => onmodelclick(e)); // 监听点击

计算点击坐标,屏幕坐标系转换成世界坐标系的过程。并赋值全局变量点击模型clickModel。

const onmodelclick = event => {
  console.log(event);
  // 获取鼠标点击位置
  mouse.x = (event.clientX / sizes.width) * 2 - 1;
  mouse.y = -(event.clientY / sizes.height) * 2 + 1;
  console.log(mouse);
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children); // 获取点击到的模型的数组,从近到远排列
  // const worldPosition = new THREE.Vector3(); // 初始化一个3D坐标,用来记录模型的世界坐标

  if (intersects.length > 0) {
    clickModel = intersects[0].object; 
    outlinePass.selectedObjects = [];
    outlinePass.selectedObjects = [clickModel];
  }
};

实现点击模型发光效果

threejs提供了一个扩展库EffectComposer.js,通过这个我们可以实现一些后期处理效果。所谓后期处理,就像ps一样,对threejs的渲染结果进行后期处理,比如添加发光效果。我们结合高亮发光描边可以实现下图发光效果。
在这里插入图片描述

  • 引入相关类
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';
  • 初始化三个全局变量
let composer;
let effectFXAA;
let outlinePass;
  • 赋值选中发光模型
const onmodelclick = event => {
...
  if (intersects.length > 0) {
    outlinePass.selectedObjects = [];
    outlinePass.selectedObjects = [clickModel];
  }
};
  • 初始化加载发光效果
// 效果合成器,shader渲染使用
const initEffectComposer = () => {
  // 处理模型闪烁问题【优化展示网格闪烁】
  // const parameters = { format: THREE.RGBAFormat };
  // const size = renderer.getDrawingBufferSize(new THREE.Vector2());
  // const renderTarget = new THREE.WebGLMultipleRenderTargets(size.width, size.height, parameters);

  composer = new EffectComposer(renderer);

  const renderPass = new RenderPass(scene, camera);
  composer.addPass(renderPass);

  outlinePass = new OutlinePass(new THREE.Vector2(sizes.width, sizes.height), scene, camera);
  outlinePass.visibleEdgeColor.set(255, 255, 0);
  outlinePass.edgeStrength = 1.0; // 边框的亮度
  outlinePass.edgeGlow = 1; // 光晕[0,1]
  outlinePass.usePatternTexture = false; // 是否使用父级的材质
  outlinePass.edgeThickness = 1.0; // 边框宽度
  outlinePass.downSampleRatio = 1; // 边框弯曲度
  composer.addPass(outlinePass);
  const outputPass = new OutputPass();
  composer.addPass(outputPass);

  effectFXAA = new ShaderPass(FXAAShader);
  effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);
  composer.addPass(effectFXAA);
};
  • 渲染循环执行
function animation() {
  stats.update();
  renderer.render(scene, camera);
  composer.render();
  // mesh.rotateY(0.01);
  requestAnimationFrame(animation);
}

debugger模式 这节主要涉及gui,并且补充一下阴影的知识。gui是一个图形用户界面工具,我们可以通过这个工具实现对属性进行动态的操作,很方便。下面标红的就是我们的界面工具
在这里插入图片描述

我们通过增加点光源来举个例子。

  • 首先我们初始化全局变量gui并且赋值
// debugger
let gui;
function initDebugger() {
  gui = new GUI();
}
  • 定义全局变量debugObject需要改变的属性。
// debugger属性
const debugObject = {
  light: {
    pointLight: {
      color: 0xff0000,
      position: {
        x: 0,
        y: 400,
        z: 1800,
      },
    },
  },
};

  • 定义点光源,对点光源的位置和颜色属性动态切换
// 点光源
const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);
pointLight.castShadow = true;
pointLight.position.set(100, 100, 300);
scene.add(pointLight);
const pointLightFolder = lightFolder.addFolder('点光源');
pointLightFolder.addColor(debuggerPointLight, 'color').onChange(function(value) {
  pointLight.color.set(value);
});
// 点光源位置
pointLightFolder.add(debuggerPointLight.position, 'x', -1000, 1000).onChange(function(value) {
  pointLight.position.x = value;
  pointLightHelper.update();
});
pointLightFolder.add(debuggerPointLight.position, 'y', -1000, 1000).onChange(function(value) {
  pointLight.position.y = value;
  pointLightHelper.update();
});
pointLightFolder.add(debuggerPointLight.position, 'z', -1000, 1000).onChange(function(value) {
  pointLight.position.z = value;
  pointLightHelper.update();
});

实现效果如图
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 开启阴影

阴影渲染

renderer = new THREE.WebGLRenderer({
  ...
});
renderer.shadowMap.enabled = true;

点光源投射光影

const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);
pointLight.castShadow = true;

模型和道路接受阴影和投射阴影

plane.receiveShadow = true;
loader.load(modelsObj.tunnelWall.obj, object => {
  object.traverse(obj => {
    if (obj.castShadow !== undefined) {
      // 开启投射影响
      // eslint-disable-next-line no-param-reassign
      obj.castShadow = true;
      // 开启被投射阴影
      // eslint-disable-next-line no-param-reassign
      obj.receiveShadow = true;
    }
  });

性能监视器stats

一个计算渲染分辨率FPS的工具,在这里提一下。
在这里插入图片描述

引入

import Stats from 'three/examples/jsm/libs/stats.module';

使用

// 性能监视器
let stats;

document.getElementById('threeMain').appendChild(stats.domElement);

function initStats() {
  stats = new Stats();
  stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom
}


function animation() {
  stats.update();
  renderer.render(scene, camera);
  composer.render();
  // mesh.rotateY(0.01);
  requestAnimationFrame(animation);
}

总结

这是我们实现目标的一个小小起点,属于冰山一角,前路漫漫,还需要阅读很多知识文档和试错阶段,如果你对后续感兴趣的话,可以跟进一下呀!谢谢!

完整代码

import React, { useEffect } from 'react';
import * as THREE from 'three';
// eslint-disable-next-line import/extensions
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// eslint-disable-next-line import/extensions
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import Stats from 'three/examples/jsm/libs/stats.module';
// eslint-disable-next-line import/extensions
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';

export default function ThreeVisual() {
  // 场景
  let scene;
  // 相机
  let camera;
  // 控制器
  let controls;
  // 网络模型
  let mesh;
  // 渲染器
  let renderer;
  // 性能监视器
  let stats;
  // debugger
  let gui;
  // 当前点击模型
  let clickModel;

  // 当前点击需要使用的
  let composer;
  let effectFXAA;
  let outlinePass;

  // debugger属性
  const debugObject = {
    light: {
      amlight: {
        color: 0xffffff,
      },
      directionalLight: {
        color: 0xffffff,
        position: {
          x: 0,
          y: 400,
          z: 1800,
        },
      },
      pointLight: {
        color: 0xff0000,
        position: {
          x: 0,
          y: 400,
          z: 1800,
        },
      },
    },
    model: {
      wall: {
        position: {
          x: 0,
          y: 210,
          z: 0,
        },
        scale: 0.12,
        opacity: {
          wallTopOpa: 0.4,
          wallSideOpa: 1,
        },
      },
      camera: {
        position: {
          x: 100,
          y: 100,
          z: 100,
        },
        scale: 1,
      },
    },
  };

  // 模型对象
  const modelsObj = {
    tunnelWall: {
      mtl: '/model/tunnelWall.mtl',
      obj: '/model/tunnelWall.obj',
      mesh: null,
    },

    camera: {
      mtl: '/model/camera/摄像头方.mtl',
      obj: '/model/camera/摄像头方.obj',
      mesh: null,
    },
  };

  const sizes = {
    width: window.innerWidth,
    height: window.innerHeight,
  };

  // 获取鼠标坐标 处理点击某个模型的事件
  const mouse = new THREE.Vector2(); // 初始化一个2D坐标用于存储鼠标位置
  const raycaster = new THREE.Raycaster(); // 初始化光线追踪

  useEffect(() => {
    // eslint-disable-next-line no-use-before-define
    threeStart();
  }, []);

  const initThree = () => {
    const width = document.getElementById('threeMain').clientWidth;
    const height = document.getElementById('threeMain').clientHeight;
    renderer = new THREE.WebGLRenderer({
      antialias: true,
      logarithmicDepthBuffer: true,
    });
    renderer.shadowMap.enabled = true;
    renderer.setSize(width, height);

    document.getElementById('threeMain').appendChild(renderer.domElement);
    // renderer.setClearColor(0xFFFFFF, 1.0);
    document.getElementById('threeMain').appendChild(stats.domElement);
  };

  const initCamera = (width, height) => {
    camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);
    camera.position.x = 0;
    camera.position.y = 500;
    camera.position.z = 1300;
    camera.up.x = 0;
    camera.up.y = 1;
    camera.up.z = 0;
    camera.lookAt({
      x: 0,
      y: 0,
      z: 0,
    });

    // 创建相机视锥体辅助对象
    // const cameraPerspectiveHelper = new THREE.CameraHelper(camera);
    // scene.add(cameraPerspectiveHelper);
  };

  const initScene = () => {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xbfd1e5);
  };

  const initLight = () => {
    const lightFolder = gui.addFolder('光');
    const {
      directionalLight: debuggerDirectionalLight,
      pointLight: debuggerPointLight,
    } = debugObject.light;
    // 环境光
    // const amlight = new THREE.AmbientLight(debugObject.light.amlight.color);
    // amlight.position.set(1000, 1000, 1000);
    // scene.add(amlight);
    // // 环境光debugger
    // const amlightFolder=lightFolder.addFolder("环境光")
    // amlightFolder.addColor(debugObject.light.amlight, 'color').onChange(function(value){
    //   amlight.color.set(value);
    // });
    // 平行光
    // 创建平行光,颜色为白色,强度为 10
    const directionalLight = new THREE.DirectionalLight(debuggerDirectionalLight.color, 1);
    // 设置平行光的方向
    directionalLight.position.set(0, 400, 1000);
    directionalLight.castShadow = true;
    const directonalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 20);
    // scene.add(directonalLightHelper);
    scene.add(directionalLight);
    // 平行光debugger
    const directionalLightFolder = lightFolder.addFolder('平行光');
    directionalLightFolder.addColor(debuggerDirectionalLight, 'color').onChange(function(value) {
      directionalLight.color.set(value);
    });
    // 平行光位置
    directionalLightFolder
      .add(debuggerDirectionalLight.position, 'x', -1000, 1000)
      .onChange(function(value) {
        directionalLight.position.x = value;
        directonalLightHelper.update();
      });
    directionalLightFolder
      .add(debuggerDirectionalLight.position, 'y', -1000, 1000)
      .onChange(function(value) {
        directionalLight.position.y = value;
        directonalLightHelper.update();
      });
    directionalLightFolder
      .add(debuggerDirectionalLight.position, 'z', -1000, 1000)
      .onChange(function(value) {
        directionalLight.position.z = value;
        directonalLightHelper.update();
      });
    // 点光源
    const pointLight = new THREE.PointLight(debuggerPointLight.color, 1);
    pointLight.castShadow = true;
    pointLight.position.set(100, 100, 300);
    const sphereSize = 10;
    const pointLightHelper = new THREE.PointLightHelper(pointLight, sphereSize);
    scene.add(pointLight);
    scene.add(pointLightHelper);
    const pointLightFolder = lightFolder.addFolder('点光源');
    pointLightFolder.addColor(debuggerPointLight, 'color').onChange(function(value) {
      pointLight.color.set(value);
    });
    // 点光源位置
    pointLightFolder.add(debuggerPointLight.position, 'x', -1000, 1000).onChange(function(value) {
      pointLight.position.x = value;
      pointLightHelper.update();
    });
    pointLightFolder.add(debuggerPointLight.position, 'y', -1000, 1000).onChange(function(value) {
      pointLight.position.y = value;
      pointLightHelper.update();
    });
    pointLightFolder.add(debuggerPointLight.position, 'z', -1000, 1000).onChange(function(value) {
      pointLight.position.z = value;
      pointLightHelper.update();
    });
  };

  const initObject = () => {
    const geometry = new THREE.BoxGeometry(3000, 6, 2400);
    const loader = new THREE.TextureLoader();
    const texture = loader.load('/model/route.png', function(t) {
      // eslint-disable-next-line no-param-reassign,no-multi-assign
      t.wrapS = t.wrapT = THREE.RepeatWrapping;
      t.repeat.set(1, 1);
    });
    const material = new THREE.MeshBasicMaterial({ color: 0xcccccc });
    geometry.position = new THREE.Vector3(0, 0, 0);
    mesh = new THREE.Mesh(geometry, [material, material, material, material, material, material]);
    mesh.receiveShadow = true; // cast投射,方块投射阴影
    scene.add(mesh);
    // 平面
    const geometryRoute = new THREE.PlaneGeometry(1024, 2400);
    const materialRoute = new THREE.MeshStandardMaterial({
      map: texture, // 使用纹理贴图
      side: THREE.BackSide, // 两面都渲染
    });
    const plane = new THREE.Mesh(geometryRoute, materialRoute);
    plane.receiveShadow = true;
    plane.position.set(0, 8, 0);
    plane.rotateX(Math.PI / 2);
    scene.add(plane);
  };

  const initControl = () => {
    // 将renderer关联到container,这个过程类似于获取canvas元素
    const pcanvas = document.getElementById('threeMain');

    controls = new OrbitControls(camera, pcanvas);

    // 如果使用animate方法时,将此函数删除
    // controls.addEventListener( 'change', render );
    // 使动画循环使用时阻尼或自转 意思是否有惯性
    controls.enableDamping = true;
    // 动态阻尼系数 就是鼠标拖拽旋转灵敏度
    // controls.dampingFactor = 0.25;
    // 是否可以缩放
    controls.enableZoom = true;
    // 是否自动旋转
    // controls.autoRotate = true;
    controls.autoRotateSpeed = 0.5;
    // 设置相机距离原点的最近距离
    // controls.minDistance  = 10;
    // 设置相机距离原点的最远距离
    controls.maxDistance = 10000;
    // 是否开启右键拖拽
    controls.enablePan = true;
  };

  const onmodelclick = event => {
    console.log(event);
    // 获取鼠标点击位置
    mouse.x = (event.clientX / sizes.width) * 2 - 1;
    mouse.y = -(event.clientY / sizes.height) * 2 + 1;
    console.log(mouse);
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children); // 获取点击到的模型的数组,从近到远排列
    // const worldPosition = new THREE.Vector3(); // 初始化一个3D坐标,用来记录模型的世界坐标

    if (intersects.length > 0) {
      clickModel = intersects[0].object;
      outlinePass.selectedObjects = [];
      outlinePass.selectedObjects = [clickModel];
      // intersects[0].object.getWorldPosition(worldPosition); // 将点中的3D模型坐标记录到worldPosition中
      // const texture = new THREE.TextureLoader().load("/model/route.png");
      // const spriteMaterial = new THREE.SpriteMaterial({
      //   map: texture,// 设置精灵纹理贴图
      // });
      // const sprite = new THREE.Sprite(spriteMaterial); // 精灵模型,不管从哪个角度看都可以一直面对你
      // scene.add(sprite);
      // sprite.scale.set(40,40,40);
      // sprite.position.set(worldPosition.x, worldPosition.y + 8, worldPosition.z); // 根据刚才获取的世界坐标设置精灵模型位置,高度加了3,是为了使精灵模型显示在点击模型的上方
    }
  };

  const initEvent = () => {
    window.addEventListener('resize', () => {
      // Update sizes
      sizes.width = window.innerWidth;
      sizes.height = window.innerHeight;

      // Update camera
      camera.aspect = sizes.width / sizes.height;
      camera.updateProjectionMatrix();

      // Update renderer
      renderer.setSize(sizes.width, sizes.height);
      composer.setSize(sizes.width, sizes.height);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);
    });
    const pcanvas = document.getElementById('threeMain');
    // 监听点击事件
    pcanvas.addEventListener('click', e => onmodelclick(e)); // 监听点击
  };

  const loadModel = () => {
    const mtlLoader = new MTLLoader();
    const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型
    mtlLoader.load(modelsObj.tunnelWall.mtl, material => {
      material.preload();
      // 设置材质的透明度
      // mtl文件中的材质设置到obj加载器
      loader.setMaterials(material);
      loader.load(modelsObj.tunnelWall.obj, object => {
        object.traverse(obj => {
          if (obj.castShadow !== undefined) {
            // 开启投射影响
            // eslint-disable-next-line no-param-reassign
            obj.castShadow = true;
            // 开启被投射阴影
            // eslint-disable-next-line no-param-reassign
            obj.receiveShadow = true;
          }
        });
        // 设置模型大小
        object.children[0].geometry.computeBoundingBox();
        object.children[0].geometry.center();
        // debugger模型属性
        const { scale, position, opacity } = debugObject.model.wall;
        // 模型本有属性
        const {
          scale: changeScale,
          position: changePositon,
          material: changeMaterial,
        } = object.children[0];
        changeScale.set(scale, scale, scale);
        changePositon.set(position.x, position.y, position.z);
        changeMaterial[0].transparent = true;
        changeMaterial[0].opacity = opacity.wallTopOpa;
        changeMaterial[1].transparent = true;
        changeMaterial[1].opacity = opacity.wallSideOpa;
        modelsObj.tunnelWall.mesh = object;
        scene.add(object);
        // 模型debugger
        const modelFolder = gui.addFolder('模型');
        const wallFolder = modelFolder.addFolder('墙');
        wallFolder
          .add(position, 'x', -100, 300)
          .step(0.5)
          .onChange(function(value) {
            changePositon.x = value;
          });
        wallFolder
          .add(position, 'y', -100, 300)
          .step(0.5)
          .onChange(function(value) {
            changePositon.y = value;
          });
        wallFolder
          .add(position, 'z', -100, 300)
          .step(0.5)
          .onChange(function(value) {
            changePositon.z = value;
          });
        wallFolder
          .add(debugObject.model.wall, 'scale', 0.01, 0.3)
          .step(0.001)
          .onChange(function(value) {
            changeScale.set(value, value, value);
          });
        wallFolder
          .add(opacity, 'wallTopOpa', 0, 1)
          .step(0.01)
          .onChange(function(value) {
            changeMaterial[0].opacity = value;
          });
        wallFolder
          .add(opacity, 'wallSideOpa', 0, 1)
          .step(0.01)
          .onChange(function(value) {
            changeMaterial[1].opacity = value;
          });
      });
    });
  };

  // 克隆摄像头模型
  const cloneCameraModel = (cameraSize, lrInterval, baInterval) => {
    const group = new THREE.Group();
    for (let i = 0; i <= cameraSize; i += 1) {
      modelsObj[`camera${i}`] = modelsObj.camera.mesh.clone();
      modelsObj[`camera${i}`].position.set(lrInterval, 180, baInterval * (i % 2 === 0 ? -i : i));
      modelsObj[`camera${i}`].scale.set(1, 1, 1);
      group.add(modelsObj[`camera${i}`])
    }
    scene.add(group);
  };

  // 加载摄像头模型
  const loadCameraModel = () => {
    const mtlLoader = new MTLLoader();
    const loader = new OBJLoader(); // 在init函数中,创建loader变量,用于导入模型
    mtlLoader.load(modelsObj.camera.mtl, material => {
      material.preload();
      // 设置材质的透明度
      // mtl文件中的材质设置到obj加载器
      loader.setMaterials(material);
      loader.load(modelsObj.camera.obj, object => {
        object.traverse(obj => {
          if (obj.castShadow !== undefined) {
            // 开启投射影响
            // eslint-disable-next-line no-param-reassign
            obj.castShadow = true;
            // 开启被投射阴影
            // eslint-disable-next-line no-param-reassign
            obj.receiveShadow = true;
          }
        });
        console.log(object);
        // 设置模型大小
        object.children[0].geometry.computeBoundingBox();
        object.children[0].geometry.center();
        // debugger模型属性
        object.children[0].scale.set(1, 1, 1);
        object.children[0].position.set(100, 100, 100);
        modelsObj.camera.mesh = object;
        cloneCameraModel(4, 60, 180);
        cloneCameraModel(4, -200, 180);
      });
    });
  };

  // 效果合成器,shader渲染使用
  const initEffectComposer = () => {
    // 处理模型闪烁问题【优化展示网格闪烁】
    // const parameters = { format: THREE.RGBAFormat };
    // const size = renderer.getDrawingBufferSize(new THREE.Vector2());
    // const renderTarget = new THREE.WebGLMultipleRenderTargets(size.width, size.height, parameters);

    composer = new EffectComposer(renderer);

    const renderPass = new RenderPass(scene, camera);
    composer.addPass(renderPass);

    outlinePass = new OutlinePass(new THREE.Vector2(sizes.width, sizes.height), scene, camera);
    outlinePass.visibleEdgeColor.set(255, 255, 0);
    outlinePass.edgeStrength = 1.0; // 边框的亮度
    outlinePass.edgeGlow = 1; // 光晕[0,1]
    outlinePass.usePatternTexture = false; // 是否使用父级的材质
    outlinePass.edgeThickness = 1.0; // 边框宽度
    outlinePass.downSampleRatio = 1; // 边框弯曲度
    composer.addPass(outlinePass);
    const outputPass = new OutputPass();
    composer.addPass(outputPass);

    effectFXAA = new ShaderPass(FXAAShader);
    effectFXAA.uniforms.resolution.value.set(1 / sizes.width, 1 / sizes.height);
    composer.addPass(effectFXAA);
  };

  function animation() {
    stats.update();
    renderer.render(scene, camera);
    composer.render();
    // mesh.rotateY(0.01);
    requestAnimationFrame(animation);
  }

  function initHelper() {
    // const axesHelper = new THREE.AxesHelper(3000);
    // scene.add(axesHelper);
  }

  function initStats() {
    stats = new Stats();
    stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom
  }

  function initDebugger() {
    gui = new GUI();
  }

  function threeStart() {
    initEvent();
    initStats();
    initDebugger();
    initThree();
    initScene();
    initCamera(sizes.width, sizes.height);
    initHelper();
    initLight();
    initControl();
    initObject();
    loadModel();
    loadCameraModel();
    initEffectComposer();
    animation();
  }

  return <div id="threeMain" style={{ width: '100vw', height: '100vh' }} />;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239538.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【学术会议介绍,SPIE 出版】第四届计算机图形学、人工智能与数据处理国际学术会议 (ICCAID 2024,12月13-15日)

第四届计算机图形学、人工智能与数据处理国际学术会议 2024 4th International Conference on Computer Graphics, Artificial Intelligence and Data Processing (ICCAID 2024) 重要信息 大会官网&#xff1a;www.iccaid.net 大会时间&#xff1a;2024年12月13-15日 大会地…

VScode C++配置opencv4.5.3——先赞后看,配置成功实属不易-(镜像加速环境w版和配置文件版)

前置准备&#xff1a;配置MinGW和CMake 手把手教——class1_VScode配置C环境_linux vscode cpp配置-CSDN博客文章浏览阅读398次&#xff0c;点赞4次&#xff0c;收藏6次。点击Windows x64 Installer:下载msi文件 安装完成后验证。配置gcc文件下bin环境。最后ctrlF5运行尝试。W…

机器学习 笔记

特征值提取 字典 from sklearn.extaction import DictVectorizer mDictVectorizer(sparseFalse)#sparse是否转换成三元组形式 data[], #传入字典数据 data1model.fit_transform(data) #使用API 英文特征值提取 from sklearn.feature_extraction.text import CountVe…

推荐一款好用的postman替代工具2024

Apifox 是国内团队自主研发的 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台&#xff0c;是非常好的一款 postman 替代工具。 它通过一套系统、一份数据&#xff0c;解决多个系统之间的数据同步问题。只要定义好接口文档&#xff0c;接口调试、数据 Mock、接口…

项目模块十七:HttpServer模块

一、项目模块设计思路 目的&#xff1a;实现HTTP服务器搭建 思想&#xff1a;设计请求路由表&#xff0c;记录请求方法与对应业务的处理函数映射关系。用户实现请求方法和处理函数添加到路由表&#xff0c;服务器只接受请求并调用用户的处理函数即可。 处理流程&#xff1a; …

Android音视频直播低延迟探究之:WLAN低延迟模式

Android WLAN低延迟模式 Android WLAN低延迟模式是 Android 10 引入的一种功能&#xff0c;允许对延迟敏感的应用将 Wi-Fi 配置为低延迟模式&#xff0c;以减少网络延迟&#xff0c;启动条件如下&#xff1a; Wi-Fi 已启用且设备可以访问互联网。应用已创建并获得 Wi-Fi 锁&a…

requests库如何处理 - POST请求常见的两种请求体格式:表单格式JSON格式

目录&#xff1a; 每篇前言&#xff1a;一、POST请求的两种常见请求体格式详解1. 表单格式&#xff08;form-encoded&#xff09; - 举例&#xff1a;福州搜索示例代码&#xff08;表单数据&#xff09;&#xff1a; 2. JSON格式 - 举例&#xff1a;CSDN搜索示例代码&#xff0…

HCIP-HarmonyOS Application Developer 习题(二十二)

1、用户将手机导航迁移至智能手表之后&#xff0c;智能手表如果需要获取手机传过来的数据&#xff0c;从下列哪个方法中获取? A、onCompleteContinuation() B、onStartContinuation() C、onRestoreData() D、onSaveData() 答案&#xff1a;C 分析&#xff1a;FA发起迁移后&am…

LLMs之Code:Github Spark的简介、安装和使用方法、案例应用之详细攻略

LLMs之Code&#xff1a;Github Spark的简介、安装和使用方法、案例应用之详细攻略 目录 Github Spark的简介 Github Spark的安装和使用方法 1、安装 2、使用方法 Github Spark的案例应用 Github Spark的简介 2024年10月30日&#xff0c;GitHub 重磅发布GitHub Spark 是一…

会议直击|美格智能受邀出席第三届无锡智能网联汽车生态大会,共筑汽车产业新质生产力

11月10日&#xff0c;2024世界物联网博览会分论坛——第三届无锡智能网联汽车生态大会在无锡举行&#xff0c;美格智能CEO杜国彬受邀出席&#xff0c;并参与“中央域控&#xff1a;重塑汽车智能架构的未来”主题圆桌论坛讨论&#xff0c;与行业伙伴共同探讨智能网联汽车产业领域…

HMSC联合物种分布模型在群落生态学中的贝叶斯统计分析

联合物种分布模型&#xff08;Joint Species Distribution Modelling&#xff0c;JSDM&#xff09;在生态学领域&#xff0c;特别是群落生态学中发展最为迅速&#xff0c;它在分析和解读群落生态数据的革命性和独特视角使其受到广大国内外学者的关注。在学界不同研究团队研发出…

【AI技术对电商的影响】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Python酷库之旅-第三方库Pandas(211)

目录 一、用法精讲 986、pandas.MultiIndex.get_loc方法 986-1、语法 986-2、参数 986-3、功能 986-4、返回值 986-5、说明 986-6、用法 986-6-1、数据准备 986-6-2、代码示例 986-6-3、结果输出 987、pandas.MultiIndex.get_locs方法 987-1、语法 987-2、参数 …

【go从零单排】通道select、通道timeout、Non-Blocking Channel Operations非阻塞通道操作

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 select 语句是 Go 的一种控制结构&#xff0c;用于等待多个通道操作。它类似于 s…

Java基于SpringBoot+Vue的宠物共享平台的设计与实现(附源码,文档)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

性能测试|JMeter接口与性能测试项目

前言 在软件开发和运维过程中&#xff0c;接口性能测试是一项至关重要的工作。JMeter作为一款开源的Java应用&#xff0c;被广泛用于进行各种性能测试&#xff0c;包括接口性能测试。本文将详细介绍如何使用JMeter进行接口性能测试的过程和步骤。 JMeter是Apache组织开发的基…

2025年使用 AI 识别解决 reCAPTCHA

人工智能无处不在。它是推动许多技术进步的动力&#xff0c;不断改变我们与网络的互动方式。从聊天机器人到个性化推荐&#xff0c;人工智能不再是遥远的未来概念&#xff0c;而是深深融入我们的日常生活。那么&#xff0c;当人工智能遇到 reCAPTCHA&#xff0c;一种最常见的在…

Axure PR 9 多级下拉选择器 设计交互

​ 大家好&#xff0c;我是大明同学。 Axure选择器是一种在交互设计中常用的组件&#xff0c;这期内容&#xff0c;我们来探讨Axure中多级下拉选择器设计与交互技巧。 下拉列表选择输入框元件 创建选择输入框所需的元件 1.在元件库中拖出一个矩形元件。 2.选中矩形元件&…

基础网络安全知识

1.ctfhub技能树 1.1 Web-SQL注入 Web-SQL注入-整数型 && 字符型 && MySQL结构 参考&#xff1a;5.9.6MySql注入 Web-SQL注入-报错注入 step1: 查库名 ?id1 and extractvalue(1,concat(0x7e,database(),0x7e))-- step2: 查看表名 ?id1 and extractvalue(1…

斯坦福泡茶机器人DexCap源码解析:涵盖收集数据、处理数据、模型训练三大阶段

前言 因为我司「七月在线」关于dexcap的复现/优化接近尾声了(每月逐步提高复现的效果)&#xff0c;故准备把dexcap的源码也分析下&#xff0c;11月​下旬则分析下iDP3的源码——为队伍「iDP3人形的复现/优化」助力 最开始&#xff0c;dexcap的源码分析属于此文《DexCap——斯…