首先,看一下案例实现的一下功能。
示例:http://ithanmang.com/drawline/index.html
操作步骤:鼠标指针移入三维网格平面之中,按下左键即可画线,画线过程中,若鼠标移出平面则停止绘制,再次移入则进行上次继续画线,鼠标右键结束绘制,Esc
键退回上一步骤。
案例需求:案例可以用于在三维场景中绘制逃生路线、以及方向线绘制、测量,物体的移动路线绘制等场景。
实现步骤
1、三维场景
创建scene、camera、renderer、controls,等个场景所需对象。具体代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>画直线</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="libs/three.js"></script>
<script src="libs/Detector.js"></script>
<script src="libs/stats.js"></script>
<script src="libs/OrbitControls.js"></script>
</head>
<body>
<script>
let scene, camera, renderer, controls;
let stats = initStats();
/* 地面网格所需变量 */
let length = 200; /*线段长度*/
/* 场景 */
function initScene() {
scene = new THREE.Scene();
}
/* 相机 */
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 200, 250);
camera.lookAt(new THREE.Vector3(0, 0, 0));
}
/* 渲染器 */
function initRender() {
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
/* 灯光 */
function initLight() {
let ambientLight = new THREE.AmbientLight(0x333333);
scene.add(ambientLight);
let directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 300, 200);
scene.add(directionalLight);
}
/* 控制器 */
function initControls() {
controls = new THREE.OrbitControls(camera, renderer.domElement);
/* 其它属性默认 */
}
/* 场景内容 */
function initContent() {
}
/* 更新数据 */
function update() {
stats.update();
controls.update();
}
/* 性能插件 */
function initStats() {
let stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild(stats.domElement);
return stats;
}
/* 窗口自动适应 */
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
/* 循环调用 */
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
update();
}
/* 初始化 */
function init() {
/* 兼容性判断 */
if (!Detector.webgl) Detector.addGetWebGLMessage();
initScene();
initCamera();
initRender();
initLight();
initContent();
initControls();
/* 事件监听 */
window.addEventListener('resize', onWindowResize, false);
}
/* 初始加载 */
(function () {
console.log('three start...');
init();
animate();
console.log('three end...');
})();
</script>
</body>
</html>
此时场景按键完毕,如果不出错的情况下,界面应该是一片黑暗,左上角有一个pfs显示模块,打开控制台查看是否报错。
场景正常开始和结束,然后进行下一步。
2、向场景中加入网格
首先,绘制线段的话,可以在场景中直接绘制,而不考虑 z
轴的存在。
但是,那样绘制出来的仅仅是二维的线段,而这里绘制的是有三个维度的线条,可以通过鼠标进行操控,因此需要绘制一个网格来作为画板。
当然,你也可以直接绘制一个平面然后绕x轴旋转90°也可以作为画板。
/* 场景内容 */
function initContent() {
let geometry = new THREE.Geometry();/* 简单基础几何 */
let lineMaterial = new THREE.LineBasicMaterial({color: 0x808080});/* 基础线材质 */
geometry.vertices.push(new THREE.Vector3(-length / 2, 0, 0));/* 顶点(-100, 0, 0) */
geometry.vertices.push(new THREE.Vector3(length / 2, 0, 0)); /* 顶点( 100, 0, 0) */
/* 循环创建线段 */
for (let i = 0; i <= length / 10; i++){
/* 横向线段 */
let lineX = new THREE.Line(geometry, lineMaterial);
lineX.position.z = (i * 10) - length / 2;
scene.add(lineX);
/* 纵向线段 */
let lineY = new THREE.Line(geometry, lineMaterial);
lineY.rotation.y = 0.5 * Math.PI;
lineY.position.x = (i * 10) - length / 2;
scene.add(lineY);
}
}
上面代码其中的length
定义了线条的长度。
/* 地面网格所需变量 */
let length = 200; /*线段长度*/
3、通过Raycaster
获取鼠标坐标
在网格平面上任意位置绘制线段,就需要获取点击位置的向量Vector3
,获取之后就可以确定第一个点的位置,一条直线由两个点来确定,由此,需要获取鼠标点击位置。
在写代码之前,需要了解一下THREE.Raycaster这个方法。
前面文章13 - three.js 笔记 - 通过 THREE.Raycaster 实现模型选中与信息显示也曾对这个对象做过介绍但是并未介绍其中所有的方法。
因为,我们需要获取不是与射线相交的物体,而是与射线相交的点也就是一个THREE.Vector3()
对象。
通过查看官方文档,可以使用Three.Raycaster
类的一个属性Ray
的一个方法intersectPlane()
器返回值是一个Vector3
对象。
Ray.intersectPlane():将这条射线与平面相交,如果没有交点,则返回交点或null。
当然这里,不仅仅可以通过平面来获取交点还有别的方法,但是这里使用plane
,别的方法请查看官方文档。
所以,我们可以先实例化一个THREE.Raycaster
,再获取它的ray
属性,然后计算射线与平面的交点,并返回次交点,代码如下。
/* 获取射线与平面相交的交点 */
function getIntersects(event) {
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
let normal = new THREE.Vector3(0, 1, 0);
/* 创建平面 */
let planeGround = new THREE.Plane(normal, 0);
/* 从相机发出一条射线经过鼠标点击的位置 */
raycaster.setFromCamera(mouse, camera);
/* 获取射线 */
let ray = raycaster.ray;
/* 计算相机到射线的对象,可能有多个对象,返回一个数组,按照距离相机远近排列 */
let intersects = ray.intersectPlane(planeGround);
/* 返回向量 */
return intersects;
}
此时如果加入一个mousemove
监听事件,就可以看到已经可以获取鼠标经过的向量.
在getIntersects
方法中加入打印输出测试
console.log("x:"+intersects.x+" y:"+intersects.y+" z:"+intersects.z);
添加鼠标移动监听事件
window.addEventListener('mousemove', getIntersects, false);
效果:
4、添加鼠标点击事件,限定鼠标点击范围
因为,我们是要在平面中画线,而不是整个三维场景中,所以需要限定一下鼠标的点击范围。
主要代码如下
let pointsArray = [];
let window_mouse = true;
/* 鼠标按下事件 */
function onMouseDown(event) {
/* 获取相机发出的射线与 Plane 相交点*/
let intersects = getIntersects(event);
/* 存放网格的三维坐标 */
let vector3_x, vector3_z;
/* 鼠标左键按下时,创建点和线段 */
if (event.button === 0) {
if (!window_mouse){
window.addEventListener('mousemove', onMouseMove, false);
/* 依据 windwo_mouse 标识避免事件的重复添加 */
window_mouse = true;
}
/* 判断交点是否在 x(-100, 100) ,z(-100, 100)(平面)之间 */
if (Math.abs(intersects.x) < length / 2 && Math.abs(intersects.z) < length / 2){
/* 若交点此时在平面之内则创建点(Points) */
let pointsGeometry = new THREE.Geometry();
pointsGeometry.vertices.push(intersects);
let pointsMaterial = new THREE.PointsMaterial({color:0xff0000, size: 3});
let points = new THREE.Points(pointsGeometry, pointsMaterial);
pointsArray.push(points);
/* 创建线段 */
let lineGeometry = new THREE.Geometry();
let lineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00});
if (pointsArray.length >= 2) {
lineGeometry.vertices.push(pointsArray[0].geometry.vertices[0], pointsArray[1].geometry.vertices[0]);
let line = new THREE.Line(lineGeometry, lineMaterial);
pointsArray.shift();
scene.add(line);
}
scene.add(points);
}
}
/* 鼠标右键按下时 回退到上一步的点,并中断绘制 */
if (event.button === 2) {
window.removeEventListener('mousemove', onMouseMove, false);
/* 移除事件之后,要设置为 false 为了避免事件的重复添加 */
window_mouse = false;
/* 鼠标左键未点击时线段的移动状态 */
if (scene.getObjectByName('line_move')) {
scene.remove(scene.getObjectByName('line_move'));
/* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
pointsArray.shift();
}
}
}
添加监听事件
window.addEventListener('mousedown', onMouseDown, false);/* 使用mousedown的时候可以判断出点击的鼠标左右键之分 */
5、添加键盘事件,事件绘制回退
/* 键盘按下事件 */
function onKeyDown(event) {
/* ESC键 回退上一步绘制,结束绘制*/
if (event.key === 'Escape'){
window.removeEventListener('mousemove', onMouseMove, false);
/* 移除事件之后,要设置为 false 为了避免 mousemove 事件的重复添加 */
window_mouse = false;
/* 鼠标左键未点击时线段的移动状态 */
if (scene.getObjectByName('line_move')) {
scene.remove(scene.getObjectByName('line_move'));
/* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
pointsArray.shift();
}
let length = scene.children.length - 1;
/* 按步骤移除点和先 */
if (scene.children[length].isLine || scene.children[length].isPoints){
scene.children.pop();
length = scene.children.length - 1;
/* 若最后一项不是线段或者点就不移除 */
if (!scene.children[length].isMesh) {
scene.children.pop();
}
}
}
}
添加监听事件
window.addEventListener('keydown', onKeyDown, false);/* 使用事件的时候要把前面的on给去掉 */
示例完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>画直线</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="libs/three.js"></script>
<script src="libs/Detector.js"></script>
<script src="libs/stats.js"></script>
<script src="libs/OrbitControls.js"></script>
</head>
<body>
<script>
let scene, camera, renderer, controls;
let stats = initStats();
/* 地面网格所需变量 */
let length = 200; /*线段长度*/
/* 场景 */
function initScene() {
scene = new THREE.Scene();
}
/* 相机 */
function initCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 200, 250);
camera.lookAt(new THREE.Vector3(0, 0, 0));
}
/* 渲染器 */
function initRender() {
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
/* 灯光 */
function initLight() {
let ambientLight = new THREE.AmbientLight(0x333333);
scene.add(ambientLight);
let directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 300, 200);
scene.add(directionalLight);
}
/* 控制器 */
function initControls() {
controls = new THREE.OrbitControls(camera, renderer.domElement);
/* 其它属性默认 */
}
/* 场景内容 */
function initContent() {
let geometry = new THREE.Geometry();/* 简单基础几何 */
let lineMaterial = new THREE.LineBasicMaterial({color: 0x808080});/* 基础线材质 */
let planeGeometry = new THREE.PlaneGeometry(length, 10);/* 平面 width:200,、height:10 */
let planeMaterial = new THREE.MeshBasicMaterial({color: 0xD9D9D9, side: THREE.DoubleSide});/* 平面材质 */
geometry.vertices.push(new THREE.Vector3(-length / 2, 0, 0));/* 顶点(-100, 0, 0) */
geometry.vertices.push(new THREE.Vector3(length / 2, 0, 0)); /* 顶点( 100, 0, 0) */
/* 循环创建线段 */
for (let i = 0; i <= length / 10; i++){
/* 横向线段 */
let lineX = new THREE.Line(geometry, lineMaterial);
lineX.position.z = (i * 10) - length / 2;
scene.add(lineX);
/* 纵向线段 */
let lineY = new THREE.Line(geometry, lineMaterial);
lineY.rotation.y = 0.5 * Math.PI;
lineY.position.x = (i * 10) - length / 2;
scene.add(lineY);
}
/* 创建包围平面 */
let planeX_left = new THREE.Mesh(planeGeometry, planeMaterial);
planeX_left.rotation.y = 0.5 * Math.PI;
planeX_left.position.x = -length / 2;
let planeX_right = planeX_left.clone();
planeX_right.position.x = length / 2;
let planeY_top = new THREE.Mesh(planeGeometry, planeMaterial);
planeY_top.position.z = -length / 2;
let planeY_bottom = planeY_top.clone();
planeY_bottom.position.z = length / 2;
scene.add(planeY_bottom);
scene.add(planeY_top);
scene.add(planeX_left);
scene.add(planeX_right);
/* 四个包围面的位置 y轴向上5 */
scene.traverse(function (object) {
if (object.isMesh){
if (object.geometry.type === 'PlaneGeometry'){
object.position.y = 5;
}
}
});
}
/* 获取射线与平面相交的交点 */
function getIntersects(event) {
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
let normal = new THREE.Vector3(0, 1, 0);
/* 创建平面 */
let planeGround = new THREE.Plane(normal, 0);
/* 从相机发出一条射线经过鼠标点击的位置 */
raycaster.setFromCamera(mouse, camera);
/* 获取射线 */
let ray = raycaster.ray;
/* 计算相机到射线的对象,可能有多个对象,返回一个数组,按照距离相机远近排列 */
let intersects = ray.intersectPlane(planeGround);
/* 返回向量 */
return intersects;
}
let pointsArray = [];
let window_mouse = true;
/* 鼠标按下事件 */
function onMouseDown(event) {
/* 获取相机发出的射线与 Plane 相交点*/
let intersects = getIntersects(event);
/* 存放网格的三维坐标 */
let vector3_x, vector3_z;
/* 鼠标左键按下时,创建点和线段 */
if (event.button === 0) {
if (!window_mouse){
window.addEventListener('mousemove', onMouseMove, false);
/* 依据 windwo_mouse 标识避免事件的重复添加 */
window_mouse = true;
}
/* 判断交点是否在 x(-100, 100) ,z(-100, 100)(平面)之间 */
if (Math.abs(intersects.x) < length / 2 && Math.abs(intersects.z) < length / 2){
/* 若交点此时在平面之内则创建点(Points) */
let pointsGeometry = new THREE.Geometry();
pointsGeometry.vertices.push(intersects);
let pointsMaterial = new THREE.PointsMaterial({color:0xff0000, size: 3});
let points = new THREE.Points(pointsGeometry, pointsMaterial);
pointsArray.push(points);
/* 创建线段 */
let lineGeometry = new THREE.Geometry();
let lineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00});
if (pointsArray.length >= 2) {
lineGeometry.vertices.push(pointsArray[0].geometry.vertices[0], pointsArray[1].geometry.vertices[0]);
let line = new THREE.Line(lineGeometry, lineMaterial);
pointsArray.shift();
scene.add(line);
}
scene.add(points);
}
}
/* 鼠标右键按下时 回退到上一步的点,并中断绘制 */
if (event.button === 2) {
window.removeEventListener('mousemove', onMouseMove, false);
/* 移除事件之后,要设置为 false 为了避免事件的重复添加 */
window_mouse = false;
/* 鼠标左键未点击时线段的移动状态 */
if (scene.getObjectByName('line_move')) {
scene.remove(scene.getObjectByName('line_move'));
/* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
pointsArray.shift();
}
}
}
/* 鼠标移动事件 */
function onMouseMove(event) {
let intersects = getIntersects(event);
/* 判断交点是否在 x(-100, 100) ,z(-100, 100)(平面)之间 */
if (Math.abs(intersects.x) < length / 2 && Math.abs(intersects.z) < length / 2) {
/* 鼠标左键未点击时线段的移动状态 */
if (scene.getObjectByName('line_move')) {
scene.remove(scene.getObjectByName('line_move'));
}
/* 创建线段 */
let lineGeometry = new THREE.Geometry();
let lineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00});
if (pointsArray.length > 0){
lineGeometry.vertices.push(pointsArray[0].geometry.vertices[0]);
let mouseVector3 = new THREE.Vector3(intersects.x, 0, intersects.z);
lineGeometry.vertices.push(mouseVector3);
let line = new THREE.Line(lineGeometry, lineMaterial);
line.name = 'line_move';
scene.add(line);
}
}
}
/* 键盘按下事件 */
function onKeyDown(event) {
/* ESC键 回退上一步绘制,结束绘制*/
if (event.key === 'Escape'){
window.removeEventListener('mousemove', onMouseMove, false);
/* 移除事件之后,要设置为 false 为了避免 mousemove 事件的重复添加 */
window_mouse = false;
/* 鼠标左键未点击时线段的移动状态 */
if (scene.getObjectByName('line_move')) {
scene.remove(scene.getObjectByName('line_move'));
/* 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘 */
pointsArray.shift();
}
let length = scene.children.length - 1;
/* 按步骤移除点和先 */
if (scene.children[length].isLine || scene.children[length].isPoints){
scene.children.pop();
length = scene.children.length - 1;
/* 若最后一项不是线段或者点就不移除 */
if (!scene.children[length].isMesh) {
scene.children.pop();
}
}
}
}
/* 更新数据 */
function update() {
stats.update();
controls.update();
}
/* 性能插件 */
function initStats() {
let stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild(stats.domElement);
return stats;
}
/* 窗口自动适应 */
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
/* 循环调用 */
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
update();
}
/* 初始化 */
function init() {
/* 兼容性判断 */
if (!Detector.webgl) Detector.addGetWebGLMessage();
initScene();
initCamera();
initRender();
initLight();
initContent();
initControls();
/* 事件监听 */
window.addEventListener('resize', onWindowResize, false);
window.addEventListener('mousedown', onMouseDown, false);/* 使用mousedown的时候可以判断出点击的鼠标左右键之分 */
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('keydown', onKeyDown, false);/* 使用事件的时候要把前面的on给去掉 */
}
/* 初始加载 */
(function () {
console.log('three start...');
init();
animate();
console.log('three end...');
})();
</script>
</body>
</html>