三种坐标系的概念:场景坐标系(世界坐标系)、屏幕坐标系、视点坐标系
- 世界坐标系: 在webGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。长度单位这样来定:窗口范围按此单位恰好是(-1,-1)到(1,1),即屏幕左下角坐标为(-1,-1),右上角 坐标为(1,1)。
- 屏幕坐标系: webGL的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。
- 视点坐标系: 是以视点(照相机)为原点,以视线的方向为Z+轴正方向的坐标系中的方向。webGL会将世界坐标先变换到视点坐标,然后进行裁剪,只有在视线范围(视见体)之内的场景才会进入下一阶段的计算。
Raycaster
这个类设计用于鼠标去获取在3D世界被鼠标选中的一些物体
Raycaster( origin, direction, near, far )
origin — 射线的起点向量。
direction — 射线的方向向量,应该归一标准化。
near — 所有返回的结果应该比 near 远。Near不能为负,默认值为0。
far — 所有返回的结果应该比 far 近。Far 不能小于 near,默认值为无穷大。
找到点击物体的大致思路
鼠标在屏幕上点击的时候,得到二维坐标p(x, y),再加上深度坐标的范围(0, 1), 就可以形成两个三位坐标A(x1, y1, 0), B(x2, y, 1), 由于它们的Z轴坐标是0和1,则转变到投影坐标系的话,一定分别是前剪切平面上的点和后剪切平面上的点,也就是说,在投影坐标系中,A点一定在能看见的所有模型的最前面,B点一定在能看见的所有的模型的最后边,将AB点连成线,AB线穿过的物体就是被点击的物体。而 Three.js提供一个射线类Raycasting来拾取场景里面的物体。更方便的使用鼠标来操作3D场景。(不过在实际代码中我们组成射线的两个点是摄像机所在视点与屏幕上点击的点连接而成的射线)
推理:
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
推导过程:
设A点为点击点(x1,y1),x1=e.clintX, y1=e.clientY
设A点在世界坐标中的坐标值为B(x2,y2);
由于A点的坐标值的原点是以屏幕左上角为(0,0);
我们可以计算可得以屏幕中心为原点的B'值
x2' = x1 - innerWidth/2
y2' = innerHeight/2 - y1
又由于在世界坐标的范围是[-1,1],要得到正确的B值我们必须要将坐标标准化
x2 = (x1 -innerWidth/2)/(innerwidth/2) = (x1/innerWidth)*2-1
同理得 y2 = -(y1/innerHeight)*2 +1
这里封装了一个通用方法用于获取intersects对象
// 获取与射线相交的对象数组
function getIntersects(event) {
event.preventDefault();
// console.log("event.clientX:" + event.clientX)
// console.log("event.clientY:" + event.clientY)
// 声明 raycaster 和 mouse 变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
// 通过鼠标点击位置,计算出 raycaster 所需点的位置,以屏幕为中心点,范围 -1 到 1
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
//通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
raycaster.setFromCamera(mouse, camera);
// 获取与raycaster射线相交的数组集合,其中的元素按照距离排序,越近的越靠前
var intersects = raycaster.intersectObjects(scene.children);
//返回选中的对象数组
return intersects;
}
通过这个函数,我们可以获取到点击时的intersects对象.那么我们把这个函数添加到点击事件里来看看具体怎么实现
window.addEventListener('click', function(event) {
let intsersects = getIntersects(event)
if(intsersects.length > 0) {
console.log(intsersects);
if(intsersects[0].object.material.name === 'belinebox') {
intsersects[0].object.material.color.set(0xff000)
render()
}
}
})
上面代码中,我们通过click事件,将event对象传给封装好的getIntersects函数,得到intersects对象.然后判定intersects对象的中material的那么值确定点击的是否是我们想要的物体.最后通过color的set方法来验证点击事件
这里需要注意,由于我们的render函数不是持续调用的,这里踩了个坑,发现点击多次之后才有反应,原因就是在改变颜色后没有调用渲染函数.
intsersects对象交互
在我们获取到intsersects对象之后,需要对其进行控制,从前面的代码中,可以通过设置material的name属性,当点击获取到intsersects对象后判断name属性来确定我们点击的模型.从而进一步做交互处理.
我们通过一个demo实现点击物体,将物体放大道2倍,并换一个颜色,再次点击,还原
window.addEventListener('click', function (event) {
let intsersects = getIntersects(event)
if (intsersects.length > 0) {
//- 判断点击的物体的name属性值
if (intsersects[0].object.material.name === 'belinebox') {
console.log(intsersects[0].object);
//- 判断当前对象的x轴缩放是否为2倍
if(intsersects[0].object.scale.x === 2) {
//- 还原物体大小1比1,并设置一个颜色
intsersects[0].object.scale.set(1, 1, 1)
intsersects[0].object.material.color.set(0xff000)
} else {
//- 设置物体大小为2倍,并设置一个颜色
intsersects[0].object.scale.set(2, 2, 2)
intsersects[0].object.material.color.set(0x0000ff)
}
render()
}
}
})
既然可以设置大小,设置颜色,那么其他属性也是可以设置的.可以参考Object对象可调用的属性及方法进行设置
文档参考地址:
https://techbrood.com/threejs/docs/#%E5%8F%82%E8%80%83%E6%89%8B%E5%86%8C/%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%9D%97(Core)/3D%E5%AF%B9%E8%B1%A1(Object3D)/3D%E5%AF%B9%E8%B1%A1(Object3D))
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>第一个three.js文件_WebGL三维场景</title>
<style>
body {
margin: 0;
overflow: hidden;
/* 隐藏body窗口区域滚动条 */
}
</style>
<!--引入three.js三维引擎-->
<script src="./js-r100/three.js"></script>
<script src="./js-r100/controls/OrbitControls.js"></script>
</head>
<body>
<script>
/**
* 创建场景对象Scene
*/
var scene = new THREE.Scene();
/**
* 创建网格模型
*/
// var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
name: 'belinebox'
}); //材质对象Material
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
/**
* 光源设置
*/
//点光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
/**
* 相机设置
*/
var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(200, 300, 200); //设置相机位置
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
/**
* 创建渲染器对象
*/
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);//设置渲染区域尺寸
renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
//- 渲染函数
function render() {
//执行渲染操作 指定场景、相机作为参数
renderer.render(scene, camera);
}
render();
//- 添加鼠标对视觉相机的操作
let controls = new THREE.OrbitControls(camera);
//监听鼠标事件,触发渲染函数,更新canvas画布渲染效果
controls.addEventListener('change', render, { passive: true });
// 获取与射线相交的对象数组
function getIntersects(event) {
event.preventDefault();
// console.log("event.clientX:" + event.clientX)
// console.log("event.clientY:" + event.clientY)
// 声明 raycaster 和 mouse 变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
// 通过鼠标点击位置,计算出 raycaster 所需点的位置,以屏幕为中心点,范围 -1 到 1
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
//通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
raycaster.setFromCamera(mouse, camera);
// 获取与raycaster射线相交的数组集合,其中的元素按照距离排序,越近的越靠前
var intersects = raycaster.intersectObjects(scene.children);
//返回选中的对象数组
return intersects;
}
//- 随机生成16进制颜色
function getColor() {
var colorElements = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f";
var colorArray = colorElements.split(",");
var color = "#";
for (var i = 0; i < 6; i++) {
color += colorArray[Math.floor(Math.random() * 16)];
}
return color;
}
//- 点击事件
window.addEventListener('click', function (event) {
let intsersects = getIntersects(event)
if (intsersects.length > 0) {
console.log(intsersects);
if (intsersects[0].object.material.name === 'belinebox') {
let mathColor = getColor();
console.log(mathColor)
intsersects[0].object.material.color.set(mathColor)
render()
}
}
})
</script>
</body>
</html>