Scene Management ---Node Update (2)

Scene Management ---Node Update (2)

作者:clayman

仅供个人学习使用,请勿转载,勿用于任何商业用途。

  上一篇文章,我介绍了"理想"状态下的节点更新方法。所谓"理想",指的是除了父子节点之间的依赖关系外,节点之间不再有其他依赖关系。不幸的是,横向依赖通常广泛存在于游戏系统中,最明显的例子就是物理/碰撞检测系统。假设下图中,所有节点都有相应的物理模拟对象,并且所有节点间都是"可碰撞的",因此,更新a节点位置的时候,可能需要知道b节点的状态,反过来,更新b节点的,又需要a节点的信息,形成了一种死锁状态。

 

    此外,每一次更新开始之前,所有物体状态都处于上次更新的时间T_last之后。无论用什么方法解决上述横向依赖问题,也不管是单线程还是多线程的更新方式,同一时刻T下,只能对某一个或者几个对象进行更新,这就必然导致了整个更新过程中对象状态不一致的情况。比如,假设每次更新过后,下一次更新之前,所有物体都处于T_current时刻的状态。当在T时刻更新节点c时,可能看到a已经处于T_current状态,但b尚未更新,仍处于T_last状态,为了避免之前介绍的死锁依赖状态,c不得不在a,b等周围对象状态不一致的情况下,做出更新。

    可见,节点更新并非简单遍历所有节点即可,对于物体间相互依赖,状态不一致,也很难有完美的解决方案。本想对这个问题仔细进行深入讨论,可Jason Gregory已经在<<Game Engine Architechture>>中进行了比我更加专业的讨论,更幸运的是这部分刚好作为试读章节,下面就是我从试读章节中扒下来的内容-_-#.............................

 

14.6.3

Object State Inconsistencies and One-Frame-Off Lag

         Let's revisit game object updating, but this time thinking in terms of each object's local notion of time.The state of game object i at time t can be denoted by a state vector Si(t). When we update a game object, we are converting its previous state vector Si(t1) into a new current state vector Si(t2) (where t2 = t1 + Δt).

 

        In theory, the states of all game objects are updated from time t1 to time t2 instantaneously and in parallel, as depicted in Figure 14.15. However, in practice, we can only update the objects one by one -- we must loop over each game object and call some kind of update function on each one in turn. If we were to stop the program half-way through this update loop, half of our game objects' states would have been updated to Si(t2), while the remaining half would still be in their previous states, Si(t1). This implies that if we were to ask two of our game objects what the current time is during the update loop, they may or may not agree! What's more, depending on where exactly we interrupt the update loop, the objects may all be in a partially updated state. For example, animation pose blending may have been run, but physics and collision resolution may not yet have been applied. This leads us to the following rule:

 

The states of all game objects are consistent before and after the update loop, but they may be inconsistent during it.  

In theory, the states of all game objects are updated instantaneously and in parallel during each iteration of the game loop.

In practice, the states of the game objects are updated one by one. This means that at some arbitrary moment during the update loop, some objects will think the current time is t2 while others think it is still t1. Some objects may be only partially updated, so their states will be internally inconsistent. In effect, the state of such an object lies at a point between t1 and t2.

 

 

Object State Caching

         As described above, one solution to this problem is to group the game objects into buckets (Section 14.6.3.2). One problem with a simple bucketed update approach is that it imposes somewhat arbitrary limitations on the way in which game objects are permitted to query one another for state information. If a game object A wants the updated state vector SB(t2) of another object B, then object B must reside in a previously updated bucket. Likewise, if object A wants the previous state vector SB(t1) of object B, then object B must reside in a yet-to-be-updated bucket. Object A should never ask for the state vector of an object within its own bucket, because as we stated in the rule above, those state vectors may be only partially updated.

 

         One way to improve consistency is to arrange for each game object to cache its previous state vector Si(t1) while it is calculating its new state vector Si(t2) rather than overwriting it in-place during its update. This has two immediate benefits. First, it allows any object to safely query the previous state vector of any other object without regard to update order. Second, it guarantees that a totally consistent state vector (Si(t1)) will always be available, even during the update of the new state vector. To my knowledge there is no standard terminology for this technique, so I'll call it state caching for lack of a better name.

 

       Another benefit of state caching is that we can linearly interpolate between the previous and next states in order to approximate the state of an object at any moment between these two points in time. The Havok physics engine maintains the previous and current state of every rigid body in the simulation for just this purpose.

The downside of state caching is that it consumes twice the memory of the update-in-place approach. It also only solves half the problem, because while the previous states at time t1 are fully consistent, the new states at time t2 still suffer from potential inconsistency. Nonetheless, the technique can be useful when applied judiciously.

 

Time-Stamping

         One easy and low-cost way to improve the consistency of game object states is to time-stamp them. It is then a trivial matter to determine whether a game object's state vector corresponds to its configuration at a previous time or the current time. Any code that queries the state of another game object during the update loop can assert or explicitly check the time stamp to ensure that the proper state information is being obtained.

 

        Time-stamping does not address the inconsistency of states during the update of a bucket. However, we can set a global or static variable to reflect which bucket is currently being updated. Presumably every game object "knows" in which bucket it resides. So we can check the bucket of a queried game object against the currently updating bucket and assert that they are not equal in order to guard against inconsistent state queries.

 

----Quote from <<Game Engine Architecture>>  14.6.3 ,author:Jason Gregory

 

 

 

转载于:https://www.cnblogs.com/clayman/archive/2010/06/02/1750067.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D文物模型标注平台</title> <style> body { margin: 0; padding: 0; font-family: Arial, sans-serif; overflow: hidden; background-color: #333; color: #eee; } #top-menu { height: 40px; background-color: #222; color: #ddd; display: flex; align-items: center; padding: 0 10px; border-bottom: 1px solid #444; } .menu-item { position: relative; padding: 10px 15px; cursor: pointer; } .menu-item:hover { background-color: #444; } .menu-item:hover .submenu { display: block; } .submenu { display: none; position: absolute; top: 100%; left: 0; background-color: #333; min-width: 200px; z-index: 1000; border: 1px solid #444; box-shadow: 0 2px 5px rgba(0,0,0,0.5); } .submenu button { display: block; width: 100%; padding: 8px 15px; text-align: left; background: none; border: none; color: #ddd; cursor: pointer; border-bottom: 1px solid #444; } .submenu button:hover { background-color: #444; } #container { display: flex; height: calc(100vh - 40px); } #sidebar { width: 250px; background-color: #2a2a2a; padding: 10px; overflow-y: auto; border-right: 1px solid #444; resize: horizontal; min-width: 200px; max-width: 500px; overflow-x: hidden; } #viewer { flex: 1; position: relative; background-color: #1a1a1a; } .view-toolbar { position: absolute; top: 10px; right: 10px; z-index: 100; background: rgba(40,40,40,0.8); padding: 5px; border-radius: 3px; display: flex; flex-direction: column; gap: 3px; border: 1px solid #444; } .annotation-list { margin-top: 10px; } .annotation-item { padding: 6px; margin-bottom: 3px; background: #333; border-radius: 3px; cursor: pointer; display: flex; justify-content: space-between; font-size: 13px; border: 1px solid #444; } .annotation-item:hover { background-color: #444; } .annotation-item.selected { background-color: #4CAF50; color: white; } .file-input { display: none; } .status-bar { position: absolute; bottom: 5px; left: 5px; background: rgba(0,0,0,0.7); color: #ddd; padding: 3px 8px; border-radius: 3px; font-size: 12px; border: 1px solid #444; } button { padding: 3px 8px; cursor: pointer; border: 1px solid #555; background: #333; border-radius: 3px; font-size: 12px; color: #ddd; } button:hover { background: #444; } button.active { background: #4CAF50; color: white; } .view-btn { width: 80px; margin: 2px 0; } h3 { color: #ddd; border-bottom: 1px solid #444; padding-bottom: 5px; } .resize-handle { width: 5px; height: 100%; position: absolute; right: 0; top: 0; cursor: col-resize; } </style> </head> <body> <div id="top-menu"> <div class="menu-item"> 文件 <div class="submenu"> <button id="open-file">打开模型...</button> <input type="file" id="file-input" class="file-input" accept=".obj,.gltf,.glb,.3ds,.fbx,.dae,.stl,.ply"> <button id="save-file">保存模型</button> <button id="export-model">导出模型...</button> <button id="save-annotations">保存标注</button> <button id="export-annotations">导出标注...</button> </div> </div> <div class="menu-item"> 编辑 <div class="submenu"> <button id="undo-btn">撤销</button> <button id="redo-btn">恢复</button> <button id="delete-btn">删除</button> <button id="clear-btn">清空标注</button> <button id="restore-colors">还原模型颜色</button> </div> </div> <div class="menu-item"> 视图 <div class="submenu"> <button id="fit-view">适应视图</button> <button id="toggle-grid">显示网格</button> <button id="toggle-axis">显示坐标轴</button> <button id="toggle-wireframe">线框模式</button> </div> </div> <div class="menu-item"> 标注工具 <div class="submenu"> <button id="box-tool">3D框标注</button> <button id="point-tool">点标注</button> <button id="polygon-tool">多边形标注</button> <button id="measure-tool">测量工具</button> <button id="text-tool">文字标注</button> </div> </div> </div> <div id="container"> <div id="sidebar"> <h3>标注列表</h3> <div class="annotation-list"> <div id="annotations-container"></div> </div> </div> <div id="viewer"> <div class="view-toolbar"> <button id="front-view" class="view-btn">前视图</button> <button id="back-view" class="view-btn">后视图</button> <button id="left-view" class="view-btn">左视图</button> <button id="right-view" class="view-btn">右视图</button> <button id="top-view" class="view-btn">顶视图</button> <button id="bottom-view" class="view-btn">底视图</button> <button id="perspective-view" class="view-btn">透视视图</button> <button id="orthographic-view" class="view-btn">正交视图</button> </div> <div class="status-bar" id="status-bar"> 就绪 </div> </div> </div> <!-- 引入Three.js库和相关插件 --> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/GLTFLoader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/OBJLoader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/FBXLoader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/DRACOLoader.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/exporters/GLTFExporter.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/exporters/OBJExporter.js"></script> <script> // 初始化3D场景 const scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a1a); let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 5; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.getElementById('viewer').appendChild(renderer.domElement); const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; controls.screenSpacePanning = false; controls.maxPolarAngle = Math.PI; controls.minDistance = 0.1; controls.maxDistance = 1000; // 添加光源 const ambientLight = new THREE.AmbientLight(0x404040, 1.5); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(1, 1, 1); scene.add(directionalLight); // 添加辅助工具 const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222); gridHelper.visible = true; scene.add(gridHelper); // 添加垂直网格线 const verticalGridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222); verticalGridHelper.rotation.x = Math.PI / 2; verticalGridHelper.visible = true; scene.add(verticalGridHelper); const axesHelper = new THREE.AxesHelper(5); axesHelper.visible = false; scene.add(axesHelper); // 模型和标注相关变量 let currentModel = null; let currentTool = null; let annotations = []; let history = []; let historyIndex = -1; let isWireframe = false; let selectedObject = null; let isMouseDown = false; let isRightMouseDown = false; let lastMousePosition = { x: 0, y: 0 }; let originalMaterials = new Map(); // 保存原始材质 // 文件操作功能 document.getElementById('open-file').addEventListener('click', function() { document.getElementById('file-input').click(); }); document.getElementById('file-input').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; const fileName = file.name; const fileExtension = fileName.split('.').pop().toLowerCase(); const fileURL = URL.createObjectURL(file); updateStatus(`正在加载: ${fileName}`); // 根据文件类型选择不同的加载器 try { if (currentModel) { scene.remove(currentModel); } let loader; switch(fileExtension) { case 'gltf': case 'glb': loader = new THREE.GLTFLoader(); // 添加DRACO解码器支持 const dracoLoader = new THREE.DRACOLoader(); dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/'); loader.setDRACOLoader(dracoLoader); loader.load(fileURL, function(gltf) { currentModel = gltf.scene; // 保存原始材质 originalMaterials.clear(); currentModel.traverse(child => { if (child.isMesh) { originalMaterials.set(child, child.material); } }); // 自动垂直对齐模型 alignModelVertically(currentModel); scene.add(currentModel); fitViewToObject(currentModel); updateStatus(`已加载: ${fileName}`); saveHistory(); }, function(xhr) { const percentComplete = (xhr.loaded / xhr.total) * 100; updateStatus(`加载中: ${fileName} - ${percentComplete.toFixed(2)}%`); }, function(error) { console.error('加载模型出错:', error); updateStatus(`加载失败: ${fileName}`); } ); break; case 'obj': loader = new THREE.OBJLoader(); loader.load(fileURL, function(object) { currentModel = object; // 保存原始材质 originalMaterials.clear(); currentModel.traverse(child => { if (child.isMesh) { originalMaterials.set(child, child.material); } }); // 自动垂直对齐模型 alignModelVertically(currentModel); scene.add(currentModel); fitViewToObject(currentModel); updateStatus(`已加载: ${fileName}`); saveHistory(); }, function(xhr) { const percentComplete = (xhr.loaded / xhr.total) * 100; updateStatus(`加载中: ${fileName} - ${percentComplete.toFixed(2)}%`); }, function(error) { console.error('加载模型出错:', error); updateStatus(`加载失败: ${fileName}`); } ); break; case 'fbx': loader = new THREE.FBXLoader(); loader.load(fileURL, function(object) { currentModel = object; // 保存原始材质 originalMaterials.clear(); currentModel.traverse(child => { if (child.isMesh) { originalMaterials.set(child, child.material); } }); // 自动垂直对齐模型 alignModelVertically(currentModel); scene.add(currentModel); fitViewToObject(currentModel); updateStatus(`已加载: ${fileName}`); saveHistory(); }, function(xhr) { const percentComplete = (xhr.loaded / xhr.total) * 100; updateStatus(`加载中: ${fileName} - ${percentComplete.toFixed(2)}%`); }, function(error) { console.error('加载模型出错:', error); updateStatus(`加载失败: ${fileName}`); } ); break; default: updateStatus(`不支持的文件格式: ${fileExtension}`); break; } } catch (error) { console.error('加载模型出错:', error); updateStatus(`加载失败: ${fileName}`); } }); // 自动垂直对齐模型 function alignModelVertically(model) { const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); // 假设模型高度方向是Y轴 // 计算模型需要旋转的角度,使最长边垂直 const maxDim = Math.max(size.x, size.y, size.z); // 如果最长边不是Y轴,则需要旋转模型 if (maxDim !== size.y) { // 找到最长边对应的轴 let rotationAxis = new THREE.Vector3(0, 0, 0); if (maxDim === size.x) { rotationAxis.set(0, 0, 1); // 绕Z轴旋转90度 } else if (maxDim === size.z) { rotationAxis.set(1, 0, 0); // 绕X轴旋转90度 } // 应用旋转 model.rotation.setFromVector3(rotationAxis.clone().multiplyScalar(Math.PI / 2)); // 重新计算边界框 box.setFromObject(model); const newCenter = box.getCenter(new THREE.Vector3()); // 调整位置使中心点不变 model.position.sub(newCenter).add(center); } } document.getElementById('save-file').addEventListener('click', function() { if (!currentModel) { updateStatus('没有可保存的模型'); return; } // 这里可以添加保存到服务器的逻辑 updateStatus('模型已保存'); }); document.getElementById('export-model').addEventListener('click', function() { if (!currentModel) { updateStatus('没有可导出的模型'); return; } const exporter = new THREE.GLTFExporter(); exporter.parse(currentModel, function(gltf) { const data = JSON.stringify(gltf); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'model.gltf'; a.click(); updateStatus('模型已导出为GLTF格式'); }); }); document.getElementById('save-annotations').addEventListener('click', function() { const data = JSON.stringify(annotations); localStorage.setItem('annotations', data); updateStatus('标注已保存到本地存储'); }); document.getElementById('export-annotations').addEventListener('click', function() { const data = JSON.stringify(annotations); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'annotations.json'; a.click(); updateStatus('标注已导出为JSON文件'); }); // 视图控制功能 document.getElementById('fit-view').addEventListener('click', function() { if (currentModel) { fitViewToObject(currentModel); } }); document.getElementById('toggle-grid').addEventListener('click', function() { gridHelper.visible = !gridHelper.visible; verticalGridHelper.visible = gridHelper.visible; updateStatus(gridHelper.visible ? '网格已显示' : '网格已隐藏'); }); document.getElementById('toggle-axis').addEventListener('click', function() { axesHelper.visible = !axesHelper.visible; updateStatus(axesHelper.visible ? '坐标轴已显示' : '坐标轴已隐藏'); }); document.getElementById('toggle-wireframe').addEventListener('click', function() { if (!currentModel) return; isWireframe = !isWireframe; currentModel.traverse(function(child) { if (child.isMesh) { child.material.wireframe = isWireframe; } }); updateStatus(isWireframe ? '线框模式已启用' : '线框模式已禁用'); }); // 还原模型颜色功能 document.getElementById('restore-colors').addEventListener('click', function() { if (!currentModel) { updateStatus('没有可还原颜色的模型'); return; } currentModel.traverse(child => { if (child.isMesh && originalMaterials.has(child)) { child.material = originalMaterials.get(child); } }); updateStatus('已还原模型原始颜色'); }); // 标准视图 document.getElementById('front-view').addEventListener('click', function() { setStandardView('front'); }); document.getElementById('back-view').addEventListener('click', function() { setStandardView('back'); }); document.getElementById('left-view').addEventListener('click', function() { setStandardView('left'); }); document.getElementById('right-view').addEventListener('click', function() { setStandardView('right'); }); document.getElementById('top-view').addEventListener('click', function() { setStandardView('top'); }); document.getElementById('bottom-view').addEventListener('click', function() { setStandardView('bottom'); }); document.getElementById('perspective-view').addEventListener('click', function() { if (camera instanceof THREE.OrthographicCamera) { const aspect = window.innerWidth / window.innerHeight; camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000); camera.position.copy(controls.target.clone().add(new THREE.Vector3(5, 5, 5))); camera.lookAt(controls.target); controls.object = camera; controls.update(); updateStatus('切换到透视视图'); } }); document.getElementById('orthographic-view').addEventListener('click', function() { if (camera instanceof THREE.PerspectiveCamera) { const width = 10; const height = width * (window.innerHeight / window.innerWidth); camera = new THREE.OrthographicCamera( width / -2, width / 2, height / 2, height / -2, 0.1, 1000 ); camera.position.copy(controls.target.clone().add(new THREE.Vector3(5, 5, 5))); camera.lookAt(controls.target); controls.object = camera; controls.update(); updateStatus('切换到正交视图'); } }); // 编辑功能 document.getElementById('undo-btn').addEventListener('click', function() { if (historyIndex > 0) { historyIndex--; restoreFromHistory(); updateStatus('已撤销上一步操作'); } else { updateStatus('没有可撤销的操作'); } }); document.getElementById('redo-btn').addEventListener('click', function() { if (historyIndex < history.length - 1) { historyIndex++; restoreFromHistory(); updateStatus('已恢复撤销的操作'); } else { updateStatus('没有可恢复的操作'); } }); document.getElementById('delete-btn').addEventListener('click', function() { deleteSelectedObject(); }); document.getElementById('clear-btn').addEventListener('click', function() { if (confirm('确定要清空所有标注吗?')) { clearAnnotations(); updateStatus('已清空所有标注'); } }); // 标注工具功能 document.getElementById('box-tool').addEventListener('click', function() { setCurrentTool('box'); }); document.getElementById('point-tool').addEventListener('click', function() { setCurrentTool('point'); }); document.getElementById('polygon-tool').addEventListener('click', function() { setCurrentTool('polygon'); }); document.getElementById('measure-tool').addEventListener('click', function() { setCurrentTool('measure'); }); document.getElementById('text-tool').addEventListener('click', function() { setCurrentTool('text'); }); // 设置当前工具 function setCurrentTool(tool) { currentTool = tool; updateStatus(`${getToolName(tool)}工具已激活 - 点击模型表面添加标注`); // 更新按钮状态 document.querySelectorAll('.submenu button').forEach(btn => { btn.classList.remove('active'); }); document.getElementById(`${tool}-tool`).classList.add('active'); } // 获取工具名称 function getToolName(tool) { const names = { 'box': '3D框标注', 'point': '点标注', 'polygon': '多边形标注', 'measure': '测量', 'text': '文字标注' }; return names[tool] || tool; } // 鼠标交互 renderer.domElement.addEventListener('mousedown', onMouseDown, false); renderer.domElement.addEventListener('mouseup', onMouseUp, false); renderer.domElement.addEventListener('mousemove', onMouseMove, false); renderer.domElement.addEventListener('wheel', onMouseWheel, false); renderer.domElement.addEventListener('contextmenu', function(e) { e.preventDefault(); // 阻止右键菜单 }, false); // 键盘交互 document.addEventListener('keydown', function(e) { if (e.key === 'Delete' && selectedObject) { deleteSelectedObject(); } }); // 删除选中对象 function deleteSelectedObject() { if (selectedObject) { scene.remove(selectedObject); annotations = annotations.filter(anno => anno.object !== selectedObject); updateAnnotationsList(); saveHistory(); selectedObject = null; updateStatus('已删除选中对象'); } else { updateStatus('请先选择要删除的对象'); } } // 辅助函数 function fitViewToObject(object) { const box = new THREE.Box3().setFromObject(object); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); const fov = camera.fov * (Math.PI / 180); let cameraZ = Math.abs(maxDim / (2 * Math.tan(fov / 2))); cameraZ *= 1.5; // 稍微远一点 controls.target.copy(center); camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, cameraZ))); controls.update(); updateStatus('视图已适应模型'); } function setStandardView(view) { if (!currentModel) return; const box = new THREE.Box3().setFromObject(currentModel); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); controls.target.copy(center); let position; switch(view) { case 'front': position = new THREE.Vector3(0, 0, maxDim * 1.5); updateStatus('切换到前视图'); break; case 'back': position = new THREE.Vector3(0, 0, -maxDim * 1.5); updateStatus('切换到后视图'); break; case 'left': position = new THREE.Vector3(-maxDim * 1.5, 0, 0); updateStatus('切换到左视图'); break; case 'right': position = new THREE.Vector3(maxDim * 1.5, 0, 0); updateStatus('切换到右视图'); break; case 'top': position = new THREE.Vector3(0, maxDim * 1.5, 0); updateStatus('切换到顶视图'); break; case 'bottom': position = new THREE.Vector3(0, -maxDim * 1.5, 0); updateStatus('切换到底视图'); break; } position.add(center); camera.position.copy(position); camera.lookAt(center); controls.update(); } function saveHistory() { // 简化版历史记录,实际应用中可能需要更复杂的实现 history = history.slice(0, historyIndex + 1); history.push({ annotations: JSON.parse(JSON.stringify(annotations)), model: currentModel ? currentModel.clone() : null }); historyIndex = history.length - 1; } function restoreFromHistory() { const state = history[historyIndex]; annotations = JSON.parse(JSON.stringify(state.annotations)); if (currentModel) scene.remove(currentModel); currentModel = state.model ? state.model.clone() : null; if (currentModel) scene.add(currentModel); updateAnnotationsList(); } function clearAnnotations() { // 移除所有标注对象 annotations.forEach(anno => { if (anno.object && anno.object.parent) { scene.remove(anno.object); } }); annotations = []; updateAnnotationsList(); saveHistory(); } function updateAnnotationsList() { const container = document.getElementById('annotations-container'); container.innerHTML = ''; annotations.forEach((annotation, index) => { const item = document.createElement('div'); item.className = 'annotation-item'; if (annotation.object === selectedObject) { item.classList.add('selected'); } item.innerHTML = ` <span>标注 ${index + 1} (${getToolName(annotation.type)})</span> <button class="delete-annotation" data-index="${index}">×</button> `; container.appendChild(item); item.querySelector('.delete-annotation').addEventListener('click', function(e) { e.stopPropagation(); scene.remove(annotation.object); annotations.splice(index, 1); updateAnnotationsList(); saveHistory(); updateStatus(`已删除标注 ${index + 1}`); }); item.addEventListener('click', function() { // 聚焦到该标注 controls.target.copy(annotation.object.position); controls.update(); selectedObject = annotation.object; updateAnnotationsList(); updateStatus(`已聚焦到标注 ${index + 1}`); }); }); } function updateStatus(message) { document.getElementById('status-bar').textContent = message; } // 鼠标交互函数 function onMouseDown(event) { if (event.button === 2) { // 右键 isRightMouseDown = true; return; } if (event.button !== 0) return; // 只处理左键 isMouseDown = true; lastMousePosition = { x: event.clientX, y: event.clientY }; // 检查是否点击了模型或标注 const mouse = new THREE.Vector2( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1 ); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); if (intersects.length > 0) { selectedObject = intersects[0].object; // 如果是标注工具激活状态,创建新标注 if (currentTool) { createAnnotation(intersects[0].point); } else { updateStatus(`已选中对象: ${selectedObject.type}`); updateAnnotationsList(); } } else { selectedObject = null; updateAnnotationsList(); } } function onMouseUp(event) { if (event.button === 2) { // 右键 isRightMouseDown = false; } if (event.button === 0) { // 左键 isMouseDown = false; } } function onMouseMove(event) { if (isRightMouseDown) { // 右键拖动旋转视图 const deltaMove = { x: event.clientX - lastMousePosition.x, y: event.clientY - lastMousePosition.y }; controls.rotateLeft(2 * Math.PI * deltaMove.x / window.innerWidth); controls.rotateUp(2 * Math.PI * deltaMove.y / window.innerHeight); } else if (isMouseDown && !currentTool) { // 左键拖动旋转视图 const deltaMove = { x: event.clientX - lastMousePosition.x, y: event.clientY - lastMousePosition.y }; controls.rotateLeft(2 * Math.PI * deltaMove.x / window.innerWidth); controls.rotateUp(2 * Math.PI * deltaMove.y / window.innerHeight); } lastMousePosition = { x: event.clientX, y: event.clientY }; } function onMouseWheel(event) { // 缩放控制 const delta = Math.sign(event.deltaY); const zoomSpeed = 0.1; if (delta > 0) { controls.dollyOut(zoomSpeed); } else { controls.dollyIn(zoomSpeed); } controls.update(); } // 创建标注函数 function createAnnotation(position) { let annotation; switch(currentTool) { case 'point': annotation = createPointAnnotation(position); break; case 'box': annotation = createBoxAnnotation(position); break; case 'polygon': annotation = createPolygonAnnotation(position); break; case 'measure': annotation = createMeasureAnnotation(position); break; case 'text': annotation = createTextAnnotation(position); break; } if (annotation) { scene.add(annotation); annotations.push({ type: currentTool, object: annotation, position: position }); selectedObject = annotation; updateAnnotationsList(); saveHistory(); updateStatus(`已添加${getToolName(currentTool)}标注`); } } // 创建点标注 function createPointAnnotation(position) { const geometry = new THREE.SphereGeometry(0.05, 16, 16); const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const sphere = new THREE.Mesh(geometry, material); sphere.position.copy(position); return sphere; } // 创建3D框标注 function createBoxAnnotation(position) { const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, wireframe: false }); const box = new THREE.Mesh(geometry, material); box.position.copy(position); return box; } // 创建多边形标注 function createPolygonAnnotation(position) { // 简化实现,实际应用中需要更复杂的逻辑 const geometry = new THREE.CircleGeometry(0.2, 6); const material = new THREE.MeshBasicMaterial({ color: 0x0000ff, transparent: true, opacity: 0.7 }); const circle = new THREE.Mesh(geometry, material); circle.position.copy(position); return circle; } // 创建测量标注 function createMeasureAnnotation(position) { // 简化实现,实际应用中需要更复杂的逻辑 const geometry = new THREE.CylinderGeometry(0.05, 0.05, 0.5, 16); const material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); const cylinder = new THREE.Mesh(geometry, material); cylinder.position.copy(position); return cylinder; } // 创建文字标注 function createTextAnnotation(position) { // 简化实现,实际应用中需要使用TextGeometry或CSS2D/3D渲染器 const geometry = new THREE.BoxGeometry(0.3, 0.1, 0.01); const material = new THREE.MeshBasicMaterial({ color: 0xffffff }); const cube = new THREE.Mesh(geometry, material); cube.position.copy(position); return cube; } // 动画循环 function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); // 窗口大小调整 window.addEventListener('resize', function() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); // 在动画循环中添加标注对象的更新逻辑 function animate() { requestAnimationFrame(animate); // 更新标注对象的位置和状态 updateAnnotations(); controls.update(); renderer.render(scene, camera); } // 更新标注对象的状态 function updateAnnotations() { annotations.forEach(annotation => { // 可以在这里添加标注对象的动画效果或状态更新 if (annotation.type === 'point' && annotation.object) { // 点标注的脉动效果 annotation.object.scale.x = 1 + Math.sin(Date.now() * 0.005) * 0.2; annotation.object.scale.y = 1 + Math.sin(Date.now() * 0.005) * 0.2; annotation.object.scale.z = 1 + Math.sin(Date.now() * 0.005) * 0.2; } }); } // 添加标注编辑功能 function enableAnnotationEditing() { // 双击标注可以进入编辑模式 renderer.domElement.addEventListener('dblclick', function(event) { if (!currentTool && selectedObject) { const annotation = annotations.find(anno => anno.object === selectedObject); if (annotation) { editAnnotation(annotation); } } }); } // 编辑标注 function editAnnotation(annotation) { // 根据标注类型显示不同的编辑界面 switch(annotation.type) { case 'text': editTextAnnotation(annotation); break; case 'box': editBoxAnnotation(annotation); break; // 其他标注类型的编辑逻辑... } } // 编辑文字标注 function editTextAnnotation(annotation) { const text = prompt('请输入新的标注文字:', '标注内容'); if (text !== null) { // 更新标注内容 updateStatus(`文字标注已更新: ${text}`); saveHistory(); } } // 编辑3D框标注 function editBoxAnnotation(annotation) { const size = prompt('请输入新的尺寸 (宽,高,深):', '0.5,0.5,0.5'); if (size) { const dimensions = size.split(',').map(Number); if (dimensions.length === 3 && !dimensions.some(isNaN)) { annotation.object.geometry.dispose(); annotation.object.geometry = new THREE.BoxGeometry(...dimensions); updateStatus(`3D框标注尺寸已更新: ${dimensions.join('x')}`); saveHistory(); } else { updateStatus('无效的尺寸格式,请使用"宽,高,深"格式'); } } } // 添加标注分组功能 let annotationGroups = {}; function createAnnotationGroup(name) { if (!annotationGroups[name]) { annotationGroups[name] = []; updateStatus(`已创建标注分组: ${name}`); return true; } return false; } function addToAnnotationGroup(annotation, groupName) { if (annotationGroups[groupName]) { annotationGroups[groupName].push(annotation); annotation.group = groupName; updateStatus(`标注已添加到分组: ${groupName}`); return true; } return false; } // 添加模型测量工具 function setupMeasurementTools() { let startPoint = null; let endPoint = null; let measurementLine = null; document.getElementById('measure-tool').addEventListener('click', function() { setCurrentTool('measure'); startPoint = null; endPoint = null; if (measurementLine) { scene.remove(measurementLine); measurementLine = null; } }); renderer.domElement.addEventListener('click', function(event) { if (currentTool === 'measure') { const mouse = new THREE.Vector2( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1 ); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true); if (intersects.length > 0) { if (!startPoint) { startPoint = intersects[0].point; updateStatus('测量: 已选择起点,请选择终点'); } else { endPoint = intersects[0].point; createMeasurementLine(startPoint, endPoint); startPoint = null; endPoint = null; } } } }); function createMeasurementLine(start, end) { if (measurementLine) { scene.remove(measurementLine); } const geometry = new THREE.BufferGeometry().setFromPoints([start, end]); const material = new THREE.LineBasicMaterial({ color: 0xffff00 }); measurementLine = new THREE.Line(geometry, material); scene.add(measurementLine); const distance = start.distanceTo(end); updateStatus(`测量结果: ${distance.toFixed(2)} 单位`); // 添加测量标注 const annotation = { type: 'measure', object: measurementLine, start: start, end: end, distance: distance }; annotations.push(annotation); updateAnnotationsList(); saveHistory(); } } // 初始化所有功能 function initFeatures() { enableAnnotationEditing(); setupMeasurementTools(); // 添加分组管理UI setupGroupManagementUI(); } // 添加分组管理UI function setupGroupManagementUI() { const sidebar = document.getElementById('sidebar'); const groupSection = document.createElement('div'); groupSection.innerHTML = ` <h3>标注分组</h3> <div id="group-management"> <input type="text" id="new-group-name" placeholder="新分组名称"> <button id="create-group">创建分组</button> <div id="groups-list"></div> </div> `; sidebar.insertBefore(groupSection, document.querySelector('.annotation-list')); document.getElementById('create-group').addEventListener('click', function() { const groupName = document.getElementById('new-group-name').value.trim(); if (groupName && createAnnotationGroup(groupName)) { updateGroupsList(); document.getElementById('new-group-name').value = ''; } }); // 右键菜单添加到标注项 document.addEventListener('contextmenu', function(e) { if (e.target.classList.contains('annotation-item')) { e.preventDefault(); showAnnotationContextMenu(e); } }); } function updateGroupsList() { const groupsList = document.getElementById('groups-list'); groupsList.innerHTML = ''; for (const groupName in annotationGroups) { const groupItem = document.createElement('div'); groupItem.className = 'group-item'; groupItem.innerHTML = ` <span>${groupName} (${annotationGroups[groupName].length})</span> <button class="delete-group" data-group="${groupName}">×</button> `; groupsList.appendChild(groupItem); groupItem.querySelector('.delete-group').addEventListener('click', function(e) { e.stopPropagation(); deleteAnnotationGroup(groupName); }); } } function deleteAnnotationGroup(groupName) { if (confirm(`确定要删除分组 "${groupName}" 吗?`)) { delete annotationGroups[groupName]; updateGroupsList(); updateStatus(`已删除分组: ${groupName}`); } } function showAnnotationContextMenu(event) { // 实现右键菜单功能 const menu = document.createElement('div'); menu.className = 'context-menu'; menu.style.position = 'absolute'; menu.style.left = `${event.clientX}px`; menu.style.top = `${event.clientY}px`; menu.style.backgroundColor = '#333'; menu.style.border = '1px solid #444'; menu.style.padding = '5px'; menu.style.zIndex = '1000'; const index = event.target.closest('.annotation-item').querySelector('.delete-annotation').dataset.index; const annotation = annotations[index]; menu.innerHTML = ` <div class="menu-item" data-action="rename">重命名</div> <div class="menu-item" data-action="change-color">更改颜色</div> <div class="menu-item" data-action="add-to-group">添加到分组</div> `; document.body.appendChild(menu); // 点击菜单项处理 menu.querySelectorAll('.menu-item').forEach(item => { item.addEventListener('click', function() { const action = this.dataset.action; switch(action) { case 'rename': renameAnnotation(annotation); break; case 'change-color': changeAnnotationColor(annotation); break; case 'add-to-group': addAnnotationToGroup(annotation); break; } menu.remove(); }); }); // 点击其他地方关闭菜单 document.addEventListener('click', function closeMenu() { menu.remove(); document.removeEventListener('click', closeMenu); }, { once: true }); } // 初始化所有功能 initFeatures();以这份代码为基础添加高级标注功能:1.标注层次结构允许用户创建嵌套的标注组,形成层次化的标注结构;2.标注样式自定义:提供自定义标注外观的选项,如颜色、大小、透明度等;3.标注模板:允许用户创建和应用标注模板,快速标注类似特征;4.标注注释:为每个标注添加详细的文字描述、标签和元数据;5.多点测量:支持多点测量,计算复杂形状的长度、角度、面积等;6.自动测量模式:识别模型的关键特征(如边缘、角落)并自动测量;7.测量结果导出:将测量数据导出为csv或PDF格式;8.多人实时协作:允许多人同时编辑一个模型,并实时同步标注;9.版本控制;记录标注和模型的历史版本,支持回滚到任意历史版本;10.标注评论和讨论:允许用户对标注添加评论,进行团队评论;11.自动标注推荐:利用AI识别模型对关键特征(如颜色、器物边缘),并建议标注位置;12.智能识别:支持基于AI的模型识别,自动分类和标注不同类型的文物。13.AR模式:允许用户通过移动设备在现实场景中查看和标注3D模型;14.VR标注:支持VR设备(如Oculus Quest),提供沉浸式标注体验;15.标注数据导出:支持将标注导出为多种格式(如OBJ、JSON、XML、CSV);16.3D打印准备:导出模型和标注,适配3D打印需求;17.AR/VR应用导出:将标注模型导出为AR/VR应用可用的格式;18.模型简化:为复杂模型提供简化选项,优化渲染性能;19.标注对象池:复用标注对象,减少内存分配和垃圾回收;20.LOD(细节层次):根据视距动态调整模型和标注的细节级别;21.插件架构:允许开发者扩展平台功能,如添加插件市场:集成插件市场,用户可以下载和安装第三方插件。自定义标注工具或分析插件;22.插件市场:集成插件市场,用户可以下载和安装第三方插件;23.暗/亮模式切换:提供界面主题切换功能;24.自定义布局:允许用户调整侧边栏、工具栏的位置和大小;25.快捷键支持:添加更多快捷键,提高操作效率;26.标注统计:提供标注数量、类型分布等统计信息;27.模型分析:计算模型体积、表面积等几何属性;28.标注热图:可视化显示标注密集区域;29.云存储集成:支持将模型和标注数据存储到云平台(如AWS、Google Drive);30.自动同步:实时同步本地和云端数据,防止数据丢失;31.孔洞填充:自动检测并填充模型中的孔洞(模型的修复工具);32.法线修复:自动修复模型法线方向,确保渲染效果正确(模型修复工具);33.UV展开:为模型生成UV映射,便于纹理标注(模型修复工具);34.标注筛选:按类型、颜色、注释内容等条件筛选标注(高级筛选与搜索);35.模型搜索:支持通过关键词搜索模型库中的文物(高级筛选与搜索);36.标注教程:内置标注指南和教程,帮助用户快速上手;37.标注练习:提供预设模型和任务,用户可以练习标注技能;38.用户权限:支持不同用户角色(如管理员、编辑、查看者),限制操作权限;39.数据加密:对存储和传输中的模型和标注数据进行加密。根据我的39个要求添加新功能,以完整的代码输出给我
最新发布
05-10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值