赞
踩
在上篇博客中介绍了如何快速利用react搭建three.js平台,并实现3D模型的可视化。本文将在上一篇的基础上强化坐标系的概念。引入AxesHelper辅助工具及文本绘制工具,带你快速理解camer、坐标系、position、可视区域。
three.js提供了 AxesHelper方法,在3D场景中建立一个,x、y、z两两垂直的坐标系。由于在网页中通常用z-index表示高度或者层级,在3D场景中z表示离屏幕的距离,z越大,物体离实现越近。所以在3D中的坐标系默认是y轴朝上。
辅助坐标系便于开发者或用户在视觉上识别和定位场景中的物体。坐标轴辅助器通常在开发阶段用于调试,但在最终的产品中也可以保留,以帮助用户理解3D空间
- const axesHelper = new THREE.AxesHelper(150);
- scene.add(axesHelper);
AxesHelper
是Three.js库中的一个类,用于创建一个可视化的坐标轴(X轴、Y轴和Z轴)。括号中的150
是一个参数,表示坐标轴的长度为150个单位。 通过THREE的AxesHelper方法创建,并通过scene添加到场景中。
通过这个方法可以看到在3D中生成了坐标轴辅助线。
three.js坐标轴颜色红R、绿G、蓝B分别对应坐标系的x、y、z轴
在旋转过程中坐标系可能会混乱,虽然你强迫自己记住xyz对应红绿蓝,但是用着用着就蒙了。可以借助文字辅助理解。然而在threeJS中文本也是当物体绘制出来。不是几行代码就能实现的。这里文字需要通过load进行加载,并且文字有自己的position,在循环动画时添加文本的lookAt为camera的position,让文本始终看着相机,即可实现文本朝向用户。
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";
- //创建文本
- function createText(content: string, font: any) {
- const textGeometry = new TextGeometry(content, {
- font: font,
- size: 1,
- height: 0.1,
- curveSegments: 1,
- });
- textGeometry.center();
- const textMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); // front
- const mesh = new THREE.Mesh(textGeometry, textMaterial);
- return mesh;
- }
- //坐标系添加文字
- const loader = new FontLoader();
- let meshX = new THREE.Mesh();
- let meshY = new THREE.Mesh();
- let meshZ = new THREE.Mesh();
- loader.load("fonts/optimer_regular.typeface.json", function (font) {
- meshX = createText("X", font);
- meshY = createText("Y", font);
- meshZ = createText("Z", font);
- meshX.position.x = 12;
- meshY.position.y = 12;
- meshZ.position.z = 12;
- scene.add(meshX);
- scene.add(meshY);
- scene.add(meshZ);
- });
这个json后缀的文件是用来解析字体的,load的第一个参数是json的url。json数据从哪获取?
打卡node_modules或者你下的threejs源码,找到examples/fonts,将其复制到public文件夹。就可以引入了
在学习threeJS过程中,我也是不断参考example示例,找所需的资源
相机在3D场景的作用可以想象成是你手中的一个真实的相机或者你的眼睛。在现实生活中,你站在某个地方,你的位置就是相机的位置。比如说,你站在一个公园的角落,从这个位置你可以看到公园里的不同景物。在Three.js中,
camera
代表了观察者(可以是你的眼睛或者一个虚拟的摄像头)在3D空间中的位置和视角。
PerspectiveCamera
相机three.js 里有几种不同的相机,在这里,我们使用的是 PerspectiveCamera(透视摄像机)这个相机类型最接近我们现实生活中观察世界的方式,因为它使用透视投影来渲染场景。透视投影的特点是近大远小,这使得远处的物体看起来比实际小,而近处的物体则显得更大。
当你创建一个 PerspectiveCamera
时,通常需要设置以下几个参数:
视野(Field of View, FOV):这是相机视野的角度,通常以度数表示。较大的 FOV 会使场景看起来更宽广,而较小的 FOV 则会使场景看起来更狭窄。
长宽比(Aspect Ratio):这是相机视口的宽度与高度的比率。通常设置为渲染窗口的宽度除以高度。
近裁剪面(Near Clipping Plane):这是相机能够看到的最近的距离。任何比这个距离更近的物体都不会被渲染。
远裁剪面(Far Clipping Plane):这是相机能够看到的最远的距离。任何比这个距离更远的物体也不会被渲染。
想象一下你正在用眼睛看世界。
PerspectiveCamera
就像你的眼睛,它有一个视野范围(FOV),决定了你能看到多宽或多窄的场景。长宽比(Aspect Ratio)决定了你看到的画面是宽屏还是窄屏。近裁剪面和远裁剪面则决定了你能看到的最近和最远的物体。
在three.js中通常通过下面的方式创建透视相机,通常设置宽高比就用视口可视区域的宽高比。
- // 创建一个Three.js相机,包括透视投影、宽高比、近裁剪面和远裁剪面
- const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
position就是在3D场景中的物体的坐标信息,在Three.js中,设置
camera.position
就是确定你的“观察点”在哪里
可以对具有postion属性的物体通过set方法设置x,y,z坐标信息
camera.position.set(15, 12, 8);
也可以对Three.js的Object3D对象通过position.x和position.y单独设置坐标轴的值
lookAt
方法就像是你在决定你的相机或者你的眼睛要聚焦在哪个点上。当你调整相机对准某个特定对象时,你实际上是在使用lookAt
方法。在现实生活中,即使你站在一个固定的位置,你的头可以转向不同的方向,去看向不同的物体。
lookAt看向的是3D空间中的具体点,相机的位置和lookAt看向的点覆盖的区域就是网页渲染初始时的视野
举例:继续上面的操场例子,即使你站在同一个角落,你可以选择看向操场的中心点,也可以看向操场上的某个运动员或者某个特定的物体。当你看向某个点时,你的注意力就集中在了那个方向上,就像是在Three.js中调用lookAt
方法,让相机镜头指向那个点。
在本文中相机的位置position和lookAt坐标系
- camera.position.set(15, 12, 8);
- camera.lookAt(0, 0, 0);
可以看到,你的眼睛是能够看到x、y、z三个坐标的正向,并且视线正对坐标原点。也就是第一个机器人。
- robot2.position.x = 6;
- robot2.position.z = 6;
示例的第一个机器人位于坐标原点,第二个机器人x轴和z轴分别偏移了6个单位。因为camera的位置是x轴偏移15,y轴偏移12,因此能够看到两个机器人。如果我将第二个机器人的x和y都大于camera呢?比如如下的设置
- robot2.position.x = 20;
- robot2.position.z = 20;
可以看到默认页面加载是看不到第二个机器人的。但是相机位置和看向点不变,我们旋转3D坐标系,可以看到第二个机器人。说明此时机器人“站在了你的后面”,因为你没看向它,但它存在吗?当然存在。
机器人2的位置信息
- robot2.position.x = 20;
- robot2.position.z = 20;
切换相机的视角使其看到机器人2
- camera.position.set(15, 12, 8);
- camera.lookAt(20, 0, 20);
可以看到页面初始时只能看到机器人2,坐标原点在后面,所以看不到原点上的机器人。但是旋转坐标系能够看到。
前面提了很多,旋转坐标系。默认坐标系是不能旋转的,在空间中位置信息确认了就是相对静止的。那我们怎么可以拖动屏幕实现立体观察的效果呢?
在 Three.js 中,如果你希望通过拖动屏幕来旋转场景中的坐标轴,通常需要使用一些交互控制器,例如
OrbitControls
。OrbitControls
是一个控制器,它允许用户通过拖动、滚动和按键来控制相机的位置和方向。
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
可以通过给controls对象添加一个事件监听,当orbitControls改变了,调用renderer重新渲染scene和camera
- // 设置相机控件轨道控制器OrbitControls
- const controls = new OrbitControls(camera, renderer.domElement);
- // 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
- controls.addEventListener('change', function () {
- renderer.render(scene, camera); //执行渲染操作
- });//监听鼠标、键盘事件
但在我们的代码中并没有显示的调用change的监听,而是在设置循环动画的时候通过renderer.render方法重新渲染每一帧
- const update = () => {
- requestAnimationFrame(update);
- camera.lookAt(20, 0, 20);
- robot.rotation.y -= 0.005; //机器人旋转
- robot2.rotation.y -= 0.005;
- // 粒子旋转
- starts.rotation.y -= 0.001;
- starts.rotation.z += 0.001;
- starts.rotation.x += 0.001;
- renderer.render(scene, camera);
- };
- update(); //自动更新
虽然在
update
函数中没有显式调用controls.update()
,但是OrbitControls
在内部已经通过事件监听器在每次交互时更新了相机的位置和方向。当你移动鼠标或触摸屏幕时,这些交互事件会被监听器捕获,OrbitControls
会相应地调整相机,然后renderer.render(scene, camera)
会使用新的相机状态来渲染场景。
简而言之,
OrbitControls
的设计允许它不需要显式的change
事件监听就能工作。它会在用户交互时自动更新相机,然后在你的渲染函数中,通过renderer.render(scene, camera)
来反映这些更新。
如果你的场景中确实需要响应用户交互以外的事件来触发渲染,或者你想要在特定条件下禁用某些控制行为,那么监听
change
事件会很有用。但在大多数情况下,对于基本的交互控制,OrbitControls
已经提供了所需的功能。
OrbitControls本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果
- import { useEffect, useRef } from "react";
- import * as THREE from "three";
- import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
- import { FontLoader } from "three/examples/jsm/loaders/FontLoader";
- import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";
- //机器人脑袋
- function createHead() {
- //SphereGeometry创建球形几何体
- const head = new THREE.SphereGeometry(4, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5);
- const headMaterial = new THREE.MeshStandardMaterial({
- color: 0x43b988,
- roughness: 0.5,
- metalness: 1.0,
- });
- const headMesh = new THREE.Mesh(head, headMaterial);
- return headMesh;
- }
- //触角
- function generateHorn(y: number, z: number, angle: number) {
- //触角 CapsuleGeometry 创建胶囊形状的几何体。胶囊形状可以看作是一个圆柱体两端加上半球体
- const line = new THREE.CapsuleGeometry(0.1, 2);
- const lineMaterial = new THREE.MeshStandardMaterial({
- color: 0x43b988,
- roughness: 0.5,
- metalness: 1.0,
- });
- const lineMesh = new THREE.Mesh(line, lineMaterial);
- lineMesh.position.y = y;
- lineMesh.position.z = z;
- lineMesh.rotation.x = angle;
- return lineMesh;
- }
- //机器人眼睛
- function generateEye(x: number, y: number, z: number) {
- //SphereGeometry创建球形几何体
- const eye = new THREE.SphereGeometry(0.5, 32, 16, 0, Math.PI * 2, 0, Math.PI * 2);
- const eyeMaterial = new THREE.MeshStandardMaterial({
- color: 0x212121,
- roughness: 0.5,
- metalness: 1.0,
- });
- const eyeMesh = new THREE.Mesh(eye, eyeMaterial);
- eyeMesh.position.x = x;
- eyeMesh.position.y = y;
- eyeMesh.position.z = z;
- return eyeMesh;
- }
- //机器人身体
- function generateBody() {
- //CylinderGeometry第一个参数是上部分圆的半径,第二个参数是下部分圆的半径,第三个参数是高度,材质使用的跟腿一样
- const body = new THREE.CylinderGeometry(4, 4, 6);
- const bodyMaterial = new THREE.MeshStandardMaterial({
- color: 0x43b988,
- roughness: 0.5,
- metalness: 1.0,
- });
- const bodyMesh = new THREE.Mesh(body, bodyMaterial);
- return bodyMesh;
- }
- //胳膊、腿
- function generateLegs(y: number, z: number) {
- const leg1 = new THREE.CapsuleGeometry(1, 4);
- const legMaterial1 = new THREE.MeshStandardMaterial({
- color: 0x43b988,
- roughness: 0.5,
- metalness: 1.0,
- });
- const leg1Mesh = new THREE.Mesh(leg1, legMaterial1);
- leg1Mesh.position.y = y;
- leg1Mesh.position.z = z;
- return leg1Mesh;
- }
- //创建机器人
- function generateRobot() {
- // 创建一个Three.js对象,用于存放机器人
- const robot = new THREE.Object3D();
- const headMesh = createHead();
- headMesh.position.y = 6.5;
- robot.add(headMesh);
- //眼睛
- const leftEye = generateEye(3, 8, -2);
- const rightEye = generateEye(3, 8, 2);
- robot.add(leftEye);
- robot.add(rightEye);
- const leftHorn = generateHorn(11, -1, (-Math.PI * 30) / 180);
- const rightHorn = generateHorn(11, 1, (Math.PI * 30) / 180);
- robot.add(leftHorn);
- robot.add(rightHorn);
- const body = generateBody();
- body.position.y = 4;
- robot.add(body);
-
- // 生成机器人左腿
- robot.add(generateLegs(0, -2));
- // 生成机器人右腿
- robot.add(generateLegs(0, 2));
- //胳膊
- robot.add(generateLegs(3, 5));
-
- robot.add(generateLegs(3, -5));
- //物体缩放
- robot.scale.x = 0.3;
- robot.scale.y = 0.3;
- robot.scale.z = 0.3;
- return robot;
- }
- //创建粒子星星
- function generateStarts(num: number) {
- //制作粒子特效
- const starts = new THREE.Object3D();
- const obj = new THREE.SphereGeometry(0.2, 3, 3);
- const material = new THREE.MeshStandardMaterial({
- color: 0x43b988,
- roughness: 0.5,
- metalness: 5,
- });
- const mesh = new THREE.Mesh(obj, material);
- for (let i = 0; i < num; i++) {
- const target = new THREE.Mesh();
- target.copy(mesh);
- target.position.x = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
- target.position.y = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
- target.position.z = Math.floor(Math.random() * 18 + Math.floor(Math.random() * -18));
- starts.add(target);
- }
- return starts;
- }
- //创建文本
- function createText(content: string, font: any) {
- const textGeometry = new TextGeometry(content, {
- font: font,
- size: 1,
- height: 0.1,
- curveSegments: 1,
- });
- textGeometry.center();
- const textMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); // front
- const mesh = new THREE.Mesh(textGeometry, textMaterial);
- return mesh;
- }
- /**
- * 创建一个Three.js场景,包括相机和渲染器
- */
- function Robot() {
- // 创建一个div容器,用于存放渲染的Three.js场景
- const containerRef = useRef<HTMLDivElement>(null);
- const scene = new THREE.Scene();
- // 创建一个Three.js相机,包括透视投影、宽高比、近裁剪面和远裁剪面
- const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
- camera.position.set(15, 12, 8);
- camera.lookAt(0, 0, 0);
- // 创建一个Three.js渲染器,包括抗锯齿
- const renderer = new THREE.WebGLRenderer({ antialias: true });
- renderer.setSize(window.innerWidth, window.innerHeight);
-
- //添加坐标系
- const axisHelper = new THREE.AxesHelper(150);
- scene.add(axisHelper);
- //坐标系添加文字
- const loader = new FontLoader();
- let meshX = new THREE.Mesh();
- let meshY = new THREE.Mesh();
- let meshZ = new THREE.Mesh();
- loader.load("fonts/optimer_regular.typeface.json", function (font) {
- meshX = createText("X", font);
- meshY = createText("Y", font);
- meshZ = createText("Z", font);
- meshX.position.x = 12;
- meshY.position.y = 12;
- meshZ.position.z = 12;
- scene.add(meshX);
- scene.add(meshY);
- scene.add(meshZ);
- });
-
- const robot = generateRobot();
- const robot2 = generateRobot();
- robot2.position.x = 6;
- robot2.position.z = 6;
- // 将机器人身体添加到场景中
- scene.add(robot);
- scene.add(robot2);
- // 创建一个Three.js方向光,包括颜色、强度
- const straightLight = new THREE.DirectionalLight(0xffffff, 5);
- // 设置方向光的位置
- straightLight.position.set(20, 20, 20);
- // 将方向光添加到场景中
- scene.add(straightLight);
-
- const starts = generateStarts(200);
- scene.add(starts);
-
- //轨道控制器
- const controls = new OrbitControls(camera, renderer.domElement);
- controls.update();
-
- const animate = () => {
- requestAnimationFrame(animate);
- robot.rotation.y -= 0.005; //机器人旋转
- robot2.rotation.y -= 0.005;
- // 粒子旋转
- starts.rotation.y -= 0.001;
- starts.rotation.z += 0.001;
- starts.rotation.x += 0.001;
- //
- meshX.lookAt(camera.position);
- meshY.lookAt(camera.position);
- meshZ.lookAt(camera.position);
- renderer.render(scene, camera);
- };
- animate(); //添加动画
-
- // 监听组件挂载和卸载
- useEffect(() => {
- // 如果div存在,将渲染器dom元素添加到div中
- if (containerRef.current) {
- containerRef.current.appendChild(renderer.domElement);
- // 渲染场景
- renderer.render(scene, camera);
- }
- }, [containerRef]);
-
- // 返回div容器,用于存放渲染的Three.js场景
- return <div ref={containerRef} style={{ width: "100vw", height: "100vh" }}></div>;
- }
-
- // 导出Robot组件
- export default Robot;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。