在threejs中想要选中一个物体,点击或者鼠标悬浮,又或者移动端的touch事件,核心都是通过new THREE.Raycaster完成的。这里用到了一个概念,即我们点击时的 屏幕坐标 转换为 three中的3D坐标。
先看效果图:
代码是:
<!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;
}
</style>
</head>
<body>
<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;
const group = new THREE.Group();
let widthImg = 200;
let heightImg = 200;
const mouse = new THREE.Vector2();
init();
initHelp();
initLight();
axesHelperWord();
animate();
// 添加平面
addPlane();
let geometry = new THREE.BoxGeometry(20, 20, 10);
let material = new THREE.MeshLambertMaterial({color: 0xcccccc});
let cube = new THREE.Mesh(geometry, material);
cube.position.y = 10;
cube.name = 'BoxGeometry';
scene.add(cube);
/**
* 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);
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
// 根据照相机,把这个向量转换到视点坐标系
const vector = new THREE.Vector3(mouse.x, mouse.y, 1).unproject(camera);
// 在视点坐标系中形成射线,射线的起点向量是照相机, 射线的方向向量是照相机到点击的点,这个向量应该归一标准化。
const raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
//射线和模型求交,选中一系列直线
const intersects = raycaster.intersectObjects(scene.children);
console.log('imtersrcts=', intersects);
/**
* 不是所有的 Material 都有这个属性
* emissive.getHex
* emissive.setHex
*/
if (intersects.length > 0) {
//选中第一个射线相交的物体
if (temporaryKeep !== intersects[0].object) {
if (temporaryKeep) temporaryKeep.material.emissive?.setHex(temporaryKeep.currentHex);
temporaryKeep = intersects[0].object;
temporaryKeep.currentHex = temporaryKeep.material.emissive?.getHex();
temporaryKeep.material.emissive?.setHex(0xff0000);
}
} else {
if (temporaryKeep) temporaryKeep.material.emissive?.setHex(temporaryKeep.currentHex);
temporaryKeep = null;
}
}
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.body.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.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>
具体思路代码中都有注释,实在不懂也没关系,函数一封装先用着就是,后面慢慢理解!