Threejs实现鼠标控制相机+键盘控制模型+点击指定点控制模型移动

news2024/9/23 22:36:48

1.前言

Threejs实现鼠标控制相机功能,键盘控制模型功能,点击指定点控制模型移动功能

键盘使用WASD控制模型移动效果图:
键盘使用WASD控制模型移动效果图

鼠标移动可控制相机的位置+控制模型移动到指定点效果图:
鼠标移动可控制相机的位置+控制模型移动到指定点效果图

2.功能拆分

根据以上效果图,可以得到以下三个主要实现的功能

  1. 鼠标移动可以使相机跟随
  2. 通过键盘的wasd按键可控制模型中人物的移动
  3. 可在threejs场景中任意选择一个点控制人物移动过去

3.使用到的技术栈

Vue3 + Vite + Threejs(GLTFLoader,DRACOLoader,Stats,CSS2DObject,CSS2DRenderer) + Element-Plus + GSAP

4.实现功能

1.首先创建一个基础的Vue3+Threejs模板

  1. 创建对应的工程(这里不做详细介绍了,只需要创建一个Vue3的模板就行了)
  2. 下载对应的第三方模块
    // 需要以下三个依赖
    yarn add threejs gsap element-plus
    
  3. 挂载element-plus
    import { createApp } from 'vue'
    import './style.css'
    import App from './App.vue'
    import ElementPlus from 'element-plus'; // 导入element-plus 组件库
    import 'element-plus/dist/index.css'; // 导入element-plsu css文件
    
    createApp(App).use(ElementPlus).mount('#app')
    
  4. 在项目中导入对应的资源
    import { onMounted, ref } from 'vue';
    import { ElMessage } from 'element-plus';
    // 导入threejs
    import * as THREE from 'three';
    
  5. 初始化场景,相机,渲染器等等
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 5000);
    camera.position.set(0, 83, 90);
    const renderer = new THREE.WebGL1Renderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor('#a9dfbe');
    scene.add(camera);
    
  6. 创建一个threejs的渲染函数
    // 渲染函数
    function render() {
      // 更新渲染器
      renderer.render(scene, camera);
    
      // 获取时钟从创建到当前所使用的时间
      let delta = clock.getDelta();
    
      // 根据时间差来控制动画的播放
      mixer && mixer.update(delta);
    
      // 重新渲染
      requestAnimationFrame(render);
    }
    
    onMounted(() => {
      render();
    })
    
  7. 创建地板网格与场景中的环境光,平行等等
    // 地板网格
    const gridHelper = new THREE.GridHelper(1000, 10);
    scene.add(gridHelper);
    
    // 环境光
    const light1 = new THREE.AmbientLight(0xffffff, 2); // 柔和的白光
    scene.add(light1);
    
    // 循环生成平行光
    const lightArr = [
      {
        x: 10,
        y: 100,
        z: 10,
      },
      {
        x: 20,
        y: 100,
        z: 20,
      },
      {
        x: 30,
        y: 100,
        z: 30,
      },
      {
        x: 40,
        y: 100,
        z: 40,
      },
      {
        x: 50,
        y: 100,
        z: 50,
      },
      {
        x: -10,
        y: 100,
        z: -10,
      },
      {
        x: -20,
        y: 100,
        z: -20,
      },
      {
        x: -30,
        y: 100,
        z: -30,
      },
      {
        x: -40,
        y: 100,
        z: -40,
      },
      {
        x: -50,
        y: 100,
        z: -50,
      },
    ];
    lightArr.forEach(({ x, y, z }) => {
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
      directionalLight.position.set(x, y, z);
      scene.add(directionalLight);
    });
    
  8. 导入模型,并且设置模型的贴图与材质等等
    // 加载模型
    const loadModel = () => {
      // 实例化解析器
      const gltfLoader = new GLTFLoader();
      const dracoLoader = new DRACOLoader();
    
      // 设置draco解压glft模型的解析器路径
      dracoLoader.setDecoderPath('./draco/gltf/');
    
      // 给gltf模型解析器设置解压解析器
      gltfLoader.setDRACOLoader(dracoLoader);
    
      // 导入人物模型
      gltfLoader.load('./character.glb', model => {
        // 获取模型
        character = model;
    
        // 设置相机跟随模型
        camera.lookAt(model.scene.position);
    
        // 调整模型的尺寸以及位置
        character.scene.scale.set(35, 35, 35);
    
        // 添加名称在模型上方
        addModelName(character.scene);
    
        // 模型导入场景中
        scene.add(character.scene);
    
        // 根据模型实例化动画
        mixer = new THREE.AnimationMixer(model.scene);
    
        // 模型支持的动画列表
        animations = {
          : mixer.clipAction(character.animations[0]),
          : mixer.clipAction(character.animations[1]),
          : mixer.clipAction(character.animations[2]),
          : mixer.clipAction(character.animations[3]),
        };
    
        // 默认首先执行等待动画
        animations['等'].play();
    
        // 修改当前执行的动画标识为等待
        currentAnimation = '等';
      });
    
      // 导入问号模型
      gltfLoader.load('./question.glb', model => {
        // 保存模型数据
        question = model.scene;
    
        // 设置模型的垂直位置
        question.position.y = 20;
    
        // 设置模型的大小
        question.scale.set(0.05, 0.05, 0.05);
    
        // 循环设置模型的材质
        question.traverse(function (gltf) {
          // 问号的边框
          if (gltf.name === 'questionmark_05_-_Default_0') gltf.material = new THREE.MeshStandardMaterial({ color: '#1E88E5' });
    
          // 问号的主体
          if (gltf.name === 'questionmark_03_-_Default_0') {
            // 导入色彩贴图
            let map = textureLoader.load('./color.jpg');
            // 设置色彩贴图重复的模式
            // 水平镜像重复
            map.wrapS = THREE.RepeatWrapping;
            // 垂直镜像重复
            map.wrapT = THREE.MirroredRepeatWrapping;
    
            // 导入金属贴图
            let metalness = textureLoader.load('./metalness.png');
            // 设置金属贴图重复的模式
            // 水平镜像重复
            metalness.wrapS = THREE.RepeatWrapping;
            // 垂直镜像重复
            metalness.wrapT = THREE.MirroredRepeatWrapping;
    
            // 设置问号主体模型的材质
            gltf.material = new THREE.MeshStandardMaterial({
              // 默认贴图
              map,
              // 金属程度
              metalness: 1,
              // 金属贴图
              metalnessMap: metalness,
            });
          }
        });
    
        // 模型添加到场景中
        scene.add(question);
    
        // 设置模型自动旋转
        setInterval(() => {
          if (question.rotation.y >= Math.PI * 2) question.rotation.y = 0;
          else question.rotation.y += 0.1;
        }, 30);
    
        // 默认隐藏模型,当触发点击行为时,问号将会出现在点击的位置
        question.visible = false;
      });
    };
    
  9. 实现相机视角跟随鼠标功能
    // 创建一个二维向量,存储xy
      const mouse = new THREE.Vector2();
    
      // 监听鼠标移动事件
      window.addEventListener(
        'mousemove',
        e => {
          // 非鼠标控制视图模式不触发
          if (controlType.value === 'target_point' || controlType.value === 'control') return;
    
          // 获取鼠标位置
          mouse.x = ((e.clientX / window.innerWidth) * 2 - 1) * 100;
          mouse.y = (-(e.clientY / window.innerHeight) * 2 + 1) * 100;
    
          // 将相机聚焦到这个点
          camera.lookAt(mouse.x + camera.position.x, mouse.y + camera.position.y, camera.position.z - 50);
    
          // 修改当前的控制状态为鼠标跟随状态
          controlType.value = 'mouse_follow';
        },
        false
      );
    
  10. 实现按下WASD控制模型的移动功能
    // 每次移动的距离
    const move_distance = 0.5;
    
    // 各个key的启用状态
      let keyStatus = {
        w: false,
        a: false,
        s: false,
        d: false,
      };
    
      // 相机更新定时器
      let cameraTimer = null;
    
      // 监听全局的鼠标按下方法
      window.addEventListener('keydown', ({ key }) => {
        // 模型未加载成功不处理
        if (!character) return;
    
        // 将key转换为小写,避免大写的WASD无法操作
        key = key.toLowerCase();
    
        // 判断是否点击了wasd
        if (!'wasd'.includes(key)) return;
    
        // 判断当前是否为移动到指定点状态
        if (controlType.value === 'target_point') return ElMessage.warning('当前正在移动到指定点,无法使用键盘控制');
    
        // 保存当前模型的坐标
        let position = { ...character.scene.position };
    
        // 设置控制状态为wasd移动状态
        controlType.value = 'control';
    
        // 当点击w时,自动取消掉s的移动
        key === 'w' && (keyStatus['s'] = false);
        // 当点击a时,自动取消掉d的移动
        key === 'a' && (keyStatus['d'] = false);
        // 当点击d时,自动取消掉a的移动
        key === 's' && (keyStatus['w'] = false);
        // 当点击s时,自动取消掉w的移动
        key === 'd' && (keyStatus['a'] = false);
    
        // 获取当前按下的按钮移动状态,如果当前按钮的按钮正在移动时,则不允许再次启动移动
        if (keyStatus[key]) return;
    
        // 将当前按下的按钮的状态改为true
        keyStatus[key] = true;
    
        // 首先清除一次相机更新定时器
        clearInterval(cameraTimer);
    
        // 添加相机更新
        cameraTimer = setInterval(() => {
          // 开启w按钮定时器
          keyStatus['w'] && (position.z -= move_distance);
          keyStatus['a'] && (position.x -= move_distance);
          keyStatus['d'] && (position.x += move_distance);
          keyStatus['s'] && (position.z += move_distance);
    
          // 解构获取移动后的坐标
          let { x, y, z } = position;
    
          // 更新模型的朝向(这里character.scene.position还没更新,所以还是之前的位置)
          character.scene.rotation.y = calculateAngle(character.scene.position.x, -character.scene.position.z, position.x, -position.z) - Math.PI / 2;
    
          // 更新位置
          character.scene.position.set(x, y, z);
    
          // 获取模型的位置
          const modelPosition = character.scene.position;
          // 设置相机的位置在模型的上方
          camera.position.set(modelPosition.x, 83, modelPosition.z + 90);
          // 确保相机始终朝向模型
          camera.lookAt(modelPosition);
        });
    
        // 执行走动画
        animations['走'].reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(0.2).play();
      });
    
      // 监听键盘抬起事件
      window.addEventListener('keyup', ({ key }) => {
        // 判断当前是否为移动到指定点状态
        if (controlType.value === 'target_point') return;
    
        // 模型未加载成功不处理
        if (!character) return;
    
        // 将key转换为小写,避免大写的WASD无法操作
        key = key.toLowerCase();
    
        // 设置对应按钮可继续操作
        keyStatus[key] = false;
    
        // 所有按钮都被松开
        if (!keyStatus.w && !keyStatus.a && !keyStatus.s && !keyStatus.d) {
          // 设置控制状态为鼠标跟随状态
          controlType.value = 'mouse_follow';
    
          // 清除相机更新定时器
          clearInterval(cameraTimer);
    
          // 停止走/跑运动
          animations['走'].fadeOut(0.3);
    
          // 执行等待动画
          animations['等'].reset().setEffectiveWeight(1).fadeIn(0.1).play();
        }
      });
    
  11. 实现控制模型移动到点击的地方
    // 监听点击事件,获取threejs中点击的坐标
    renderer.domElement.addEventListener('click', pickupObjects);
    
    // 拾取对象
    function pickupObjects(event) {
      // wasd控制状态无法移动到指定点
      if (controlType.value === 'control') return ElMessage.warning('当前处于键盘控制状态,无法移动到指定点');
    
      // 点击屏幕创建光线投射用于进行鼠标拾取
      var raycaster = new THREE.Raycaster();
    
      // 将鼠标位置归一化为设备坐标. x 和 y 方向的取值范围是 (-1 to +1)
      var vector = new THREE.Vector2();
      vector.x = (event.clientX / window.innerWidth) * 2 - 1;
      vector.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
      // 创建平面 设置平面法向量Vector3 和 原点到平面距离(这里表示无限平面以Y轴为标准平铺在threejs场景中的)
      var groundplane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
    
      // 通过相机和鼠标位置更新射线投射器
      raycaster.setFromCamera(vector, camera);
    
      // 接受射线和平面的交点(也就是鼠标点击的3D坐标)
      var intersectionPoint = new THREE.Vector3();
    
      // 计算射线和平面的交点
      raycaster.ray.intersectPlane(groundplane, intersectionPoint);
    
      // intersectionPoint 就是点击位置的世界坐标
      currentAnimation === '等' && modelMoveTarget(character.scene, intersectionPoint);
    
      // 设置控制状态为移动到指定点
      controlType.value = 'target_point';
    }
    
    // 模型移动动画
    const modelMoveTarget = (model, target) => {
      // 当前位置
      let originPositon = new THREE.Vector3(model.position.x, 0, model.position.z);
      // 目标位置
      let targetPositon = new THREE.Vector3(target.x, 0, target.z);
      // 计算移动距离
      let distance = originPositon.distanceTo(targetPositon);
    
      // 当前距离与目标距离之间的距离差值小于等于1则目标点在当前点的位置,不需要移动
      if (distance <= 1) return;
    
      // 添加当前点与目标点的线条
      let line = addLine(originPositon, targetPositon);
    
      // 旋转模型到目标点的方向(指定目标点相对于当前目标点的相对角度)
      model.rotation.y = calculateAngle(originPositon.x, -originPositon.z, targetPositon.x, -targetPositon.z) - Math.PI / 2;
    
      // 显示问号模型
      question.visible = true;
    
      // 设置问号模型的位置
      question.position.set(targetPositon.x, 20, targetPositon.z);
    
      // 距离小于300时执行走动画,大于300时执行跑动画
      const isFar = distance > 300;
    
      // 记录当前的动画类型
      currentAnimation = isFar ? '跑' : '走';
    
      // 将等待动画权重降低
      animations['等'].fadeOut(0.2);
    
      // 执行跑/走动画
      animations[isFar ? '跑' : '走']
        .reset()
        .setEffectiveTimeScale(isFar ? 1 : 2)
        .setEffectiveWeight(1)
        .fadeIn(0.2)
        .play();
    
      // 使用gsap动画执行运动动画
      gsap.to(model.position, {
        ...targetPositon,
        // 这里配置运动时间与距离挂钩, 距离越远, 运动时间越长
        duration: parseInt(distance) / (isFar ? 50 : 25),
        // 动画轨迹为线性轨迹
        esas: 'none',
        // 动画运动回调
        onUpdate() {
          // 更新线条的距离
          line = addLine(new THREE.Vector3(model.position.x, 0, model.position.z), targetPositon);
    
          // 当执行的整体进度大于(跑时大于0.95,走时大于0.85)时,则执行动画执行完成回调,这样会有较好的过渡效果
          if (this.progress() >= (isFar ? 0.95 : 0.85) && currentAnimation !== '等') {
            // 停止走/跑运动
            animations[isFar ? '跑' : '走'].fadeOut(0.3);
            // 执行等待动画
            animations['等'].reset().setEffectiveWeight(1).fadeIn(0.1).play();
            // 隐藏问号模型
            question.visible = false;
            // 删除距离线条
            scene.remove(line);
    
            // 设置控制状态为鼠标控制状态
            controlType.value = 'mouse_follow';
          } else {
            // 设置相机的位置在模型的上方
            camera.position.set(model.position.x, 83, model.position.z + 90);
          }
        },
        // 监听动画执行完成
        onComplete() {
          // 修改当前执行的动画
          currentAnimation = '等';
        },
      });
    };
    
    // 计算坐标2相对于坐标1的角度
    function calculateAngle(x1, y1, x2, y2) {
      // 计算坐标差值
      const dx = x2 - x1;
      const dy = y2 - y1;
    
      // 计算角度(弧度值)
      let angleRad = Math.atan2(dy, dx);
    
      return angleRad; // 直接返回弧度值,即正负π之间的值
    }
    

5.关键点与流程解释

1. 鼠标移动相机视角跟随功能:
  • 通过监听鼠标移动事件获取到当前鼠标在浏览器视图中的2D坐标
  • 之后计算转为Threejs场景中的3D的坐标(这里只有3D的x坐标与y坐标,z坐标默认为固定值)
  • 获取之后再原先相机的位置基础上加上计算得出的xy坐标即可实现此功能
2. 通过WASD控制人物模型移动功能
  • 首先这里需要两个标识变量,分别为keyStatus用于标识WASD的启用状态,cameraTimer用来更新相机的位置
  • 之后分别监听键盘按下与松开方法,判断按下的是哪个键,就设置对应键的keyStatus为true,表示在此方向移动模型
  • 这里注意,当前监听到按下W键时,应该自动将S键改为false,即使当前正在按着S键,这是为了避免W键与S键冲突,让后面按下的键可以覆盖之前按下的键的行为,并且按下S,按下A或者D时,都应该控制对应的键的状态为false
  • 然后获取移动之前的模型的坐标,注意这里使用了浅拷贝,将原先模型的xyz拷贝到一个新的对象中,避免直接修改影响模型(直接修改模型的各个方向的坐标容易导致模型闪烁)
  • 根据按下的WASD对浅拷贝出来的坐标进行修改
  • 然后创建一个定时器(cameraTimer)用于更新模型坐标以及模型的朝向,并且控制执行模型动画
  • 监听按键松开事件,判断松开的是什么按键,调整对应按键的keyStatus值为false
  • 在监听按钮松开事件中判断是否所有的keyStatus都为false,这表示WASD按键都松开了,这里处理将更新相机坐标与模型坐标定时器(cameraTimer)清除,并且停止模型的动画,执行停止动画

3. 指定点控制模型移动过去

  • 首先监听渲染器挂载的dom的点击时间,这里注意不要监听全局的点击时间,因为可能点击了其他了区域,并非Threejs的场景区域也会导致触发这里的点击
  • 监听到点击后通过点击的xy坐标计算出Threejs场景中的xy坐标
  • 设置点击的xy坐标为目标坐标,当前的xy为当前坐标,计算得出距离
  • 判断距离是否超出预设值,超出预设值则执行动画,未超出则执行动画
  • 使用gsap更新模型的坐标到目标坐标

6.结尾

以上就是本次分享的全部内容了,如果有疑问或者好的意见欢迎私信😁😁😁

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

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

相关文章

leetcode-121-买卖股票的最佳时机

原理&#xff1a; 核心原理&#xff1a; 如果我们真的在买卖股票&#xff0c;我们肯定会想&#xff1a;如果我是在历史最低点买入就好了&#xff01;该历史最低点是指卖出当天之前的历史最低点而不是全局最低点。 实现步骤&#xff1a; 1、初始化变量preprices[0]表示历史股…

20240809 每日AI必读资讯

乒乓球AI机器人赢了人类&#xff01;正反手灵活转换&#xff0c;擦网球高球都能接 - 谷歌发布首个达到人类竞技水平的机器人Agent&#xff0c;挑战乒乓球赛场。 - 机器人通过学习大量乒乓球状态数据&#xff0c;掌握了正手上旋球、反手瞄准等技能&#xff0c;展现出高速运动…

CTFHUB | web进阶 | PHP | Bypass disable_function | bypass iconv 2

开启题目 查看源码&#xff0c;发现可以蚁剑连接 进入之后无发现&#xff0c;使用插件 iconv 上传脚本 进入之后发现多了一个 .antproxy.php&#xff0c;复制文件名重新拼接连接 进入终端&#xff0c;查看根目录之后发现了有两个 flag 文件&#xff0c;之后发现了本题的 flag

STM32CUBEMX+PWM多一个尖峰的问题

问题描述&#xff1a;使用TIM2的通道3产生PWM波形&#xff0c;产生n个数量的波形后&#xff0c;在停止的时候会有一个尖峰。 怀疑是自动重载值临界的时候有问题&#xff0c;对重载值多减一个值&#xff0c;但还是有这个问题。 解决&#xff1a;电路是默认低电平&#xff0c;我…

skynet 连接redis

文章目录 概述main.luaagent.luaredis.lua 小结 概述 之前写过skynet 入门篇&#xff0c;还有skynet实操篇&#xff1b;这2篇&#xff0c;主要写了skynet如何使用&#xff0c;还有些skynet的调用流程之类。 其实&#xff0c;看过skynet的demo之后&#xff0c;发现skynet中没有…

L1-书生·浦语大模型全链路开源体系介绍

视频观看地址&#xff1a;书生浦语大模型全链路开源开放体系_哔哩哔哩_bilibili 本视频介绍了书生葡语大模型的开源开放体系&#xff0c;包括技术发展、性能提升、模型架构、开源生态等。 要点: - &#x1f31f; 开源开放体系涵盖数据收集、标注、训练、微调、评测、部署等全…

Ubuntu 系统的部署和基础操作(使用)

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言 Ubuntu 是一款基于 Debian 的开源 Linux 操作系统&#xff0c;以其易用性和强大的社区支持而广受欢迎。对于许多初次接触 Linux 的用户来说&#xff0c;Ubuntu 是理想的入门选择。本文将介绍 Ubuntu 系统的基本操作和使用…

cordova打包后请求不到接口(接口请求失败)

原因&#xff1a;CORS跨域问题导致 解决方法&#xff1a; 将根目录下的config.xml打开&#xff0c;添加 preference 即可

10分钟学会docker安装与使用

文章目录 1、docker简介2、docker的基本组成3、docker的安装与配置4、docker的常用命令 1、docker简介 什么是容器&#xff1f; 它是一种虚拟化的方案&#xff0c;是操作系统级别的虚拟化&#xff0c;只能运行相同或相似内核的操作系统&#xff0c;依赖于Linux内核特性&#x…

Qt实现圆形窗口

重新实现paintEvent()函数。 效果如下&#xff1a; 效果为蓝色区域&#xff0c;背景是vs接面&#xff0c;代码直接复制可用&#xff0c;留给有需要的人。 #ifndef CircleWidget_h__ #define CircleWidget_h__#include <QWidget>class CCircleWidget : public QWidget {Q…

MySQL安装以及配置

目录 1. MySQL安装包下载 2. 安装 3. 配置 4. 使用MySQL 5. 配置环境变量 1. MySQL安装包下载 1.1 迅雷下载 分享文件&#xff1a;MySQL安装包.zip 链接&#xff1a;https://pan.xunlei.com/s/VO3llUOt6rFFWl9TdrTrJI-cA1?pwdxere# 1.2 官网下载 MySQL :: Download MyS…

如何从戴尔笔记本电脑硬盘恢复数据

“如何从坏掉的戴尔笔记本电脑硬盘中恢复数据&#xff1f;我无法访问硬盘&#xff0c;但我确实需要从硬盘中检索数据。我有很多重要文件被困在那里。” 人们学习如何从戴尔笔记本电脑硬盘恢复数据的原因有很多&#xff0c;例如有意或无意删除、硬盘格式化、安全警告隔离受病毒…

springboot社区旧物回收系统-计算机毕业设计源码94813

目 录 摘要 1 绪论 1.1 研究背景 1.2研究意义 1.3论文结构与章节安排 2 社区旧物回收系统系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 社…

适合出行的蓝牙耳机推荐有吗?盘点4款开放式耳机排行版前十名

如果说出行想要佩戴耳机&#xff0c;但是又不知道选什么样的耳机&#xff0c;那其实你可以看看我的建议。因为我自己其实是个比较爱玩爱出去乱逛且选择恐惧症的耳机重度患者&#xff0c;所以就平时经常会跑去公园散步戴耳机听歌&#xff0c;或者是自己去野炊之类的&#xff1b;…

二叉树的遍历与根据遍历序列求二叉树

二叉树的遍历&#xff1a; 1、先序遍历&#xff1a;DLR 2、中序遍历: LDR 3、后序遍历: LRD (L表示遍历左子树&#xff0c;D表示遍历根结点&#xff0c;R表示遍历右子树&#xff09; 以下图举例说明&#xff1a; 以先序遍历为例&#xff1a; 1、因为先序遍历的规则为D…

直击Vue2/3watch的底层逻辑,字符串长度对侦听效率的影响

目录 直击Vue2/3watch的底层逻辑&#xff0c;字符串长度对侦听效率的影响 一、Vue 2的底层原理 二、Vue 3的底层原理 三、基础类型性能消耗 四、数据变化比较原理 1、Vue 2 中的引用类型比较 2、Vue 3 中的引用类型比较 3、字符串比较&#xff08;基础类型比较&#xf…

数据结构(学习)2024.8.6(顺序表)

今天开始学习数据结构的相关知识&#xff0c;大概分为了解数据结构、算法&#xff1b;学习线性表&#xff1a;顺序表、链表、栈、队列的相关知识和树&#xff1a;二叉树、遍历、创建&#xff0c;查询方法、排序方式等。 目录 一、数据结构 数据 逻辑结构 1.线性结构 2.树…

土压力计的基本工作原理:振弦式土压力计的奥秘

在土木工程、地质勘探及地下结构工程中&#xff0c;土压力计作为一种重要的监测设备&#xff0c;扮演着至关重要的角色。它能够实时、准确地测量土体内部的压力变化&#xff0c;为工程的安全性和稳定性提供可靠的数据支持。其中&#xff0c;振弦式土压力计以其高精度、稳定性好…

20-导线及NetLabel的添加

1.放置电气导线&#xff1a;ctrlw 2.放置网络标签

FCBFormer复现

表1 Kvasir-SEG复现结果&#xff1a;mDice&#xff1a;0.93846&#xff0c;mIoU&#xff1a;0.890299&#xff0c;mPrec.&#xff1a;0.945872&#xff0c;mRec.&#xff1a;0.940086 CVC-ClinicDB复现结果&#xff1a;mDice&#xff1a;0.946874&#xff0c;mIoU&#xff1a;0…