Three.js 简介
随着 HTML5 标准的颁布,以及主流浏览器的功能日益强大,直接在浏览器中展示三维图像和动画已经 变得越来越容易。WebGL 技术为在浏览器中创建丰富的三维图形提供了丰富且强大的接口,它们不仅可 以创建二维图形和应用,还可以充分利用 GPU,创建漂亮的、高性能的三维应用。但是直接使用 WebGL 编程非常复杂,需要了解 WebGL 的内部细节,学习复杂的着色器语法和足够多的数学和图形学方面的 专业知识才能学好 WebGL。 Three.js 就是一款基于原生 WebGL 封装运行在浏览器中的 3D 引擎,可 以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。使用 Three.js 不必学习 WebGL 的详细细节,就能轻松创建出漂亮的三维图形。
Three.js 长短单位默认是米,时间是秒,除过相机的 fov 是角度其余都是弧度
Scene
Scene 是场景对象,所有的网格对象、灯光、动画等都需要放在场景中,使用 new THREE.Scene 初始 化场景,下面是场景的一些常用属性和方法。
- fog:设置场景的雾化效果,可以渲染出一层雾气,隐层远处的的物体。 Fog(color, near, far) color: 表示雾的颜色,如设置为白色,场景中远处物体为蓝色,场景中最近处距离物体是自身颜色 ,最远和最近之间的物体颜色是物体本身颜色和雾颜色的混合效果。 near:表示应用雾化效果的最 小距离,距离活动摄像机长度小于 near 的物体将不会被雾所影响。 far:表示应用雾化效果的最 大距离,距离活动摄像机长度大于 far 的物体将不会被雾所影响。
- overrideMaterial:强制场景中所有物体使用相同材质。
- autoUpdate:设置是否自动更新。
- background:设置场景背景,默认为黑色。
- children:所有对象的列表。
- add():向场景中添加对象。
- remove():从场景中移除对象。
- getChildByName():根据名字直接返回这个对象。
- traverse():传入一个回调函数访问所有的对象。
在 react 里 使用 THREE
利用 React Hooks 封装 THREE,方便使用
import { useCallback, useEffect, useRef } from "react";
// 0.144 版没有默认导出,需要什么导出什么
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import _ from "lodash";
import Stats from "stats.js";
import { GUI } from "dat.gui";
/** 非常重要 需要通过gui控制必须要给 mesh 或 group 设置 name 属性, 并且 key 里要包含 name 属性
* key 可以是 ‘,’ 逗号分隔的对象 第一个是父对象的名称,第二个是子对象名称或属性名 */
export type guiChangePropertyType = {
[key: string]:
| { name: string; min?: number; max?: number; step?: number }
| { name: string; min?: number; max?: number; step?: number }[];
};
//自定义hooks
function useTHREE(props: {
//画布的父容器
placeRenderingRef: React.RefObject<HTMLElement>;
//是否使用辅助轴
isUseAxes?: boolean;
//是否使用相机轨道控制器
isUseOrbitControls?: boolean;
//是否使用性能监控
isUsePerformanceMonitor?: boolean;
//是否使用辅助相机
isUseAssistCamera?: boolean;
/** dat.gui 工具可以改变的属性 */
guiChangePropertys?: guiChangePropertyType;
/** 点击画布的元素拾取 mesh */
clickHandle?: (clickMesh: (THREE.Mesh | THREE.Group)[]) => void;
/**相机轨道控制器变化了 */
orbitControlsChange?: () => void;
}) {
//渲染器
const renderer = useRef<THREE.WebGLRenderer>();
//场景
const scene = useRef<THREE.Scene>();
//相机
const camera = useRef<THREE.Camera>();
//辅助相机
const assistCamera = useRef<THREE.Camera>();
//辅助轴
const axesHelper = useRef<THREE.AxesHelper>();
//THREE 提供的对象可以计算俩次渲染的时间差
const clock = useRef<THREE.Clock>();
//性能监视器
const starts = useRef<Stats>();
const orbitControls = useRef<OrbitControls>();
//在页面加载的时候的初始动画
const primaryAnimation = useRef<Map<string, Function>>();
//根据传入的 json 配置 dat.ui 界面
useEffect(() => {
if (!props.guiChangePropertys) return;
const func = () => {
const gui = new GUI({ closed: false });
const allPropertysArr = _.keys(props.guiChangePropertys);
allPropertysArr.forEach((p) => {
const objs = p.split(",");
//获取mesh 对象
// eslint-disable-next-line @typescript-eslint/no-unused-vars
//嵌套获取对象
let object3D: THREE.Object3D | null = null;
objs.forEach((p, index) => {
try {
object3D =
index === 0
? (scene.current?.getObjectByName(p) as THREE.Object3D)
: _.has(object3D, p)
? _.get(object3D, p)
: (object3D?.getObjectByName(p) as THREE.Object3D);
} catch (error) {
return;
}
});
if (!object3D) return;
//如果是数组说明是一个属性有多个子属性
if (props.guiChangePropertys && _.isArray(props.guiChangePropertys[p])) {
const folder = gui.addFolder(p);
folder.open();
const obj = props.guiChangePropertys[p] as {
name: string;
min?: number;
max?: number;
step?: number;
}[];
//展开子属性
obj.forEach((q) => {
if (q.name === "color") {
folder.addColor(object3D as THREE.Object3D, q.name);
} else {
if (_.isFunction(_.get(object3D, q.name))) {
const bindParameter = {
[q.name]: 0,
};
const tmp = folder
.add(bindParameter, q.name, q.min, q.max, q.step)
.listen();
tmp.onFinishChange((value) => {
const tmp = _.get(object3D, q.name) as Function;
tmp.call(object3D, value);
});
} else
folder
.add(object3D as THREE.Object3D, q.name, q.min, q.max, q.step)
.listen();
}
});
// 不是数组
} else {
const obj =
props.guiChangePropertys &&
(props.guiChangePropertys[p] as {
name: string;
min?: number;
max?: number;
step?: number;
});
if (!obj) return;
if (obj.name === "color") {
gui.addColor(object3D as THREE.Object3D, obj.name);
} else {
if (_.isFunction(_.get(object3D, obj.name))) {
const bindParameter = {
[obj.name]: 0,
};
const tmp = gui
.add(bindParameter, obj.name, obj.min, obj.max, obj.step)
.listen();
tmp.onFinishChange((value) => {
const tmp = _.get(object3D, obj.name) as Function;
tmp.call(object3D, value);
});
} else
gui
.add(object3D as THREE.Object3D, obj.name, obj.min, obj.max, obj.step)
.listen();
}
}
});
};
window.onload = func;
}, [props.guiChangePropertys]);
useEffect(() => {
clock.current = new THREE.Clock();
}, []);
//初始化性能监视器
const initStarts = useCallback((placeRenderingRef: React.RefObject<HTMLElement>) => {
var tmp = new Stats();
//默认显示的界面,点击的时候可以切换
tmp.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
// 将stats的界面对应左上角
tmp.dom.style.position = "absolute";
tmp.dom.style.left = "0px";
tmp.dom.style.top = "0px";
placeRenderingRef.current &&
(placeRenderingRef.current as HTMLElement).appendChild(tmp.dom);
starts.current = tmp;
}, []);
//配置相机
const configCamera = useCallback(
(fov: number, cameraPosition: THREE.Vector3, cameraLookAt: THREE.Vector3) => {
if (camera.current instanceof THREE.PerspectiveCamera) {
(camera.current as THREE.PerspectiveCamera).fov = fov;
}
//相机的位置
camera.current?.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
//相机看向的位置
camera.current?.lookAt(cameraLookAt);
//如果有辅助相机,一并更新
if (assistCamera.current instanceof THREE.PerspectiveCamera) {
(assistCamera.current as THREE.PerspectiveCamera).fov = fov;
}
assistCamera.current?.position.set(
cameraPosition.x,
cameraPosition.y,
cameraPosition.z
);
assistCamera.current?.lookAt(cameraLookAt);
},
[]
);
//增加几何体
const addObjectToScene = useCallback(
(mesh: THREE.Object3D[]) =>
mesh.forEach((p) => {
scene.current && scene.current.add(p);
}),
[]
);
//增加各种光源
const addLightsToScene = useCallback(
(lights: THREE.Light[]) =>
lights.forEach((p) => {
if (p instanceof THREE.AmbientLight) {
//环境光不能有俩个
const tmp1 = scene.current?.children.find(
(q) => q instanceof THREE.AmbientLight
);
!tmp1 && scene.current && scene.current.add(p);
} else {
//相同位置不能添加俩次
const tmp2 = scene.current?.children.find((q) => {
return _.isEqual(p.position, q.position);
});
!tmp2 && scene.current && scene.current.add(p);
}
}),
[]
);
//设置初始动画
const configPrimaryAnimation = useCallback((animation: Map<string, Function>) => {
primaryAnimation.current = animation;
}, []);
//执行渲染
const render = useCallback(() => {
//clear 的效果还不知道
// renderer.current && renderer.current?.clear();
primaryAnimation.current &&
primaryAnimation.current.forEach((p) => {
p && p();
});
// 以每秒60次的频率来绘制场景。requestAnimationFrame这个函数,它用来替代 setInterval,
//这个新接口具备多个优点,比如浏览器Tab切换后停止渲染以节约资源、和屏幕刷新同步避免无效刷新、
//在不支持该接口的浏览器中能安全回退为setInterval。
scene.current &&
camera.current &&
renderer.current &&
renderer.current?.render(scene.current, camera.current);
starts.current?.update();
requestAnimationFrame(render);
}, []);
//页面加载的时候生成渲染器
useEffect(() => {
// 需要控制相机的宽高比和屏幕的保持一致,near 尽可能小 far 尽可能大,方便进行缩放,
// 这里的值没有单位是相对的
// 场景渲染用透视相机,符合人眼的近大远小
!camera.current &&
(camera.current = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
1,
10000
));
!scene.current && (scene.current = new THREE.Scene());
!renderer.current &&
(renderer.current = new THREE.WebGLRenderer({
//抗锯齿
antialias: true,
}));
//设置场景的颜色
renderer.current && renderer.current.setClearColor("white");
renderer.current && (renderer.current.pixelRatio = window.devicePixelRatio);
//设置平面网格
const gridHelper = new THREE.GridHelper(80, 30, 0x888888, 0x888888);
gridHelper.position.set(0, 0, 0);
scene.current.add(gridHelper);
}, []);
//点击获取几何体
useEffect(() => {
function onDocumentMouseDown(event: { clientX: number; clientY: number }) {
if (!camera.current || !props.placeRenderingRef.current || !scene.current) return;
//鼠标是二维向量
let mouse = new THREE.Vector2();
//射线是相机到鼠标点击位置之间的一条线
let ray = new THREE.Raycaster();
const px = props.placeRenderingRef.current.getBoundingClientRect().left;
const py = props.placeRenderingRef.current.getBoundingClientRect().top;
//鼠标点位的坐标转换为3D 坐标
//通过鼠标点击位置,计算出 raycaster 所需点的位置,映射到,以画布为中心点,范围 -1 到 1的位置去
//以下的公式是等比列函数换算的
mouse.x =
((event.clientX - px) / props.placeRenderingRef.current.offsetWidth) * 2 - 1;
mouse.y =
-((event.clientY - py) / props.placeRenderingRef.current.offsetHeight) * 2 + 1;
//通过鼠标点击的位置和当前相机的矩阵计算出射线位置
ray.setFromCamera(mouse, camera.current);
// intersectObjects 第一个参数是一个数组 传 场景(scence)的各个对象 第二个参数 是否检查后代对象
// 有些模型是是在group 组里,需要检查后代对象
const intersects = _.unionBy(
ray.intersectObjects(
[
...scene.current.children.filter(
(p) => p.type === "Mesh" || p.type === "Group"
),
],
true
),
"uuid"
);
/*鼠标点和相机之间射线上的所有对象
这将返回一个 Array,其中包含与cubes的孩子的所有光线交点,按距离排序(最近的对象排在第一位)。
每个交集都是一个具有以下属性的对象:
距离:相交发生的距离相机多远
点:光线与其相交的对象中的确切点
面:相交的面。
对象:与哪个对象相交 */
if (intersects.length > 0) {
const meshs = intersects
.filter((q) => q.object.type === "Mesh")
.map((q) => q.object as THREE.Mesh);
const groups = intersects
.filter((q) => q.object.type === "Group")
.map((q) => q.object as THREE.Group);
props.clickHandle && props.clickHandle([...meshs, ...groups]);
}
}
if (props.placeRenderingRef.current && !props.placeRenderingRef.current.onclick) {
const ref = props.placeRenderingRef.current;
ref.addEventListener("click", onDocumentMouseDown);
return () => {
ref.removeEventListener("click", onDocumentMouseDown);
};
}
}, [props, props.clickHandle, props.placeRenderingRef]);
//容器大小变化后,刷新画布
useEffect(() => {
const func = () => {
const ele = props.placeRenderingRef.current as HTMLElement;
//尺寸变化了,必须要更新相机
camera.current &&
((camera.current as THREE.PerspectiveCamera).aspect =
window.innerWidth / window.innerHeight);
camera.current &&
(camera.current as THREE.PerspectiveCamera).updateProjectionMatrix();
renderer.current && (renderer.current.pixelRatio = window.devicePixelRatio);
renderer.current && renderer.current.setSize(ele.clientWidth, ele.clientHeight);
//窗口大小变化后立马刷新下
renderer.current &&
scene.current &&
camera.current &&
renderer.current.render(scene.current, camera.current);
};
window.addEventListener("resize", func);
return () => {
window.removeEventListener("resize", func);
};
}, [props.placeRenderingRef]);
useEffect(() => {
//配置辅助相机
if (scene.current && props.isUseAssistCamera && !assistCamera.current) {
assistCamera.current = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
1,
10000
);
scene.current.add(assistCamera.current);
const cameraHelper = new THREE.CameraHelper(assistCamera.current);
scene.current.add(cameraHelper);
cameraHelper.update();
}
//显示辅助轴
if (scene.current && props.isUseAxes && !axesHelper.current) {
axesHelper.current = new THREE.AxesHelper(800);
scene.current.add(axesHelper.current);
}
//显示相机轨道控制器
if (renderer.current && camera.current) {
if (orbitControls.current && props.isUseOrbitControls) {
orbitControls.current.enabled = true;
return;
}
if (orbitControls.current && !props.isUseOrbitControls) {
orbitControls.current.enabled = false;
return;
}
if (!orbitControls.current && props.isUseOrbitControls) {
orbitControls.current = new OrbitControls(
camera.current,
renderer.current.domElement
);
orbitControls.current.enableDamping = true;
orbitControls.current.addEventListener("change", (e) => {
props.orbitControlsChange && props.orbitControlsChange();
scene.current &&
camera.current &&
renderer.current &&
renderer.current?.render(scene.current, camera.current);
});
}
}
}, [props, props.isUseAssistCamera, props.isUseAxes, props.isUseOrbitControls, render]);
//渲染到dom
useEffect(() => {
if (renderer.current && props.placeRenderingRef.current) {
if ((props.placeRenderingRef.current as HTMLElement).childElementCount === 0) {
const ele = props.placeRenderingRef.current as HTMLElement;
//设置画布初始大小和容器一样大
renderer.current.setSize(ele.clientWidth, ele.clientHeight);
ele.appendChild(renderer.current.domElement);
}
!starts.current &&
props.isUsePerformanceMonitor &&
initStarts(props.placeRenderingRef);
}
}, [initStarts, props.isUsePerformanceMonitor, props.placeRenderingRef]);
//返回
return {
configCamera,
configPrimaryAnimation,
addObjectToScene,
addLightsToScene,
camera,
render,
clock,
};
}
export default useTHREE;
使用自定义 hooks
自定义的 useTHREE(mainCanvas, true, true, true) hooks
需要注的是需要去除 react 的 <React.StrictMode>
,此标记是 react 用来做 hooks 副作用的并发
测试,总是会重复执行 hooks 一次
import React, { useEffect, useRef } from 'react';
import * as THREE from "three";
import { MathUtils, Vector3 } from 'three';
import useTHREE from './myHooks'
import './App.css';
// dat.gui 更改的属性,函数也可以改变 mesh1 是名字
// 提供此对象,能方便的可需要更改属性的几何体进行解耦
const changePropertys: guiChangePropertyType = {
//这种会分组
'mesh1,position': [
{ name: 'x', min: -10, max: 10, step: .01 },
{ name: 'y', min: -10, max: 10, step: .01 },
{ name: 'z', min: -10, max: 10, step: .01 }],
'mesh1,scale': [
{ name: 'x', step: .01 },
{ name: 'y', step: .01 },
{ name: 'z', step: .01 }],
'mesh1': [
{ name: 'rotateX', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'rotateY', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'rotateZ', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'translateX', min: -10, max: 10, step: 0.1 },
{ name: 'translateY', min: -10, max: 10, step: 0.1 },
{ name: 'translateZ', min: -10, max: 10, step: 0.1 },
{ name: 'visible', },
],
'mesh1,material':
{ name: 'color' },
}
function App() {
const mainCanvas = useRef(null)
//使用自定义 THREE hooks
const { configCamera, configPrimaryAnimation, addObjectToScene, addLightsToScene,
camera, render, clock } =
useTHREE({
placeRenderingRef: mainCanvas,
isUseAxes:true,
isUseOrbitControls: true,
isUsePerformanceMonitor:true,
guiChangePropertys: changePropertys,
/** 点击画布的元素拾取 mesh,group */
clickHandle: (clickMesh => {
clickMesh.forEach(console.log)
},
orbitControlsChange: () => {}
});
//页面加载的时候配置初始的THREE
useEffect(() => {
//配置相机 相对单位
configCamera(60, new Vector3(5, 5, 5), new Vector3(0, 0, 0));
//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5,
wireframe: false, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(box, material);
//如果需要 dar.gui 控制属性必须设置 name
mesh.name='mesh1'
addObjectToScene([mesh]);
//配置光照
//0xffffff 白色
/** 光线太亮的话,物体的棱角不会太明显 pointLight 光源的位置不对的话,棱角也不会明显 */
const ambientLight = new THREE.AmbientLight('white', 0.6);
const pointLight = new THREE.PointLight('white', 0.1)
pointLight.position.set(1, 20, 30)
// 只有环境光,没有点光或聚光的话,显示不出暗面,就不能突出棱角
addLightsToScene([ambientLight, pointLight]);
//配置初始动画
configPrimaryAnimation(new Map<string, Function>([["a1", () =>
mesh.rotateX(MathUtils.degToRad(1))]]))
//改变相机,也可以做动画的效果
//camera.current && (camera.current.position.x+=0.01)
}, [addLightsToScene, configPrimaryAnimation, configCamera, addObjectToScene])
useEffect(() => {
if (!mainCanvas.current) return
render();
}, [render])
//更新相机的 fov ,相机的距离并没有改变,但是物体的远近效果变了
const changeFov=(fov:number)=>{
(camera.current as THREE.PerspectiveCamera).fov = fov;
(camera.current as THREE.PerspectiveCamera)?.updateProjectionMatrix()
}
return (
<div className="App">
{/* 画布要设置大小 */}
<div ref={mainCanvas} style={{width:'90rem',height:'40rem',border:'2px solid blue'}}/>
</div>
);
}
export default App;
光源
threejs 渲染的真实性和光源的使用有很大的关系,THREE.Light 光源的基类。 threejs 在光源上常 见的种类:
THREE.AmbientLight 环境光,处于多次漫反射的形成的光,这种光任何几何体的都能够照射到,没有 方向性,所以不会产生阴影效果,光的强度在任何地方都一样的,不会衰减。主要是均匀整体改变 Threejs 物体表面的明暗效果,这一点和具有方向的光源不同,比如点光源可以让物体表面不同区域 明暗程度不同。
THREE.DirectionalLight 平行光(方向光) 可以看做是模拟太阳发出的光源,这个光源所发出的光都 是相互平行的。光的强度在照射的范围内是一样的,不会产生阴影效果平行光顾名思义光线平行,对 于一个平面而言,平面不同区域接收到平行光的入射角一样。点光源因为是向四周发散,所以设置好 位置属性.position 就可以确定光线和物体表面的夹角,对于平行光而言,主要是确定光线的方向,光 线方向设定好了,光线的与物体表面入射角就确定了,仅仅设置光线位置是不起作用的。在三维空间 中为了确定一条直线的方向只需要确定直线上两个点的坐标即可,所以 Threejs 平行光提供了位置 .position 和目标.target 两个属性来一起确定平行光方向。目标.target 的属性值可以是 Threejs 场景中任何一个三维模型对象,比如一个网格模型 Mesh,这样 Threejs 计算平行光照射方向的时候 ,会通过自身位置属性.position 和.target 表示的物体的位置属性.position 计算出来。
THREE.SpotLight 聚灯光,类似于射灯,在照射的中心很亮,边缘区域很暗,有阴影效果聚光源可以 认为是一个沿着特定方会逐渐发散的光源,照射范围在三维空间中构成一个圆锥体。通过属性.angle 可以设置聚光源发散角度,聚光源照射方向设置和平行光光源一样是通过位置.position 和目标 .target 两个属性来实现。
THREE.PointLight 点光源就像生活中的白炽灯,光线沿着发光核心向外发散,同一平面的不同位置 与点光源光线入射角是不同的,点光源照射下,同一个平面不同区域是呈现出不同的明暗效果。和环 境光不同,环境光不需要设置光源位置,而点光源需要设置位置属性.position,光源位置不同,物 体表面被照亮的面不同,远近不同因为衰减明暗程度不同。
仅仅使用环境光的情况下,你会发现整个立方体没有任何棱角感,这是因为环境光只是设置整个空间 的明暗效果。如果需要立方体渲染要想有立体效果,需要使用具有方向性的点光源、平行光源等。
光线线是和几何体的法线和材质有关系
- MeshBasicMaterial 光线影响几何体的明暗,不会影响颜色的反射。这种材质不会反射光线
- MeshLambertMaterial 材质的法线和颜色都会影响光线,这种材质会反射光线,环境光和法线没有关 系,是整体的变亮或变暗,带有方向的光源受法线影响如果光线是红色的,几何体是绿色,显示的效 果是几何体是黑色的,因为几何体吸收的是除过绿色的其他光
点,线,三角面,几何体
THREE 所有的几何体都是通过 THREE.BufferAttribute THREE.BufferGeometry 俩个 API 生成的在 R125 版本开始移除了 THREE.Geometry,代码被移到 three/examples/jsm/deprecated/Geometry 里
顶点位置数据渲染
通过 THREE.BufferGeometry 可以够着自定义的图形
const geo = new THREE.BufferGeometry();
//有点可以组成线,线组成面。 THREE 的面是三角面,需要3个顶点
const arr = new Float32Array([
0, 0, 0, 2, 0, 2, 2, 0, 0,
0, 0, 0, 2, 0, 2, 2, 2, 2,
0, 0, 0, 0, 0, 2, 2, 2, 2,
]);
//缓存 3个一组
const points = new THREE.BufferAttribute(arr, 3);
//给几何体增加点
geo.setAttribute("position", points);
//自定义面测下只能用 MeshBasicMaterial 材质
const material = new THREE.MeshBasicMaterial({ color: "red", side: DoubleSide });
const obj = new THREE.Mesh(geo, material);
addObjectToScene([obj]);
如上代码绘制的 3 个三角面
渲染成点模型,修改材质
var material=new THREE.PointsMaterial({
color:'red'
size: 0.5 //点大小
});
const obj=new THREE.Points(geo,material)
addObjectToScene([obj]);
如上代码绘制的 9 个点
渲染成线模型,修改材质
var material = new THREE.LineBasicMaterial({
color: "red",
}); //线条颜色
var obj = new THREE.Line(geo, material);
addObjectToScene([obj]);
如上代码绘制的 9 条线
几何体的本质
立方体几何体 BoxGeometry 本质上就是一系列的顶点构成,只是 Threejs 的 APIBoxGeometry 把顶点 的生成细节封装了,用户可以直接使用。比如一个立方体网格模型,有 6 个面,每个面至少两个三角 形拼成一个矩形平面,每个三角形三个顶点构成,对于球体网格模型而言,同样是通过三角形拼出来一 个球面,三角形数量越多,网格模型表面越接近于球形。
如下的球体,查看顶点,和线 mesh ,漂亮的球体也是有大量的点连城的线绘制的三角面
const sphere = new THREE.SphereGeometry(2);
const materia = new THREE.PointsMaterial({
color: "pink",
size: 0.1,
});
const points = new THREE.Points(sphere, materia);
addObjectToScene([points]);
const sphere = new THREE.BoxGeometry(2);
const materia = new THREE.LineBasicMaterial({
color: "pink",
side: DoubleSide,
});
const points = new THREE.Line(sphere, materia);
addObjectToScene([points]);
颜色插值计算
线段颜色有顶点颜色插值得来
const line = new THREE.BufferGeometry();
//线段起点终点
const pointArr = new Float32Array([0, 0, 0, 0, 0, 2]);
line.setAttribute("position", new THREE.Float32BufferAttribute(pointArr, 3));
//线段端点颜色 插值的颜色是0-1 而不是 0-255
const colorArr = new Float32Array([
1,
0,
0, //顶点1颜色
0,
1,
0, //顶点2颜色
]);
line.setAttribute("color", new THREE.Float32BufferAttribute(colorArr, 3));
//线段材质
const material = new THREE.LineBasicMaterial({
//颜色有顶点决定
vertexColors: true,
side: THREE.DoubleSide,
});
const obj = new THREE.Line(line, material);
obj.position.z = 0.5;
obj.position.y = 0.5;
addObjectToScene([obj]);
面颜色插值
//有顶点组成面
const rectangle = new THREE.BufferGeometry();
//逆时针放置点
const pointArr = new Float32Array([
0, 0, 0, 0, 0, 2, 2, 0, 0,
0, 0, 2, 2, 0, 2, 2, 0, 0,
]);
rectangle.setAttribute("position", new THREE.Float32BufferAttribute(pointArr, 3));
const colorArr = new Float32Array([
1,
0,
0, //顶点0颜色 红
0,
1,
0, //顶点1颜色 绿
0,
0,
1, //顶点2颜色 蓝
0,
1,
0, //顶点3颜色
1,
0,
0, //顶点4颜色
0,
0,
1, //顶点5颜色
]);
rectangle.setAttribute("color", new THREE.Float32BufferAttribute(colorArr, 3));
const material = new THREE.MeshBasicMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});
const obj = new THREE.Mesh(rectangle, material);
addObjectToScene([obj]);
带法线的几何体
如果渲染的材质是可以反射光的材质,必须要设置法向
上面的例子面插值,如果更改材质为 MeshLambertMaterial,面显示黑色,就是因为没有设置法线的原 因
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});
增加法线后 MeshLambertMaterial 材质才知道如何反射光线
//增加法线,用来反射光线,每个顶点都要定义
const normalArr = new Float32Array([
0, 1, 0, 0, 1, 0, 0, 1, 0,
0, 1, 0, 0, 1, 0, 0, 1, 0,
]);
rectangle.setAttribute("normal", new THREE.Float32BufferAttribute(normalArr, 3));
通过索引减少重复的点
绘制一个正方形常规是用 4 个点,而不是 6 个点,有俩个点是重复的,但是在 THREE 里正方形是由 三角形拼接的,不考虑分段,一个正方形需要俩个三角形才能拼接出来俩个三角形是 6 个顶点,有俩 个是重复的,通过索引复用顶点可以用四个点绘制正方形
//逆时针放置四个点
const pointArr = new Float32Array([
0,
0,
0, //顶点0
0,
0,
2, //顶点1
2,
0,
2, //顶点2
2,
0,
0, //顶点3
]);
//四个点的颜色
const colorArr = new Float32Array([
1,
0,
0, //顶点0颜色 红
0,
1,
0, //顶点1颜色 绿
0,
0,
1, //顶点2颜色 蓝
1,
0,
0, //顶点4颜色 红
]);
//四个点的法线增加法线,用来反射光线
const normalArr = new Float32Array([
0,
1,
0, //顶点1法线
0,
1,
0, //顶点2法线
0,
1,
0, //顶点3法线
0,
1,
0, //顶点4法线
]);
//通过索引绘制俩个三角形,逆时针绘制三角形
//必须是无符号整数
const indexArr = new Uint16Array([
0,
1,
3, //第一个三角形
1,
2,
3, //第二个三角形
]);
//有顶点组成面
const rectangle = new THREE.BufferGeometry();
rectangle.setAttribute("position", new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.setAttribute("normal", new THREE.Float32BufferAttribute(normalArr, 3));
rectangle.setAttribute("color", new THREE.Float32BufferAttribute(colorArr, 3));
//设置索引
rectangle.index = new THREE.BufferAttribute(indexArr, 1);
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});
const obj1 = new THREE.Mesh(rectangle, material);
obj1.position.x = -5;
addObjectToScene([obj1]);
通过索引自定义正方体
//逆时针放置点
const pointArr = new Float32Array([
0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0,
]);
const colorArr = new Float32Array([
1,
0,
0, //顶点0颜色 红
0,
1,
0, //顶点1颜色 绿
0,
0,
1, //顶点2颜色 蓝
1,
0,
0, //顶点4颜色 红
1,
0,
0, //顶点0颜色 红
0,
1,
0, //顶点1颜色 绿
0,
0,
1, //顶点2颜色 蓝
1,
0,
0, //顶点4颜色 红
]);
//增加法线,用来反射光线
const normalArr = new Float32Array([
0,
1,
0, //顶点1法线
0,
1,
0, //顶点2法线
0,
1,
0, //顶点3法线
0,
1,
0, //顶点4法线
0,
1,
0, //顶点1法线
0,
1,
0, //顶点2法线
0,
1,
0, //顶点3法线
0,
1,
0, //顶点4法线
]);
//必须是无符号整数
const indexArr = new Uint8Array([
0, 1, 2, 2, 3, 0,
3, 2, 5, 5, 4, 3,
6, 7, 4, 4, 6, 6,
1, 6, 2, 2, 6, 5,
0, 7, 4, 4, 3, 0,
0, 1, 7, 7, 1, 6,
]);
//有顶点组成面
const rectangle = new THREE.BufferGeometry();
//geometry.setIndex( indices );
rectangle.setAttribute("position", new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.setAttribute("normal", new THREE.Float32BufferAttribute(normalArr, 3));
rectangle.setAttribute("color", new THREE.Float32BufferAttribute(colorArr, 3));
rectangle.index = new THREE.BufferAttribute(indexArr, 1);
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
wireframe: true,
});
const obj = new THREE.Mesh(rectangle, material);
addObjectToScene([obj]);
访问几何体数据及拷贝复制
访问几何体并更改数据
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(box, material);
//可以访问几何体的高度可以,但是更改几何体的尺寸后,THREE.js 并不会去渲染修改后的数据
mesh.geometry.parameters.height = 2;
//只能通过 scale 去更改
//几何体的 scale 会改变几何体的顶点坐标
box.scale.set(1, 2, 1);
//mesh的 scale 不会改变顶点数据
mesh.scale.set(1, 2, 1);
//如果更改属性长宽高要通过 mesh 的缩放
//可以更改几何体的材质属性
mesh.material.color = new THREE.Color("red");
addObjectToScene([mesh]);
拷贝复制
对几何体 colone 是深拷贝,对 mesh colone 是浅 colone
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide,
});
console.log(box.id); //5
const newBox = box.clone();
console.log(newBox.id); //6
console.log(material.id); //8
const newMaterial = material.clone();
console.log(newMaterial.id); //9
const mesh = new THREE.Mesh(box, material);
console.log(mesh.id); //13
console.log(mesh.geometry.id); //5
console.log(mesh.material.id); //8
const newMesh = mesh.clone();
console.log(newMesh.id); //14
console.log(newMesh.geometry.id); //5
console.log(newMesh.material.id); //8
mesh 的 copy 是浅 copy
//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide,
});
console.log(box.id); //5
const newBox = new THREE.BoxGeometry().copy(box);
console.log(newBox.id); //6
const mesh = new THREE.Mesh(newBox, material);
console.log(mesh.geometry.id); //6
console.log(mesh.material.id); //8
const newMesh = new THREE.Mesh().copy(mesh);
console.log(newMesh.geometry.id); //6
console.log((newMesh.material as THREE.MeshLambertMaterial).id); //8
本地坐标和世界坐标
世界坐标中心点默认在画布的中心位置,一个 scene 只有一个世界坐标,方向参考右手定则默认世 界坐标的中心画布中心,通过如下方法可以更改世界坐标的中心 scene.current.position.set(1,1,1)
本地坐标,每个 mesh 都有一个本地坐标,默认坐标的中心在几何体的中心位置在不指定 mesh 本地 坐标的情况下,默认本地坐标和世界坐标的中心重合本地坐标的 position 是参考世界坐标的,对 mesh 进行 平移,缩放,旋转 会改变 mesh 的 position 位置
上面的自定义正方体的例子,默认本地坐标和世界坐标是重合的,对 mesh 进行平移,就能看出来本地 坐标的中心在几何体的中心位置(图中的几何体的中心点在坐标 (0,0,0) 位置) 对几何体的进行平移, 缩放,旋转的参考都参考几何体本地坐标进行的,会改变几何体的顶点数据,因为顶点的坐标是参考本 地坐标定义的
对 mesh 进行 平移,缩放,旋转 ,参考的是本地坐标,mesh 内的几何体的顶点坐标相对 mesh 的本 地坐标并没有发生改变(除过缩放),因为 THREE.js 会一起更新几何体,
对 mesh 进行 平移,缩放,旋转 ,能改变 mesh 相对于世界坐标的 position 位置
const obj = new THREE.Mesh(rectangle, material);
//让 mesh 显示坐标辅助
const axesHelper = new THREE.AxesHelper(3);
obj.add(axesHelper);
// mesh 进行移动,其实改变的是 mesh 的本地坐标,几何体的顶点数据并没有改变
obj.translateX(2);
obj.translateZ(-2);
旋转 缩放 平移 ,旋转方向是右手定则
旋转 底层是欧拉角 Euler
欧拉角是用来表示三维坐标系中方向和方向变换的
对几何进行旋转,会改变几何体的顶点坐标,因为本地坐标没有旋转,相对的几何体的顶点坐标就变化 了对 mesh ,group 对象进行旋转不对改变几何体的顶点坐标,因为几何体的顶点坐标是参考本地坐标 的在本地坐标旋转的时候,几何体的本地顶点坐标也会跟着一起变化。
rotateX, rotateY,rotateZ,
要有旋转的效果必须渲染函数里重复调用
测试的版本是 R144
对于 mesh 或 geometry 旋转效果是围绕几何体自身的本地坐标轴旋转 group 对象是围绕世界坐标 系旋转
还是自定义正方体的例子.本地坐标没有改变
对 mesh 进行旋转参考的坐标轴是几何体本地的坐标轴,
const obj = new THREE.Mesh(rectangle, material);
console.table(rectangle.getAttribute("position"));
//对几何体的本地坐标参考自己的 Z 轴进行旋转90度
obj.rotateZ(Math.PI * 0.5);
//对几何体的本地坐标 在自己的 Y 轴方向移动 单位1
obj.translateY(1);
//在目前的本地坐标内,对几何体在 x 轴移动 单位1
rectangle.translate(1, 0, 0);
console.table(rectangle.getAttribute("position"));
const axesHelper = new THREE.AxesHelper(3);
obj.add(axesHelper);
addObjectToScene([obj]);
rotateOnAxis,
rotateOnWorldAxis 以世界坐标系为起点
因为向量的原因只能是 mesh group 使用此 API
如果绕 Y 轴旋转如下设置 rotateOnAxis(new THREE.Vector3(0,1,0) ,MathUtils.degToRad(1)) 如果 绕 Y 轴反向旋转如下设置 rotateOnAxis(new THREE.Vector3(0,-1,0) ,MathUtils.degToRad(1))
以 mesh 本地坐标中心为起点 new Vector3(1,1,1) 为终点的自定义旋转轴
const obj1 = new THREE.Mesh(rectangle, material);
obj1.translateX(1);
const obj2 = obj1.clone();
const v3 = new Vector3(1, 1, 1);
obj2.rotateOnAxis(v3, Math.PI * 0.25);
obj2.translateOnAxis(v3, 1);
const axesHelper = new THREE.AxesHelper(1.5);
obj1.add(axesHelper);
obj2.add(axesHelper.clone());
addObjectToScene([obj1, obj2]);
当父对象旋转的时候,子对象绕父对象旋转
const cylinder = new THREE.CylinderGeometry(0.5, 1, 4);
const cylinderMesh = new THREE.Mesh(cylinder, material);
cylinderMesh.position.set(5, cylinder.parameters.height / 2, 0);
const box = new THREE.BoxGeometry(1.2, 1.2, 1.2);
const boxMesh = new THREE.Mesh(box, material);
boxMesh1.position.set(2, box1.parameters.height / 2 - cylinder.parameters.height / 2, 2);
//添加子对象
cylinderMesh.add(boxMesh1);
addMeshesToScene([cylinderMesh]);
//父对象旋转
configPrimaryAnimation(
new Map<string, Function>([["a1", () => cylinderMesh.rotateY(MathUtils.degToRad(1))]])
);
平移
translateX ,translateY, translateZ
对几何进行 translate,会改变几何体的顶点坐标, 因为本地坐标没有 translate 对 mesh ,组对象 进行 translate 不会改变顶点坐标,因为几何体的顶点坐标会跟着 mesh 的本地坐标进行 translate
还是自定义正方体的例子.本地坐标没有改变
此正方体的长宽高都是 1,先对几何体的本地坐标参考自己的 Y 轴旋转 90 度在对几何体的本地坐标 x 轴上参考自己的 X 轴 平移 1,在对几何体在参考本地坐标 X 轴平移 -1
const obj = new THREE.Mesh(rectangle, material);
console.table(rectangle.getAttribute("position"));
// 让 mesh 的本地坐标进行平移,几何体的顶点数据并没有改变
// 平移的参考点并不是世界坐标,是参考自己的本地坐标
console.table(rectangle.getAttribute("position"));
//本地坐标参考自己旋转 90 度
obj.rotateY(Math.PI * 0.5);
//本地坐标在当前的本地坐标系内 X 轴平移 1 个单位
obj.translateX(1);
console.table(rectangle.getAttribute("position"));
//几何体参考自己本地坐标 X 轴平移 -1 个单位
rectangle.translate(-1, 0, 0);
console.table(rectangle.getAttribute("position"));
const axesHelper = new THREE.AxesHelper(3);
obj.add(axesHelper);
translateOnAxis 沿着着指定的向量方向平移一定的距离
translateOnAxis 可以灵活的向任意方向平移,因为向量的原因只能是 mesh group 使用此 API
const obj = new THREE.Mesh(rectangle, material);
//对第一个mesh X 轴平移 1个单位
obj1.translateX(1);
//克隆第一个 mesh
const obj2 = obj.clone();
//对第二个 mesh2 以自己本地坐标系的中心为起点,以 new Vector3(1,1,1)为终点作为平移的参考线
const v3 = new Vector3(1, 1, 1);
obj2.translateOnAxis(v3, 1);
const axesHelper = new THREE.AxesHelper(3);
obj1.add(axesHelper);
obj2.add(axesHelper.clone());
addObjectToScene([obj1, obj2]);
缩放
对几何体进行 scale,会改变几何体的顶点坐标, 缩放是三个方向都可以的 scale(x,y,z)
几何体 scale(-x,-y,-z) 的效果是几何体会到反方向的底部。
对 mesh , group 进行 scale ,几何体的体积会变化,但是比较奇怪的是顶点坐标不会变化。或许是 为了和 平移,旋转 保持一致吧
mesh scale(-x,-y,-z) 的效果是几何体会到反方向的底部。坐标轴也会到反方向的底部
设置几何体的几何中心
center 几何体调用此方法后会让几何体的中心居于本地坐标的中心,而不是默认的顶点坐标最小点 会改变几何体的所有顶点坐标
const obj = new THREE.Mesh(rectangle, material);
console.table(rectangle.getAttribute("position"));
console.table(obj.position);
obj.translateX(1);
obj.translateZ(1);
rectangle.center();
console.table(rectangle.getAttribute("position"));
console.table(obj.position);
const axesHelper = new THREE.AxesHelper(3);
obj.add(axesHelper);
addObjectToScene([obj]);
矩阵变换、欧拉、四元数、
几何体的层次结构
通过 group 建立层级关系 嵌套的层级关系,里面的对象参考外面对象的坐标
const ax = new THREE.AxesHelper(5);
//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(box, material);
mesh.translateX(1);
mesh.translateZ(1);
mesh.translateY(0.5);
//通过 group 可以建立层级关系
const group = new THREE.Group();
group.add(mesh);
group.position.set(1, 1, 1);
console.log(group.position); // 1,1,1
console.log(mesh.position); // 1,0.5,1
//通过 getWorldPosition 获取对象的相对世界坐标的位置
const v3 = new THREE.Vector3();
mesh.getWorldPosition(v3);
console.log(v3); //2,1.5,2
group.add(ax.clone());
mesh.add(ax);
addObjectToScene([group]);
group.remove 方法也可以移除添加的对象
group.add(mesh);
const newMesh = mesh.clone().translateX(2);
group.add(newMesh);
group.remove(newMesh);
mesh 之间也可以建立层级关系 嵌套的层级关系,里面的对象参考外面对象的坐标 ** mesh.remove 也可以移除内部的对象
const ax = new THREE.AxesHelper(5);
//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide,
});
const v3 = new THREE.Vector3();
const mesh = new THREE.Mesh(box, material);
mesh.translateX(1);
mesh.translateZ(1);
mesh.translateY(0.5);
const group = new THREE.Group();
group.position.set(1, 0, 1);
group.add(ax.clone());
group.add(mesh);
const newMesh = mesh.clone().translateX(1);
newMesh.material = new THREE.MeshLambertMaterial().copy(mesh.material);
newMesh.material.color = new THREE.Color("red");
newMesh.position.set(1, 0.5, 1);
//mesh 之间建立的层级关系
mesh.add(newMesh.add(ax.clone()));
newMesh.getWorldPosition(v3); //3,1,3
console.log(v3);
mesh.add(ax);
addObjectToScene([group]);
绘制平面图形
直线
const ax = new THREE.AxesHelper(5);
const startV2 = new THREE.Vector2(0, 0);
const endV2 = new THREE.Vector2(5, 5);
const line = new THREE.LineCurve(startV2, endV2);
const geo = new THREE.BufferGeometry().setFromPoints(line.getPoints(50));
const mesh = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "pink",
})
);
mesh.rotateX(Math.PI * 0.5);
mesh.add(ax);
addObjectToScene([mesh]);
三位空间的直线
const ax = new THREE.AxesHelper(5);
const startV3 = new THREE.Vector3(0, 0, 0);
const endV3 = new THREE.Vector3(2, 2, 2);
const geo = new THREE.BufferGeometry().setFromPoints([startV3, endV3]);
const mesh = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "pink",
})
);
mesh.add(ax);
addObjectToScene([mesh]);
const ax = new THREE.AxesHelper(5);
const startV2 = new THREE.Vector3(0, 0);
const endV2 = new THREE.Vector3(5, 5, 5);
const line = new THREE.LineCurve3(startV2, endV2);
const geo = new THREE.BufferGeometry().setFromPoints(line.getPoints(50));
const mesh = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "pink",
})
);
mesh.add(ax);
addObjectToScene([mesh]);
圆弧
ArcCurve
const ax = new THREE.AxesHelper(5);
// aClockwise 表示绘制的是是否按照顺时针绘制
const arc = new THREE.ArcCurve(0, 0, 5, 0, Math.PI, false);
//平面图形必须要转换为三维图形
//arc.getPoints(50) 在指线上取指定的顶点
const geo = new THREE.BufferGeometry().setFromPoints(arc.getPoints(50));
const mesh1 = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "red",
})
);
mesh
.rotateX(Math.PI * 0.5)
.translateX(2)
.translateY(1);
mesh.add(ax);
addObjectToScene([mesh]);
椭圆
Ellipse
const ax = new THREE.AxesHelper(5);
//最后一个参数指定旋转椭圆的弧度
const ellipse = new THREE.EllipseCurve(0, 0, 5, 2, 0, 2 * Math.PI, false, Math.PI * 0.5);
const geo = new THREE.BufferGeometry().setFromPoints(ellipse.getPoints(50));
const mesh = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "red",
})
);
mesh.rotateX(Math.PI * 0.5);
mesh.add(ax);
addObjectToScene([mesh]);
样条曲线
-
SplineCurve 二维样条曲线
-
CatmullRomCurve3 三维样条曲线
const ax = new THREE.AxesHelper(5);
const curve1 = new THREE.CatmullRomCurve3(
[new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 1), new THREE.Vector3(1, 1, -1)],
true
);
const curve2 = new THREE.CatmullRomCurve3(
[new THREE.Vector3(0, 0, 0), new THREE.Vector3(-1, 1, 1), new THREE.Vector3(-1, 1, -1)],
true
);
const geo1 = new THREE.BufferGeometry().setFromPoints(curve1.getPoints(50));
const geo2 = new THREE.BufferGeometry().setFromPoints(curve2.getPoints(50));
const mesh1 = new THREE.Line(geo1, new LineBasicMaterial({ color: "#B09A37" }));
const mesh2 = new THREE.Line(geo2, new LineBasicMaterial({ color: "#B09A37" }));
const group = new THREE.Group();
group.add(mesh1, mesh2);
group.add(ax);
addObjectToScene([group]);
贝塞尔曲线
贝塞尔曲线有二维和三维的
const ax = new THREE.AxesHelper(5);
//二阶三维贝塞尔
const curve1 = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(1, 1, 1),
new THREE.Vector3(1, 1, -1)
);
const curve2 = new THREE.CubicBezierCurve3(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(-1, 1, 1),
new THREE.Vector3(-1, 1, -1),
new THREE.Vector3(-1, 1, -2)
);
//三阶三维贝塞尔
const geo1 = new THREE.BufferGeometry().setFromPoints(curve1.getPoints(50));
const geo2 = new THREE.BufferGeometry().setFromPoints(curve2.getPoints(50));
const mesh1 = new THREE.Line(geo1, new LineBasicMaterial({ color: "#B09A37" }));
const mesh2 = new THREE.Line(geo2, new LineBasicMaterial({ color: "#B09A37" }));
const group = new THREE.Group();
group.add(mesh1, mesh2);
group.add(ax);
addObjectToScene([group]);
多个线条组合曲线 CurvePath
绘制跑道,注意要按照固定的顺序绘制,此线是按照顺时针绘制的
const ax = new THREE.AxesHelper(5);
const curve1 = new THREE.ArcCurve(0, 5, 2, Math.PI, 0, true);
const curve2 = new THREE.LineCurve(new THREE.Vector2(2, 5), new THREE.Vector2(2, 2));
const curve3 = new THREE.ArcCurve(0, 2, 2, 0, Math.PI, true);
const curve4 = new THREE.LineCurve(new THREE.Vector2(-2, 2), new THREE.Vector2(-2, 5));
const curvePath = new THREE.CurvePath();
curvePath.curves.push(curve1, curve2, curve3, curve4);
const geo = new THREE.BufferGeometry();
geo.setFromPoints(curvePath.getPoints(200) as THREE.Vector2[]);
const line = new THREE.Line(
geo,
new THREE.LineBasicMaterial({
color: "#022239",
})
);
line.rotateX(Math.PI * 0.5).translateX(2);
line.add(ax);
addObjectToScene([line]);
path
只是线段,没有三角面,path 能显示的只能是点及有点组成的线段,不能渲染成形状
//也可以在构造函数里传入点
const path = new THREE.Path();
path.moveTo(2, 1);
path.lineTo(3, 1);
//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
path.bezierCurveTo(3, 6, 5, 6, 5, 1);
//不带 to的 是坐标从零开始计算
path.arc(1, 0, 1, Math.PI, 0, true);
//在回到之前的坐标
path.lineTo(8, 1);
path.lineTo(8, 0.5);
path.lineTo(2, 0.5);
path.lineTo(2, 1);
//shape.arc(1,3,1,-Math.PI*0.5,Math.PI*0.5,false)
const geo = new THREE.BufferGeometry().setFromPoints(path.getPoints(30));
const mesh = new THREE.Line(
geo,
new LineBasicMaterial({ color: "red", side: THREE.DoubleSide })
);
addObjectToScene([mesh]);
shape
shape 类似 HTML DOM Canvas 对象,通过路径来绘制二维形状平面。简单理解就是在一个平面上用 不规则的线连接成一个图形
多个线条的组合
// shape 类似 HTML DOM Canvas 对象
const shape = new THREE.Shape();
shape.moveTo(2, 1);
shape.lineTo(3, 1);
//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
shape.bezierCurveTo(3, 6, 5, 6, 5, 1);
//不带 to的 是坐标从零开始计算
shape.arc(1, 0, 1, Math.PI, 0, true);
//在回到之前的坐标
shape.lineTo(8, 1);
shape.lineTo(8, 0.5);
shape.lineTo(2, 0.5);
//shape.arc(1,3,1,-Math.PI*0.5,Math.PI*0.5,false)
const geo = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(
geo,
new MeshLambertMaterial({ color: "red", side: THREE.DoubleSide, wireframe: false })
);
mesh.add(ax);
addObjectToScene([mesh]);
多个点的组合
const points = [
new THREE.Vector2(1, 1),
new THREE.Vector2(2, 0),
new THREE.Vector2(3, 1),
new THREE.Vector2(3, 2),
new THREE.Vector2(2, 3),
new THREE.Vector2(1, 2),
];
// shape 类似 HTML DOM Canvas 对象
const shape = new THREE.Shape(points);
const geo = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(
geo,
new MeshLambertMaterial({ color: "red", side: THREE.DoubleSide, wireframe: true })
);
addObjectToScene([mesh]);
在形状内部挖孔
shape.holes 可以用 path 或 shape 去挖 shape 的孔
const shape = new THREE.Shape();
shape.moveTo(2, 1);
shape.lineTo(3, 1);
//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
shape.bezierCurveTo(3, 6, 5, 6, 5, 1);
//不带 to的 是坐标从零开始计算
shape.arc(1, 0, 1, Math.PI, 0, true);
//在回到之前的坐标
shape.lineTo(8, 1);
shape.lineTo(8, 0.5);
shape.lineTo(2, 0.5);
shape.lineTo(2, 1);
const path1 = new THREE.Path();
path1.arc(4, 3, 0.5, 0, Math.PI * 2, false);
const path2 = new THREE.Path();
path2.arc(6, 1.2, 0.5, 0, Math.PI * 2, true);
//用path 去挖孔
shape.holes.push(path1, path2);
const geo = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(
geo,
new MeshLambertMaterial({ color: "red", wireframe: false, side: THREE.DoubleSide })
);
addObjectToScene([mesh]);
多个 shape 的组合
const shape1 = new THREE.Shape();
shape1.arc(1, 1, 1, 0, Math.PI * 2, false);
const shape2 = new THREE.Shape();
shape2.arc(2.4, 1, 1, 0, Math.PI * 2, false);
const shape3 = new THREE.Shape();
shape3.arc(1.6, 2.3, 1, 0, Math.PI * 2, false);
const geo = new THREE.ShapeGeometry([shape1, shape2, shape3]);
const mesh = new THREE.Mesh(
geo,
new MeshLambertMaterial({ color: "red", wireframe: false, side: THREE.DoubleSide })
);
const shape4 = new THREE.Shape();
shape4.arc(1, 1, 1, 0, Math.PI * 2, false);
const shape5 = new THREE.Shape();
shape5.arc(3, 1, 1, 0, Math.PI * 2, false);
const shape6 = new THREE.Shape();
shape6.arc(2, 2.7, 1, 0, Math.PI * 2, false);
const geo1 = new THREE.ShapeGeometry([shape4, shape5, shape6]);
const mesh1 = new THREE.Mesh(
geo1,
new MeshLambertMaterial({ color: "red", wireframe: false, side: THREE.DoubleSide })
);
mesh1.translateX(5);
addObjectToScene([mesh, mesh1]);