Appearance
射线Ray
射线Ray和三维向量Vector3一样属于数学几何计算相关的API,可以进行射线交叉计算。
学习Three.js中的射线Ray概念,你可以类比数学几何中提到的射线,在三维空间中,一条线把一个点作为起点,然后沿着某个方向无限延伸。
js
// 创建射线对象Ray
const ray = new THREE.Ray()射线起点.origin
射线Ray的起点.origin在3D空间中的坐标,可以用一个三维向量Vector3的x、y、z分量表示。
js
// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);起点.origin属性值是三维向量Vector3,也可以用.set()方法设置。
js
// 设置射线起点
ray.origin.set(1, 0, 3);射线方向.direction
射线Ray的方向.direction通用用一个三维向量Vector3表示,向量长度保证为1,也就是单位向量。
js
// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(1,0,0);
// 表示射线沿着x轴负方向
ray.direction = new THREE.Vector3(-1,0,0);
// 表示射线沿着y方向
ray.direction = new THREE.Vector3(0,1,0);注意.direction的值需要是单位向量,不是的话可以执行.normalize()归一化或者说标准化。
js
ray.direction = new THREE.Vector3(5,0,0).normalize();
// 表示射线沿着xy坐标轴的中间线
ray.direction = new THREE.Vector3(1,1,0).normalize();.intersectTriangle()方法
射线Ray有很多关于数学计算的方法,下面就先介绍一个与三角形交叉计算相关的方法.intersectTriangle(),简单说,就是计算一个射线和一个三角形在3D空间中是否交叉。
执行.intersectTriangle()方法,如果相交返回交点坐标,不相交返回空值null。
js
// 三角形三个点坐标
const p1 = new THREE.Vector3(100, 25, 0);
const p2 = new THREE.Vector3(100, -25, 25);
const p3 = new THREE.Vector3(100, -25, -25);
const point = new THREE.Vector3();//用来记录射线和三角形的交叉点
// `.intersectTriangle()`计算射线和三角形是否相交叉,相交返回交点,不相交返回null
const result = ray.intersectTriangle(p1,p2,p3,false,point);
console.log('交叉点坐标', point);
console.log('查看是否相交', result);.intersectTriangle()参数4表示是否进行背面剔除,p1,p2,p3可以理解为一个三角形,有正反两面,一面是正面,一面是反面
在一面观察p1,p2,p3,如果沿着三个点的顺序转圈是逆时针方向,表示正面,另一面观察,p1,p2,p3就是顺时针方向,表示背面。
.intersectTriangle()参数4设为true,表示进行背面剔除,虽然从几何空间上讲,该案例源码射线和三角形虽然交叉,但在threejs中,三角形背面对着射线,视为交叉无效,进行背面剔除,返回值r是null。
js
const r = ray.intersectTriangle(p1,p2,p3,true,point);
console.log('查看是否相交', r);测试案例
准备三个mesh用于射线拾取
js
const geometry = new THREE.SphereGeometry(25, 50, 50);
const material = new THREE.MeshLambertMaterial({
color: 0x009999,
});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = mesh1.clone();
mesh2.position.y = 100;
const mesh3 = mesh1.clone();
mesh3.position.x = 100;
const model = new THREE.Group();
// 三个网格模型mesh1,mesh2,mesh3用于射线拾取测试
model.add(mesh1, mesh2, mesh3);
model.updateMatrixWorld(true);射线投射器Raycaster
射线投射器Raycaster具有一个射线属性.ray,该属性的值就是射线对象Ray。
js
const raycaster = new THREE.Raycaster();
console.log('射线属性',raycaster.ray);
// 设置射线起点
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
// 设置射线方向射线方向沿着x轴
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);射线交叉计算(.intersectObjects()方法)
射线投射器Raycaster通过.intersectObjects()方法可以计算出来与自身射线.ray相交的网格模型。
.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算,未选中对象返回空数组[],选中一个对象,数组1个元素,选中多个对象,数组多个元素,如果选中多个对象,对象在数组中按照先后排序。
js
const raycaster = new THREE.Raycaster();
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
// 射线发射拾取模型对象
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log("射线器返回的对象", intersects);.intersectObjects()射线拾取返回信息
射线拾取返回的intersects里面的元素包含多种信息,你可以通过threejs文档查看,或者在浏览器控制台打印查看。
.intersectObjects()和.intersectObject()功能相同,只是具体语法不同,.intersectObjects()返回数组元素包含的信息,可以参考文档关于.intersectObject()的介绍。
js
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
console.log("交叉点坐标", intersects[0].point);
console.log("交叉对象",intersects[0].object);
console.log("射线原点和交叉点距离",intersects[0].distance);
}射线选中的模型对象改变材质颜色
js
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
if (intersects.length > 0) {
// 选中模型的第一个模型,设置为红色
intersects[0].object.material.color.set(0xff0000);
}注意事项
注意射线拾取的时候,mesh1, mesh2, mesh3位置要确保更新的情况下,执行射线计算,threejs一般是渲染器执行一次.render()之后,你设置的mesh.position或者mesh父对象的position才会真实生效。
js
...
// 注意更新下模型的世界矩阵,你设置的mesh.position生效,再进行射线拾取计算
model.updateMatrixWorld(true);
...
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
mesh2.position.y = 100;
mesh3.position.x = 100;
const model = new THREE.Group();
// 三个网格模型mesh1,mesh2,mesh3用于射线拾取测试
model.add(mesh1, mesh2, mesh3);
//注意更新下模型的世界矩阵,你设置的position生效,再进行射线拾取计算
model.updateMatrixWorld(true);
const raycaster = new THREE.Raycaster();
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);核心代码
js
renderer.domElement.addEventListener('click', function (event) {
// .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
const px = event.offsetX;
const py = event.offsetY;
//屏幕坐标px、py转WebGL标准设备坐标x、y
//width、height表示canvas画布宽高度
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
//创建一个射线投射器`Raycaster`
const raycaster = new THREE.Raycaster();
//.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
// 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
//.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
// 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
// 选中模型的第一个模型,设置为红色
intersects[0].object.material.color.set(0xff0000);
}
})效果
完整代码
js
import * as THREE from 'three';
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
onMounted(() => {
// 创建场景
const scene = new THREE.Scene();
const geometry = new THREE.SphereGeometry(25, 50, 50);
const material = new THREE.MeshLambertMaterial({
color: 0x009999
});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = mesh1.clone();
mesh2.geometry = mesh1.geometry.clone();
mesh2.material = mesh1.material.clone();
mesh2.position.y = 100;
const mesh3 = mesh1.clone();
mesh3.position.x = 100;
mesh3.geometry = mesh1.geometry.clone();
mesh3.material = mesh1.material.clone();
const model = new THREE.Group();
model.add(mesh1, mesh2, mesh3);
model.updateMatrixWorld(true);
scene.add(model); //网格模型添加到场景中
// AxesHelper:辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
// tip: 环境光
// 环境光AmbientLight:没有特定的光源位置,整体改变场景的光照明暗度
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(100, 60, 50);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
// directionalLight.target = mesh;
scene.add(directionalLight);
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
scene.add(dirLightHelper);
/**
* 光源设置
*/
//点光源
// const pointLight = new THREE.PointLight(0xffffff, 1.0);
// pointLight.decay = 0.0; //光源光照强度不随距离改变衰减
// //点光源位置
// // pointLight.position.set(400, 0, 0);//点光源放在x轴上
// pointLight.position.set(400, 200, 300); //偏移光源位置,观察渲染效果变化
// scene.add(pointLight); //点光源添加到场景中
// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
const width = window.innerWidth; //宽度
const height = window.innerHeight; //高度
/**
* 透视投影相机设置
*/
// 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000:远裁截面
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(470, 359, 297); //相机在Three.js三维坐标系中的位置
camera.lookAt(0, 0, 0); //相机观察目标指向Three.js坐标系原点
/**
* 创建渲染器对象
*/
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
document.getElementById('canvas').appendChild(renderer.domElement);
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
renderer.domElement.addEventListener('click', function (event) {
// .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
const px = event.offsetX;
const py = event.offsetY;
//屏幕坐标px、py转WebGL标准设备坐标x、y
//width、height表示canvas画布宽高度
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
//创建一个射线投射器`Raycaster`
const raycaster = new THREE.Raycaster();
//.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
// 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
//.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
// 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log('射线器返回的对象', intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
// 选中模型的第一个模型,设置为红色
intersects[0].object.material.color.set(0xff0000);
}
});
function render() {
renderer.render(scene, camera); //执行渲染操作
// console.log(camera.position);
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
// controls.addEventListener('change', function () {
// renderer.render(scene, camera); //执行渲染操作
// // 浏览器控制台查看相机位置变化
// console.log('camera.position', camera.position);
// }); //监听鼠标、键盘事件
window.onresize = () => {
if (route.query.type === '1') {
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
return;
} else if (route.query.type === '2') {
// 重置渲染器输出画布canvas尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix();
}
};
});