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

React Three Fiber 实现 3D 模型视图切换、显隐边框、显隐坐标轴

本文介绍了基于React和Three.js的3D模型视图控制实现方案。通过react-three/fiber框架,系统实现了视图切换(正视图、侧视图、俯视图、等轴视图)、边界显示控制和坐标轴显示控制等功能。关键技术点包括:1) 使用React Context管理视图状态;2) 通过ViewProvider预设不同视角的相机位置参数;3) 利用Box3Helper创建模型边界盒;4) 使用AxesHelper实现坐标轴可视化。系统采用组件化设计,将视图控制面板与模型渲染分离,通过自定义hook实现数据共享。该方案提供了直观的3D模型交互体验,适用于各类3D可视化应用场景。

视图控制对象管理 

由于控制视图的面板和canvas里加载模型的是两个独立的功能,如何进行数据通信呢?这里使用react的createContext搭配Provider进行数据注入和数据读取。

定义useView

通过createContext管理全局对象,

import { createContext, useContext } from 'react';
import * as THREE from 'three';// 定义视图类型
export type ViewType = 'front' | 'top' | 'side' | 'axis' | 'free';// 创建 Context
type ViewContextType = {currentView: ViewType;setCurrentView: (view: ViewType) => void;cameraPosition: THREE.Vector3 | null;cameraTarget: THREE.Vector3 | null;boundaryStatus: boolean;setBoundaryStatus: (status: boolean) => void;showAxis: boolean;setShowAxis: (status: boolean) => void;
};export const ViewContext = createContext<ViewContextType | undefined>(undefined);// 自定义 Hook 简化使用
export const useView = () => {const context = useContext(ViewContext);if (context === undefined) {throw new Error('useView must be used within a ViewProvider');}return context;
};

定义 ViewProvider

import {  ReactNode, useState } from 'react';
import * as THREE from 'three';
import { ViewContext, type ViewType } from '.';
const viewConfigs = {front: { position: new THREE.Vector3(0, 0,2), target: new THREE.Vector3(0, 0, 0) }, // 正视图(前)top: { position: new THREE.Vector3(0, 2, 0), target: new THREE.Vector3(0, 0, 0) }, // 俯视图(上)side: { position: new THREE.Vector3(2, 0,0), target: new THREE.Vector3(0, 0, 0) }, // 侧视图(右)axis: { position: new THREE.Vector3(1, 1, 1), target: new THREE.Vector3(0, 0, 0) }, // 轴测图free: null // 自由视角(不预设,保留当前位置)
};export const ViewProvider = ({ children }: { children: ReactNode }) => {const [currentView, setCurrentView] = useState<ViewType>('free');const [boundaryStatus, setBoundaryStatus] = useState<boolean>(false);const [showAxis, setShowAxis] = useState<boolean>(false);// 根据当前视图返回相机参数const cameraPosition = viewConfigs[currentView]?.position || null;const cameraTarget = viewConfigs[currentView]?.target || null;const value ={currentView,setCurrentView,cameraPosition,cameraTarget,boundaryStatus,setBoundaryStatus,showAxis,setShowAxis,}return (<ViewContext.Provider value={value}>{children}</ViewContext.Provider>);
};

 修改App.tsx

组件要使用context内容必须要通过ViewProvider进行包裹。这里我们直接在App.tsx将其包裹在最外层。ModelManagerProvider是用来获取几何模型数据管理几何的,配合右侧的几何管理面板使用的,在此案例中可以忽略。

import { ModelManagerProvider } from './utils/viewHelper/viewContext'
import { Home } from './views'
import { App as AntApp } from 'antd'
import { ViewProvider } from './views/ViewContext/ViewProvider'function App() {return (<ViewProvider><ModelManagerProvider><AntApp style={{ width: '100%', height: '100%' }}><Home /></AntApp></ModelManagerProvider></ViewProvider>)
}export default App

 

 CityModal加载模型

import { useGLTF } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { useModelManager } from '../../../utils/viewHelper/viewContext'
import { useView } from '../../ViewContext'export const CityModel = ({ url }: { url: string }) => {const { scene } = useGLTF(url)const modelRef = useRef<THREE.Group>(null)const helper = useModelManager()const { camera } = useThree()const { cameraPosition, cameraTarget, boundaryStatus, showAxis } = useView()console.log(cameraPosition)const raycaster = useRef(new THREE.Raycaster())const pointer = useRef(new THREE.Vector2())const boxHelperRef = useRef<THREE.Box3Helper>(null)// 坐标系辅助器引用const axesHelperRef = useRef<THREE.AxesHelper>(null)// 存储所有创建的边缘线对象const edgeLines = useRef<Map<string, THREE.LineSegments>>(new Map())useEffect(() => {if (cameraPosition && cameraTarget) {camera.position.copy(cameraPosition)camera.lookAt(cameraTarget)}}, [cameraPosition, cameraTarget])useEffect(() => {if (boxHelperRef.current) {if (boundaryStatus) {scene.add(boxHelperRef.current)} else {scene.remove(boxHelperRef.current)}}}, [boundaryStatus])// 控制坐标系显示/隐藏useEffect(() => {if (axesHelperRef.current) {if (showAxis) {scene.add(axesHelperRef.current) // 添加到场景} else {scene.remove(axesHelperRef.current)}}}, [showAxis])// 绑定点击事件useEffect(() => {window.addEventListener('click', handleClick)return () => window.removeEventListener('click', handleClick)}, [])// 模型加载后初始化useEffect(() => {if (!modelRef.current) returnaddModel()const box = new THREE.Box3().setFromObject(modelRef.current)const center = new THREE.Vector3()box.getCenter(center)const size = new THREE.Vector3()box.getSize(size)// 2. 将模型中心移到世界原点(居中)modelRef.current.position.sub(new THREE.Vector3(center.x, center.y, center.z),) // 反向移动模型,使其中心对齐原点boxHelperRef.current = new THREE.Box3Helper(box, 0x00ff00) // 绿色边框// 遍历模型设置通用属性并标记可交互addMaterial()const axisLength = Math.max(size.x, size.y, size.z)const axisPosition = new THREE.Vector3(center.x,0, // Y 轴起点:地面(Y=0)center.z, // Z 轴起点)if (!axesHelperRef.current) {axesHelperRef.current = new THREE.AxesHelper(axisLength * 2) // 参数为轴长axesHelperRef.current.position.copy(axisPosition) // 放置在模型左下角附近}}, [])//添加材质const addMaterial = () => {if (!modelRef.current) return// 遍历模型设置通用属性并标记可交互modelRef.current.traverse((child) => {if (child instanceof THREE.Mesh) {child.castShadow = truechild.receiveShadow = truechild.material.transparent = true// 标记为可交互(后续可通过此属性过滤)child.userData.interactive = trueif (!child.name.includes('River')) {child.material.color.setStyle('#0a1a3a')}addHighlight(child)// 保存原始材质(用于后续恢复或高亮逻辑)if (!child.userData.baseMaterial) {child.userData.baseMaterial = child.material // 存储原始材质}}})}// 添加边缘高亮效果const addHighlight = (object: THREE.Mesh) => {if (!object.geometry) return// 创建边缘几何体const geometry = new THREE.EdgesGeometry(object.geometry)// 创建边缘线材质const material = new THREE.LineBasicMaterial({color: 0x4c8bf5, // 蓝色边缘linewidth: 2, // 线宽})// 创建边缘线对象const line = new THREE.LineSegments(geometry, material)line.name = 'surroundLine'// 复制原始网格的变换line.position.copy(object.position)line.rotation.copy(object.rotation)line.scale.copy(object.scale)// 设置为模型的子对象,确保跟随模型变换object.add(line)edgeLines.current.set(object.uuid, line)}// 处理点击事件const handleClick = (event: MouseEvent) => {if (event.button !== 0) return// 计算点击位置的标准化设备坐标pointer.current.x = (event.clientX / window.innerWidth) * 2 - 1pointer.current.y = -(event.clientY / window.innerHeight) * 2 + 1// 执行射线检测raycaster.current.setFromCamera(pointer.current, camera)const intersects = raycaster.current.intersectObject(modelRef.current!,true,)console.log(intersects)}// 添加模型到管理器const addModel = () => {if (modelRef.current) {helper.addModel({id: '模型1',name: '模型1',url: url,model: modelRef.current,})}}return (<><primitive object={scene} ref={modelRef} /></>)
}

ViewOperation视图控制组件

import { Button, Flex, Image, Tooltip } from 'antd'
import {BorderOutlined,DragOutlined,
} from '@ant-design/icons'
import { useView } from '../ViewContext'
export const ViewOperation = () => {const {setCurrentView,boundaryStatus,setBoundaryStatus,showAxis,setShowAxis,} = useView()return (<divclassName="absolute top-[10px] left-[50%] text-white z-10"style={{ transform: 'translateX(-50%)' }}><Flex gap={10} align="center"><Tooltip title={showAxis ? '隐藏坐标系' : '显示坐标系'}><Buttontype="text"onClick={() => {setShowAxis(!showAxis)}}><DragOutlined className="text-white" /></Button></Tooltip><Tooltip title={boundaryStatus ? '隐藏边界' : '显示边界'}><Buttontype="text"onClick={() => {setBoundaryStatus(!boundaryStatus)}}><BorderOutlined className="text-white" /></Button></Tooltip><Tooltip title={'正视图'}><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('front')}}><Image src="/images/zhengshitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="俯视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('top')}}><Image src="/images/fushitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="侧视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('side')}}><Image src="/images/ceshitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="轴视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('axis')}}><Image src="/images/zhoushitu.png" width={20} preview={false} /></Button></Tooltip></Flex></div>)
}

核心功能讲解

视图切换

在ViewProvider里定义好各种视图对应的camera的位置

const viewConfigs = {front: { position: new THREE.Vector3(0, 0,2), target: new THREE.Vector3(0, 0, 0) }, // 正视图(前)top: { position: new THREE.Vector3(0, 2, 0), target: new THREE.Vector3(0, 0, 0) }, // 俯视图(上)side: { position: new THREE.Vector3(2, 0,0), target: new THREE.Vector3(0, 0, 0) }, // 侧视图(右)axis: { position: new THREE.Vector3(1, 1, 1), target: new THREE.Vector3(0, 0, 0) }, // 轴测图free: null // 自由视角(不预设,保留当前位置)
};

在视图控制组件中切换不同的视角 

      const {setCurrentView,} = useView()      <Tooltip title={'正视图'}><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('front')}}><Image src="/images/zhengshitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="俯视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('top')}}><Image src="/images/fushitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="侧视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('side')}}><Image src="/images/ceshitu.png" width={20} preview={false} /></Button></Tooltip><Tooltip title="轴视图"><Buttontype="text"onClick={(e) => {e.stopPropagation()setCurrentView('axis')}}><Image src="/images/zhoushitu.png" width={20} preview={false} /></Button></Tooltip>

 之后再模型加载组件中根据相机位置信息及时更新相机

获取相机camera对象

  const { camera } = useThree()

 获取相机位置、相机朝向

  const { cameraPosition, cameraTarget } = useView()

 通过useEffect监听cameraPosition位置变化,改变相机位置

  useEffect(() => {if (cameraPosition && cameraTarget) {camera.position.copy(cameraPosition)camera.lookAt(cameraTarget)}}, [cameraPosition, cameraTarget])

模型边框显示

模型边框显示是利用Box3Helper创建了一个box边框对象,根据是否显隐控制scene是add还是remove掉。

 防止react渲染canvas丢失数据,通常对3D对象定义的是useRef对象

  const boxHelperRef = useRef<THREE.Box3Helper>(null)

 根据boundaryStatus控制scene是否将边框对象添加入场景中

 const { scene } = useGLTF(url)const {  boundaryStatus} = useView()useEffect(() => {if (boxHelperRef.current) {if (boundaryStatus) {scene.add(boxHelperRef.current)} else {scene.remove(boxHelperRef.current)}}}, [boundaryStatus])

已加载的模型都可以通过new THREE.Box3()的setFromObject(模型)获得的box对象,然后借助Box3Helper轻松生成一个外边框模型,但是没有添加到scene中,再上面的useEffect里当boundaryStatus为true时将其添加入场景就可以显示了 

const box = new THREE.Box3().setFromObject(modelRef.current)boxHelperRef.current = new THREE.Box3Helper(box, 0x00ff00) // 绿色边框 仅创建并没添加到场景中,因此不会显示return (<><primitive object={scene} ref={modelRef} /></>)

坐标轴显示

同边框显示一样,坐标轴也是scene中的一个对象而已。

  // 坐标系辅助器引用const axesHelperRef = useRef<THREE.AxesHelper>(null)
  // 控制坐标系显示/隐藏useEffect(() => {if (axesHelperRef.current) {if (showAxis) {scene.add(axesHelperRef.current) // 添加到场景} else {scene.remove(axesHelperRef.current)}}}, [showAxis])

 这里我将坐标轴设置在模型的中心点,就要计算模型的center;

并且坐标轴要设置长度,为了包裹模型,可以设置比模型最大的长度的倍数。

    const box = new THREE.Box3().setFromObject(modelRef.current)const center = new THREE.Vector3()box.getCenter(center)const axisLength = Math.max(size.x, size.y, size.z)const axisPosition = new THREE.Vector3(center.x,0, // Y 轴起点:地面(Y=0)center.z, // Z 轴起点)if (!axesHelperRef.current) {axesHelperRef.current = new THREE.AxesHelper(axisLength * 2) // 参数为轴长axesHelperRef.current.position.copy(axisPosition) // 放置在模型左下角附近}

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

相关文章:

  • R 语言科研绘图第 64 期 --- 哑铃图
  • Python 基础语法2:组合数据类型、异常
  • Kafka——集群核心参数配置
  • TensorFlow深度学习实战(26)——生成对抗网络详解与实现
  • [Dify]-进阶1- Dify 支持的多种 AI 模型解析与选择建议
  • 排序算法(二):插入排序
  • 数据安全防护技术:筑牢信息安全防线
  • IoC容器深度解析:架构、原理与实现
  • 区块链开发协作工具全景图:从智能合约管理到去中心化治理
  • LabVIEW浏览器ActiveX事件交互
  • Oracle物化视图函数使用注意事项
  • 新型eSIM攻击技术可克隆用户资料并劫持手机身份
  • AWS RDS PostgreSQL可观测性最佳实践
  • crawl4ai--bitcointalk爬虫实战项目
  • ubuntu安装kafka(无zk版本)
  • Leaflet面试题及答案(81-100)
  • linux打包固件shell脚本
  • 打开xmind文件出现黑色
  • 理解Linux文件系统:从物理存储到统一接口
  • 【第一章编辑器开发基础第二节编辑器布局_3GUI元素和布局大小(3/4)】
  • 一般的非线性规划求解(非凸函数)
  • 自定义类型 - 联合体与枚举(百度笔试题算法优化)
  • 《C++初阶之STL》【泛型编程 + STL简介】
  • Spring原理揭秘--初识AOP
  • Spring 学习笔记
  • UI前端大数据处理新挑战:如何高效处理实时数据流?
  • JavaScript 与 C语言基础知识差别
  • GO语言中的垃圾回收(GC)
  • 怎么挑选最新贝琪入门电钢琴才高效?
  • Java进程、线程与协程对比