赞
踩
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> body { width: 100%; height: 100%; } * { margin: 0; padding: 0; } .label { font-size: 20px; color: #000; font-weight: 700; } .circle { width: 20px; height: 20px; border-radius: 10px; position: absolute; left: 0; top: 0; background-color: red; z-index: 1000; } #container { position: relative; width: 100%; height: 100vh; } </style> </head> <body> <div class="circle"></div> <div id="container"></div> <script type="importmap"> { "imports": { "three": "../three-155/build/three.module.js", "three/addons/": "../three-155/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import Stats from 'three/addons/libs/stats.module.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js'; import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'; let stats, labelRenderer, gpuPanel, temporaryKeep; let camera, scene, renderer, controls, cubeBox; const group = new THREE.Group(); let widthImg = 200; let heightImg = 200; const mouse = new THREE.Vector2(); init(); initHelp(); initLight(); axesHelperWord(); animate(); // 添加平面 addPlane(); // 用这个模型演示 世界坐标 转为 屏幕坐标 let point = new THREE.Vector3(-40, 20, 30); let geometry = new THREE.BoxGeometry(2, 2, 2); let material = new THREE.MeshLambertMaterial({color: 0xccc000}); let cube = new THREE.Mesh(geometry, material); cube.position.x = point.x; cube.position.y = point.y; cube.position.z = point.z; cube.name = 'BoxGeometry'; scene.add(cube); // 这个演示 屏幕坐标转为世界坐标 let geometry2 = new THREE.BoxGeometry(2, 2, 2); let material2 = new THREE.MeshLambertMaterial({color: 0x000000}); cubeBox= new THREE.Mesh(geometry2, material2); cubeBox.name = 'BoxGeometry2'; scene.add(cubeBox); /** * CylinderGeometry(radiusTop : Float, radiusBottom : Float, height : Float, radialSegments : Integer, heightSegments : Integer) radiusTop—顶部圆柱体的半径。默认值为1。 radiusBottom—底部圆柱体的半径。默认值为1。 height——圆柱体的高度。默认值为1。 radialSegments—圆柱体圆周上的分段面数。默认值为32 heightSegments—沿圆柱体高度的面行数。默认值为1。 */ let geometry1 = new THREE.CylinderGeometry(15, 15, 10, 32, 1); let material1 = new THREE.MeshLambertMaterial({color: 0xffff00}); let cylinder = new THREE.Mesh(geometry1, material1); cylinder.position.set(30, 5, -50); cylinder.name = 'CylinderGeometry'; scene.add(cylinder); setTimeout(() => { point3DT2D(); }, 300); function point3DT2D() { // 下面就是世界坐标转为屏幕坐标的代码 let worldVector = new THREE.Vector3(point.x, point.y, point.z); // 世界坐标转标准设备坐标 // 官方释义:将此矢量从世界空间投影到相机的标准化设备坐标(NDC)空间中。 let standardVector = worldVector.project(camera); // canvas画布的宽高尺寸 let container = document.querySelector('#container'); if (!container) return; let cavWidth = container.offsetWidth / 2; let cavHeight = container.offsetHeight / 2; console.log(standardVector); let x = Math.round(standardVector.x * cavWidth + cavWidth); // 设备坐标转屏幕坐标 let y = Math.round(-standardVector.y * cavHeight + cavHeight); // 设备坐标转屏幕坐标 /** * 更新立方体元素位置 */ console.log(x); console.log(y); let box = document.querySelector('.circle'); box.style.left = x + 'px'; box.style.top = y + 'px'; } function onDocumentMouseMove(event) { event.preventDefault(); // 将鼠标点击位置的屏幕坐标转成threejs中的标准坐标,具体解释见代码释义 如果 canvas有左边距 和 上边距 需 要减去 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 新建一个三维单位向量 假设z方向就是1 或者 0.5,这个为什么是这样,有知道详情的还请赐教 // 官方释义:将此矢量从相机的标准化设备坐标(NDC)空间投影到世界空间中。 const vector = new THREE.Vector3(mouse.x, mouse.y, 0.5).unproject(camera); // 将获取的坐标设置为模型的位置 cubeBox.position.x = vector.x; cubeBox.position.y = vector.y; cubeBox.position.z = vector.z; } function addPlane() { // 创建一个平面 PlaneGeometry(width, height, widthSegments, heightSegments) const planeGeometry = new THREE.PlaneGeometry(widthImg, heightImg, 1, 1); // 创建 Lambert 材质:会对场景中的光源作出反应,但表现为暗淡,而不光亮。 const planeMaterial = new THREE.MeshPhongMaterial({ color: 0xb2d3e6, side: THREE.DoubleSide }); const plane = new THREE.Mesh(planeGeometry, planeMaterial); // 以自身中心为旋转轴,绕 x 轴顺时针旋转 45 度 plane.rotation.x = -0.5 * Math.PI; plane.position.set(0, -4, 0); scene.add(plane); } function init() { camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 10, 2000 ); camera.up.set(0, 1, 0); camera.position.set(60, 40, 60); camera.lookAt(0, 0, 0); scene = new THREE.Scene(); scene.background = new THREE.Color( '#ccc' ); renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); document.getElementById( 'container' ).appendChild( renderer.domElement ); labelRenderer = new CSS2DRenderer(); labelRenderer.setSize( window.innerWidth, window.innerHeight ); labelRenderer.domElement.style.position = 'absolute'; labelRenderer.domElement.style.top = '0px'; labelRenderer.domElement.style.left = '0px'; labelRenderer.domElement.style.pointerEvents = 'none'; document.getElementById( 'container' ).appendChild( labelRenderer.domElement ); controls = new OrbitControls( camera, renderer.domElement ); controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.ROTATE }; controls.enablePan = true; // 设置最大最小视距 controls.minDistance = 20; controls.maxDistance = 1000; window.addEventListener( 'resize', onWindowResize ); stats = new Stats(); stats.setMode(1); // 0: fps, 1: ms document.body.appendChild( stats.dom ); gpuPanel = new GPUStatsPanel( renderer.getContext() ); stats.addPanel( gpuPanel ); stats.showPanel( 0 ); scene.add( group ); document.addEventListener('click', onDocumentMouseMove, false); } function initLight() { const light = new THREE.DirectionalLight(new THREE.Color('rgb(253,253,253)')); light.position.set(100, 100, -10); light.intensity = 3; // 光线强度 light.castShadow = true; // 是否有阴影 light.shadow.mapSize.width = 2048; // 阴影像素 light.shadow.mapSize.height = 2048; // 阴影范围 const d = 80; light.shadow.camera.left = -d; light.shadow.camera.right = d; light.shadow.camera.top = d; light.shadow.camera.bottom = -d; light.shadow.bias = -0.0005; // 解决条纹阴影的出现 // 最大可视距和最小可视距 light.shadow.camera.near = 0.01; light.shadow.camera.far = 2000; const AmbientLight = new THREE.AmbientLight(new THREE.Color('rgb(255, 255, 255)')); scene.add( light ); scene.add( AmbientLight ); } function initHelp() { // const size = 100; // const divisions = 5; // const gridHelper = new THREE.GridHelper( size, divisions ); // scene.add( gridHelper ); // The X axis is red. The Y axis is green. The Z axis is blue. const axesHelper = new THREE.AxesHelper( 100 ); scene.add( axesHelper ); } function axesHelperWord() { let xP = addWord('X轴'); let yP = addWord('Y轴'); let zP = addWord('Z轴'); xP.position.set(50, 0, 0); yP.position.set(0, 50, 0); zP.position.set(0, 0, 50); } function addWord(word) { let name = `<span>${word}</span>`; let moonDiv = document.createElement( 'div' ); moonDiv.className = 'label'; // moonDiv.textContent = 'Moon'; // moonDiv.style.marginTop = '-1em'; moonDiv.innerHTML = name; const label = new CSS2DObject( moonDiv ); group.add( label ); return label; } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function animate() { requestAnimationFrame( animate ); stats.update(); controls.update(); labelRenderer.render( scene, camera ); renderer.render( scene, camera ); } </script> </body> </html>
直接复制上面的代码,将three资源路径修改后,启动即可看到效果
上面是代码实现,以下我们看下一些理论知识点:
坐标系之间的转换关系大致为:
局部坐标 -> 世界坐标 -> 观察空间坐标 -> 裁剪空间坐标 -> 屏幕空间坐标
我们将 观察空间坐标系 和 裁剪空间坐标系 之间的转换统一处理,最终得到 标准设备坐标系
因此坐标转换过程就变成了:
局部坐标 -> 世界坐标 -> 标准设备坐标 -> 屏幕空间坐标
原本世界坐标转换到观察空间坐标需要乘上视图矩阵 CameraMatrixWorldInverse(ViewMatrix)
随后,观察空间坐标转换到裁剪空间坐标需要乘上相机投影矩阵:ProjectMatrix
在 ThreeJS 中有一个方法 Vector3.project(camera) 综合了这两步:
// 这是之前的版本
project( camera ) {
return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );
}
最新的是
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。