当前位置: 首页 > news >正文

react+threejs实现自适应分屏查看/3D场景对比功能/双场景对比查看器

本文 实现了一个 双场景对比查看器,使用 Three.js 创建了两个不同的 3D 场景,并通过一个可拖动的滑块控制左右分屏显示比例,方便用户直观比较两个场景的差异。核心逻辑是在单个渲染器中渲染两个场景,通过裁剪(scissor)来分屏显示。

  1. 双场景渲染

    • 左侧场景 :显示一个 实体二十面体IcosahedronGeometry + MeshStandardMaterial)。

    • 右侧场景 :显示同一个几何体的 线框模式wireframe)。

  2. 滑块控制分屏

    • 用户可以通过拖动中间的 圆形滑块 调整左右视图的分割比例。

    • 滑块位置动态更新渲染区域(renderer.setScissor)。

  3. 交互功能

    • 支持 OrbitControls(鼠标拖拽旋转/缩放场景)。

    • 滑块拖动时禁用相机控制,避免冲突。

如何做到单个物体实现两个场景的不同渲染效果?

要在单个物体上实现两个场景中渲染不同类型的效果,关键在于共享几何体但使用不同材质

  1. 几何体共享

    const sharedGeometry = new THREE.IcosahedronGeometry(1, 3);
    • 两个场景使用同一个几何体实例

    • 节省内存,确保几何形状完全一致

  2. 材质分离

    // 实体材质
    const solidMaterial = new THREE.MeshStandardMaterial({...});// 线框材质 
    const wireMaterial = new THREE.MeshStandardMaterial({wireframe: true
    });
    • 相同几何体绑定不同材质

    • 材质属性完全独立设置

  3. 场景隔离

    const solidMesh = new THREE.Mesh(sharedGeometry, solidMaterial);
    sceneL.add(solidMesh);const wireMesh = new THREE.Mesh(sharedGeometry, wireMaterial);
    sceneR.add(wireMesh);
    • 每个场景包含独立的网格实例

    • 共享几何体但渲染效果不同

  4. 扩展应用

可以轻松扩展为其他对比效果:

typescript// 不同颜色对比
const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const material2 = new THREE.MeshBasicMaterial({ color: 0x00ff00 });// 不同光照效果对比
const materialA = new THREE.MeshPhongMaterial({ shininess: 100 });
const materialB = new THREE.MeshLambertMaterial();// 不同纹理对比
const textureLoader = new THREE.TextureLoader();
const materialX = new THREE.MeshStandardMaterial({ map: textureLoader.load('texture1.jpg') 
});
const materialY = new THREE.MeshStandardMaterial({map: textureLoader.load('texture2.jpg')

单个画布上同时渲染两个场景是怎么做到的?

实现在一个canvas上渲染两个独立的场景关键方法是setScissor ,通过指定渲染区域可以将canvas划分为多个独立渲染区域

setScissor 的关键作用

setScissor 是 Three.js 渲染器的一个重要方法,它定义了渲染的剪裁区域:

renderer.setScissor(x, y, width, height)

参数说明:

  • x, y: 剪裁区域的左下角坐标(相对于画布)

  • width, height: 剪裁区域的尺寸

关键特性:

  1. 剪裁测试:必须先用 setScissorTest(true) 启用剪裁测试,否则设置无效

  2. 局部渲染:所有渲染操作只会影响指定的矩形区域

  3. 性能优化:只渲染指定区域,减少不必要的绘制

自定义 render 函数的双场景渲染流程

  // 渲染函数const render =() => {if (!rendererRef.current || !cameraRef.current) returnrendererRef.current.clear()// 渲染左侧场景rendererRef.current.setScissor(0,0,sliderPosRef.current,window.innerHeight,)if (sceneLRef.current) {rendererRef.current.render(sceneLRef.current, cameraRef.current)}// 渲染右侧场景rendererRef.current.setScissor(sliderPosRef.current,0,window.innerWidth - sliderPosRef.current,window.innerHeight,)if (sceneRRef.current) {rendererRef.current.render(sceneRRef.current, cameraRef.current)}controlsRef.current?.update()requestRef.current = requestAnimationFrame(render)}

详细执行流程:

  1. 清除画布:rendererRef.current.clear() 清除整个画布,准备新帧的渲染

  2. 左侧场景渲染

    • 设置剪裁区域为从画布左边缘到滑块位置

    • 只在这个区域内渲染左侧场景

    • 示例:如果滑块在中间,则渲染左半部分

  3. 右侧场景渲染

    • 设置剪裁区域为从滑块位置到画布右边缘

    • 只在这个区域内渲染右侧场景

    • 示例:如果滑块在中间,则渲染右半部分

  4. 动画循环:通过 requestAnimationFrame 持续调用渲染函数

滑块位置移动的实现机制,如何实现左右拖拽的效果?

滑块移动是通过结合React状态管理、DOM事件处理和Three.js渲染协同工作实现的。监听鼠标点击、移动、放开时操作,实时更新滑块的位置,并更新渲染器。下面详细解析其工作原理:

核心实现要素

  1. 状态管理

    const [sliderPosition, setSliderPosition] = useState(window.innerWidth / 2)
    const sliderPosRef = useRef(sliderPosition) // 同步滑块位置的ref
  2. DOM结构

    <div ref={sliderRef} style={{ left: `${sliderPosition - 20}px` }} />

移动实现的三阶段

1. 鼠标/触摸按下阶段(pointerdown)

    const onPointerDown = (e: PointerEvent) => {if (e.isPrimary === false) returnisDragging = truestartX = e.clientX // 记录初始鼠标X位置startPos = sliderPosRef.current// 记录滑块初始位置if (controlsRef.current) controlsRef.current.enabled = false// 禁用相机控制slider.setPointerCapture(e.pointerId)// 锁定指针事件}
  • 作用:准备拖拽操作,保存初始状态

  • 关键点

    • setPointerCapture 确保后续事件即使离开滑块元素也能被捕获

    • 禁用OrbitControls防止与相机旋转冲突

2. 移动阶段(pointermove)

    const onPointerMove = (e: PointerEvent) => {if (!isDragging || e.isPrimary === false) returnconst deltaX = e.clientX - startX// 计算鼠标移动距离const newPos = Math.max(0, Math.min(window.innerWidth, startPos + deltaX))// 计算新位置setSliderPosition(newPos)// 更新React状态}
  • 位置计算

    • deltaX = 当前鼠标X - 初始鼠标X

    • newPos = 初始滑块位置 + deltaX

    • Math.max/min 约束滑块不超出窗口边界

  • 实时反馈

    • 状态更新触发React重新渲染

    • 滑块DOM元素的left样式随之更新

3. 释放阶段(pointerup)

    const onPointerUp = (e: PointerEvent) => {isDragging = falseif (controlsRef.current) controlsRef.current.enabled = true// 恢复相机控制slider.releasePointerCapture(e.pointerId)// 释放指针捕获}

事件监听

  // 滑块交互useEffect(() => {const slider = sliderRef.currentif (!slider) returnlet isDragging = falselet startX = 0let startPos = 0const onPointerDown = (e: PointerEvent) => {if (e.isPrimary === false) returnisDragging = truestartX = e.clientX // 记录初始鼠标X位置startPos = sliderPosRef.current// 记录滑块初始位置if (controlsRef.current) controlsRef.current.enabled = false// 禁用相机控制slider.setPointerCapture(e.pointerId)// 锁定指针事件}const onPointerMove = (e: PointerEvent) => {if (!isDragging || e.isPrimary === false) returnconst deltaX = e.clientX - startX// 计算鼠标移动距离const newPos = Math.max(0, Math.min(window.innerWidth, startPos + deltaX))// 计算新位置setSliderPosition(newPos)// 更新React状态}const onPointerUp = (e: PointerEvent) => {isDragging = falseif (controlsRef.current) controlsRef.current.enabled = true// 恢复相机控制slider.releasePointerCapture(e.pointerId)// 释放指针捕获}slider.addEventListener('pointerdown', onPointerDown)window.addEventListener('pointermove', onPointerMove)window.addEventListener('pointerup', onPointerUp)return () => {slider.removeEventListener('pointerdown', onPointerDown)window.removeEventListener('pointermove', onPointerMove)window.removeEventListener('pointerup', onPointerUp)}}, [])

与Three.js渲染的协同

 状态同步

  // 更新滑块位置的refuseEffect(() => {sliderPosRef.current = sliderPosition}, [sliderPosition])

渲染适应

  • 在render函数中

// 左侧剪裁区域宽度 = slider位置
renderer.setScissor(0, 0, sliderPosRef.current, height)// 右侧剪裁区域起点 = slider位置,宽度 = 总宽度 - slider位置
renderer.setScissor(sliderPosRef.current, 0, width - sliderPosRef.current, height)

跨设备兼容处理

  1. 指针事件统一

    • 使用PointerEvent同时处理鼠标和触摸事件

    • isPrimary检查避免多指触摸的干扰

  2. 触摸优化

    • CSS设置touch-action: none防止浏览器默认触摸行为

    • 指针捕获确保手指移出滑块后仍能跟踪

性能优化点

  1. 引用缓存

    • 使用sliderPosRef避免渲染循环频繁读取状态

    • 减少React状态更新对渲染循环的影响

  2. 节流渲染

    • 自然遵循requestAnimationFrame的刷新率(通常60fps)

    • 不需要额外的节流/防抖逻辑

这种实现方式通过合理分层(交互层+渲染层)实现了流畅的滑块控制体验,同时保持代码的简洁性和可维护性。

完整代码

import React, { useRef, useState, useEffect } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'const SceneComparison: React.FC = () => {const containerRef = useRef<HTMLDivElement>(null)const sliderRef = useRef<HTMLDivElement>(null)const [sliderPosition, setSliderPosition] = useState(window.innerWidth / 2)// Three.js相关引用const rendererRef = useRef<THREE.WebGLRenderer | null>(null)const cameraRef = useRef<THREE.PerspectiveCamera | null>(null)const controlsRef = useRef<OrbitControls | null>(null)const sceneLRef = useRef<THREE.Scene | null>(null)const sceneRRef = useRef<THREE.Scene | null>(null)const requestRef = useRef<number>(0)const sliderPosRef = useRef(sliderPosition) // 新增ref跟踪滑块位置// 更新滑块位置的refuseEffect(() => {sliderPosRef.current = sliderPosition}, [sliderPosition])// 渲染函数const render =() => {if (!rendererRef.current || !cameraRef.current) returnrendererRef.current.clear()// 渲染左侧场景rendererRef.current.setScissor(0,0,sliderPosRef.current,window.innerHeight,)if (sceneLRef.current) {rendererRef.current.render(sceneLRef.current, cameraRef.current)}// 渲染右侧场景rendererRef.current.setScissor(sliderPosRef.current,0,window.innerWidth - sliderPosRef.current,window.innerHeight,)if (sceneRRef.current) {rendererRef.current.render(sceneRRef.current, cameraRef.current)}controlsRef.current?.update()requestRef.current = requestAnimationFrame(render)}// 初始化场景useEffect(() => {if (!containerRef.current) return// 1. 创建渲染器createRenderer()// 2. 创建相机createCamera()// 3. 创建控制器const controls = new OrbitControls(cameraRef.current!, containerRef.current)controlsRef.current = controls// 4. 创建场景createScene()// 启动渲染循环requestRef.current = requestAnimationFrame(render)window.addEventListener('resize', handleResize)return () => {cancelAnimationFrame(requestRef.current)window.removeEventListener('resize', handleResize)if (containerRef.current && rendererRef.current?.domElement) {containerRef.current.removeChild(rendererRef.current.domElement)}}}, [])// 滑块交互useEffect(() => {const slider = sliderRef.currentif (!slider) returnlet isDragging = falselet startX = 0let startPos = 0const onPointerDown = (e: PointerEvent) => {if (e.isPrimary === false) returnisDragging = truestartX = e.clientX // 记录初始鼠标X位置startPos = sliderPosRef.current// 记录滑块初始位置if (controlsRef.current) controlsRef.current.enabled = false// 禁用相机控制slider.setPointerCapture(e.pointerId)// 锁定指针事件}const onPointerMove = (e: PointerEvent) => {if (!isDragging || e.isPrimary === false) returnconst deltaX = e.clientX - startX// 计算鼠标移动距离const newPos = Math.max(0, Math.min(window.innerWidth, startPos + deltaX))// 计算新位置setSliderPosition(newPos)// 更新React状态}const onPointerUp = (e: PointerEvent) => {isDragging = falseif (controlsRef.current) controlsRef.current.enabled = true// 恢复相机控制slider.releasePointerCapture(e.pointerId)// 释放指针捕获}slider.addEventListener('pointerdown', onPointerDown)window.addEventListener('pointermove', onPointerMove)window.addEventListener('pointerup', onPointerUp)return () => {slider.removeEventListener('pointerdown', onPointerDown)window.removeEventListener('pointermove', onPointerMove)window.removeEventListener('pointerup', onPointerUp)}}, [])//创建渲染器const createRenderer = () => {if (!containerRef.current) returnconst renderer = new THREE.WebGLRenderer({ antialias: true })renderer.setPixelRatio(window.devicePixelRatio)renderer.setSize(window.innerWidth, window.innerHeight)renderer.setScissorTest(true)rendererRef.current = renderercontainerRef.current.appendChild(renderer.domElement)}//创建相机const createCamera = () => {const camera = new THREE.PerspectiveCamera(35,window.innerWidth / window.innerHeight,0.1,100,)camera.position.z = 6cameraRef.current = camera}//创建左右场景const createScene = () => {// 创建左侧场景const sceneL = new THREE.Scene()sceneL.background = new THREE.Color(0xbcd48f)const lightL = new THREE.HemisphereLight(0xffffff, 0x444444, 3)lightL.position.set(-2, 2, 2)sceneL.add(lightL)const sharedGeometry = new THREE.IcosahedronGeometry(1, 3)const solidMesh = new THREE.Mesh(sharedGeometry,new THREE.MeshStandardMaterial(),)sceneL.add(solidMesh)sceneLRef.current = sceneL//  创建右侧场景const sceneR = new THREE.Scene()sceneR.background = new THREE.Color(0x8fbcd4)const lightR = lightL.clone()sceneR.add(lightR)const wireMesh = new THREE.Mesh(sharedGeometry.clone(),new THREE.MeshStandardMaterial({ wireframe: true }),)sceneR.add(wireMesh)sceneRRef.current = sceneR}// 窗口大小调整const handleResize = () => {if (cameraRef.current && rendererRef.current) {cameraRef.current.aspect = window.innerWidth / window.innerHeightcameraRef.current.updateProjectionMatrix()rendererRef.current.setSize(window.innerWidth, window.innerHeight)setSliderPosition(window.innerWidth / 2)}}return (<div style={{ position: 'relative', width: '100vw', height: '100vh' }}><div ref={containerRef} style={{ width: '100%', height: '100%' }} /><divref={sliderRef}style={{position: 'absolute',cursor: 'ew-resize',width: '40px',height: '40px',backgroundColor: '#F32196',opacity: 0.7,borderRadius: '50%',top: 'calc(50% - 20px)',left: `${sliderPosition - 20}px`,touchAction: 'none',}}/></div>)
}export default SceneComparison

总结

本文介绍了一种基于Three.js的双场景对比查看器实现方案。该方案通过可拖动的滑块控制左右分屏显示比例,允许用户直观比较两个3D场景的差异。核心技术要点包括:1.使用setScissor方法实现单画布双场景渲染;2.共享几何体但分别应用不同材质(实体与线框模式);3.通过React状态管理与DOM事件处理实现滑块交互;4.与OrbitControls相机控制的无缝集成。该系统具有内存高效(共享几何体)、交互流畅(60fps渲染)和扩展性强(支持多种对比效果)的特点,适用于3D模型、材质和光照效果的直观对比

http://www.dtcms.com/a/297437.html

相关文章:

  • GitHub git push 推送大文件
  • Linux: network: wireshark: tcp的segment重组是怎么判断出来的
  • Git下载与安装全攻略
  • reflections:Java非常好用的反射工具包
  • SEC_FirePower 第二天作业
  • 【深度学习新浪潮】Claude code是什么样的一款产品?
  • Keepalived 原理及配置(高可用)
  • 校园二手交易小程序的设计与实现
  • 局域网 IP地址
  • mid360连接机载电脑,远程桌面连接不上的情况
  • 智慧校园(智能出入口控制系统,考勤,消费机,电子班牌等)标准化学校建设,加速业务规模发展,满足学校、家长及学生对智能化、便捷化校园管理的需求清单如下
  • 三骏破局AI时代:电科金仓以“马背智慧”重定义数据库一体机
  • 从数据脱敏到SHAP解释:用Streamlit+XGBoost构建可复现的川崎病诊断系统
  • 12. isaacsim4.2教程-ROS 导航
  • 剖析 Web3 与传统网络模型的安全框架
  • IAR编辑器如何让左侧的工具栏显示出来?
  • Spring之【Bean后置处理器】
  • ELK Stack技术栈
  • 编译器-gcc/g++和自动化构建-make/Makefile
  • 软件工程:软件需求
  • Maximator增压器DLE 5-1-GG Artikelnr.1000015617
  • 走入Linux的世界:编辑器Vim
  • WPF高级学习(一)
  • 仙人掌cacti中的RCE案例
  • 虚拟直线阈值告警人员计数算法暑期应用
  • VoWiFi技术深度解析:架构、流程与演进
  • Oracle MCP本地部署测试
  • Oracle数据块8KB、OS默认认块管理4KB,是否需调整大小为一致?
  • CSS3新特性深度解析:Position Sticky粘性定位完整指南
  • FalconFS: Distributed File System for Large-Scale Deep Learning Pipeline——论文阅读