初见threejs
threejs底层封装了强大的webGL技术,让开发者们可以开箱即用 (其实也并非开箱即用,还是挺麻烦的🙂)。
恰巧朋友遇到了些难题,借此契机,接触了下threejs。
官网是支持中文的,虽然翻译的很差,但寥胜于无。
这里还有个野生的中文文档,感兴趣的可以看看,毕竟也是某位开发者的一腔热血之作!
treejs紧紧围绕了渲染器(Renderer)、场景(Scene)、照相机(Camera),坐标系,物体,光照这些概念,熟悉3d建模或影视特效的人应该非常的熟悉!
下边我就从一个简单的例子,一点点深入。
我们创建一个场景,你可以理解为秀场。
然后再创建一个模型,你可以理解为模特。
最后让模型在场景中展示!
安装
如果你要是使用node或工程化前端项目,直接npm或yarn安装即可
yarn add three
如果你是古老的原始纯html项目,那就cdn引入即可
将<version>
替换为 Three.js 的实际版本,例如"0.160.0"。最新版本可以在npm 版本列表中找到。
<script src="https://unpkg.com/three@<version>/build/three.module.js"></script>
<!-- 辅助包,非必须,用到再去找即可 -->
<script src="https://unpkg.com/three@<version>/examples/jsm/某个辅助工具.js"></script>
最简单的例子
创建场景
const scene = new THREE.Scene();
创建模型
在threejs中规定,创建模型分3部,具体如下
// 创建物体几何形状
const geometry = new THREE.BoxGeometry(0.4, 0.6, 0.8); // 参数为长宽高
// (基于上一步的几何形状)创建物体外观材质
const material = new THREE.MeshBasicMaterial();
// (基于上一步的外观材质)创建物体网格模型
const mesh = new THREE.Mesh(geometry, material);
创建摄像机
这是什么东西?!
其实也好理解,你自己本身就充当摄像机去看 秀场里的模特。
threejs一切围绕3d,那想了解3d,空间感是必须要有的。
而摄像机就是空间感的表达工具!
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(1.2, 0, 6); // 设置相机位置
一切就绪,开始展示
// 创建场景 并将上边的内容都添加到场景中
const scene = new THREE.Scene();
scene.add(mesh);
scene.add(camera);
// 创建一个渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
// 执行渲染命令并挂载到页面(会输出一个canvas画布,也就是一个HTML元素,你可以插入到web页面中)
const webglNode = document.querySelector(".webgl");
webglNode.appendChild(renderer.domElement);
优化
可以看到白花花的一片,很难看出立体的感觉,我们来进行下优化
添加三维坐标系
这样可以加深我们对空间的感知度,就像上高数那样🙂
红、绿、蓝分别对应坐标系的x、y、z轴,对于three.js的3D坐标系默认y轴朝上
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
换物体外观材质并打灯
可以看到加上三维坐标系之后,确实能提醒我们“这是一个三维物体”。但是直观来说,我看到的还是一坨白!
好在threejs支持多种物品材质,其中就有可以反光的材质,然后我们再打一个灯光,这效果不就上来了?!
// 修改材质为反光材质
const material = new THREE.MeshPhongMaterial();
// 创建灯光
let point = new THREE.PointLight(0xffffff, 1); //创建一个光源: 光的颜色、光的强度
point.position.set(0.6, 0.4, 1.6); //设置光源的位置
const lightHelp = new THREE.PointLightHelper(point, 0.06); // 光源辅助线,便于确认光源位置,后期可以去除
scene.add(point);
scene.add(lightHelp);
注意灯光右很多种,选择合适的即可
const ambientLight = new THREE.AmbientLight(0xab193d, 0.1) // 创建环境光
const dirLight = new THREE.DirectionalLight(0xffffff); // 创建平行光
调整镜头方向
不知道大家有没有发现,入股摄像头移动,但是镜头不做调整的话,物体很快就消失在场景画面中了。
其实这也很好理解,就像电视剧一样,无论涉摄像头怎么移动,演员始终聚集在画面中。
这就是镜头的功劳!
camera.lookAt(0, 0, 0); // 设置镜头方向为坐标原点 其实就是聚焦物体本身上
然后我们调整一下摄像头位置,向上(y轴)移动点,以便看到顶部。在优化下光源位置,看看是不是完美很多
point.position.set(0.6, 1, 1.6); //设置光源的位置
camera.position.set(1.2, 2, 6); // 设置相机位置
动画
这样呆呆的,就像一个静态图一样。
试想一下,如果早知道咱们做出来是静态的,那我直接用ps画出来多好?!
所以,动肯定是要动的。
// 动画封装(这里是旋转90度后,将其旋转回原始位置)
const reRender = ({
scene,
camera,
mesh,
renderer,
geometry,
rotation = THREE.MathUtils.degToRad(90),
}) => {
let halfwayRotated = true;
const handel = () => {
if (!halfwayRotated && mesh.rotation.y < rotation) {
mesh.rotation.y += 0.01;
} else {
halfwayRotated = true;
if (mesh.rotation.y > 0) {
mesh.rotation.y -= 0.01;
} else {
halfwayRotated = false;
}
}
renderer.render(scene, camera);
requestAnimationFrame(handel);
};
handel();
};
reRender({ scene, camera, mesh, geometry, renderer });
贴图
threejs当然是支持贴图的。
物体本身贴图
从一开始,我就想画一个机箱。
所以我的物体一开始我就设置成了机箱的形状。
核心点:materials.map=loader.load(tietu),
// 创建纹理到
const loader = new THREE.TextureLoader(); // 纹理贴图加载器
const titus = ["../img/zuo.png", "../img/you.png","../img/shang.png","../img/xia.png","../img/qian.png","../img/hou.png"];
// 创建外观材质
const materials = titus.map(tietu=>(new THREE.MeshStandardMaterial({
map: loader.load(tietu), // 引入纹理
metalness: 1, //金属光泽:金属度
roughness: 0, //金属光泽:粗糙度
})));
let point = new THREE.PointLight(0xffffff, 100000); // 调整光源亮度
为配合贴图,我更换了更加合适的材质,并且调整了灯光的亮度
这里是机箱图片素材
截至目前页面全部代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="webgl"></div>
<script type="module">
import * as THREE from "./lib/three.module.js";
// 场景宽高
const width = 1600; //宽度
const height = 900; //高度
// 创建物体几何形状
const geometry = new THREE.BoxGeometry(0.4, 0.6, 0.8); // 参数为几何物体的长宽高坐标
// 创建纹理到
const loader = new THREE.TextureLoader(); // 纹理贴图加载器
const titus = [
"../img/zuo.png",
"../img/you.png",
"../img/shang.png",
"../img/xia.png",
"../img/qian.png",
"../img/hou.png",
];
// 创建外观材质
const materials = titus.map(
(tietu) =>
new THREE.MeshStandardMaterial({
map: loader.load(tietu), // 引入纹理
metalness: 1, //金属光泽:金属度
roughness: 0, //金属光泽:粗糙度
})
);
// (基于上一步的外观材质)创建物体网格模型
const mesh = new THREE.Mesh(geometry, materials);
// 辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(150);
// 创建摄像机
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
camera.position.set(1.2, 2, 6); // 设置相机位置
camera.lookAt(0, 0, 0); // 设置镜头方向
// 创建灯光
let point = new THREE.PointLight(0xffffff, 100000); //创建一个光源: 光的颜色、光的强度
point.position.set(0.6, 1, 1.6); //设置光源的位置
const lightHelp = new THREE.PointLightHelper(point, 0.06); // 光源辅助线,便于确认光源位置,后期可以去除
// 创建场景 并向场景中添加以上内容
const scene = new THREE.Scene();
scene.add(mesh);
scene.add(camera);
scene.add(axesHelper);
scene.add(point);
scene.add(lightHelp);
// 创建一个渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
// 执行渲染命令并挂载到页面(会输出一个canvas画布,也就是一个HTML元素,你可以插入到web页面中)
const webglNode = document.querySelector(".webgl");
webglNode.appendChild(renderer.domElement);
// 动画封装(旋转90度后,将其旋转回原始位置)
const reRender = ({
scene,
camera,
mesh,
renderer,
geometry,
rotation = THREE.MathUtils.degToRad(360),
}) => {
let halfwayRotated = true;
const handel = () => {
if (!halfwayRotated && mesh.rotation.y < rotation) {
mesh.rotation.y += 0.01;
} else {
halfwayRotated = true;
if (mesh.rotation.y > 0) {
mesh.rotation.y -= 0.01;
} else {
halfwayRotated = false;
}
}
renderer.render(scene, camera);
requestAnimationFrame(handel);
};
handel();
};
reRender({ scene, camera, mesh, geometry, renderer });
</script>
</body>
</html>
纹理贴图是异步
对的,纹理贴图的load方法是异步。
我们上边的初始化的时候贴图其实是空的,并没有加载完,因为后期动画的时候又重新reRender了,所以你看起来并无异常。
不然你可以试试把动画关闭,问题即可复现出来:物体会显示为一坨黑色。
怎么优化呢?其实也简单,异步即可。
// 封装的根据图片地址 返回加载后的贴图对象texture
const getTextures = async (imgUrls) => {
const texturesPromises = imgUrls.map(
(img) =>
new Promise((resolve) =>
loader.load(img, (texture) => resolve(texture))
)
);
const textures = await Promise.all(texturesPromises);
return textures;
};
const textures = await getTextures([
"../img/zuo.png",
"../img/you.png",
"../img/shang.png",
"../img/xia.png",
"../img/qian.png",
"../img/hou.png",
]);
// 创建外观材质
const materials = textures.map((texture) => {
return new THREE.MeshStandardMaterial({
map: texture, // 引入纹理
metalness: 1, //金属光泽:金属度
roughness: 0, //金属光泽:粗糙度
});
});
场景背景贴图
核心点:scene.background = texture;
可以看到如此,如此一来场景(物体四周)便有了贴图画面,有了vr的感觉。
只不过因为没有打灯,物体看起里黢黑,这不重要!
helper.js
import * as THREE from "three";
// 定义一些常量,后边可以简写,也尽可能避免魔法数字
// export const width = window.innerWidth;
// export const height = window.innerHeight;
export const width = 1600; //宽度
export const height = 900; //高度
export const ar = width / height; // Aspect Ratio宽高比
// 创建Cube纹理
export const createCubeTexture = () => {
const cubeTextureLoader = new THREE.CubeTextureLoader(); // cube纹理加载器
return new Promise((resolve) => {
cubeTextureLoader // 加载纹理
.setPath("./img/grassland/")
.load(
["px.png", "nx.png", "py.png", "ny.png", "pz.png", "nz.png"],
(res) => {
resolve(res);
}
);
});
};
// 创建盒子类型的物体(参数为纹理贴图,可以不传)
export const createBoxGeometryMesh = (params) => {
const geometry = new THREE.BoxGeometry(30, 30, 30); // 创建物体几何形状
const materials = new THREE.MeshStandardMaterial({ // 创建外观材质
metalness: 0.8, // 金属度: 默认是0.5,范围是0到1之间。木材或石材用0,金属使用1。
roughness: 0.06 // 表面粗糙程度: 默认0.5,范围是0~1之间,0表示平滑的镜面反射,1表示完全漫反射
});
if (params) {
const { map, envMap } = params;
map && (materials.map = map);
envMap && (materials.envMap = envMap);
}
const mesh = new THREE.Mesh(geometry, materials); // 创建物体网格模型
return mesh;
};
// 渲染页面
export const renderHtml = (scene, camera) => {
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); // 设置渲染的尺寸大小
renderer.render(scene, camera);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
return renderer;
};
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="webgl"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
"helper": "./helper.js"
}
}
</script>
<script type="module" async>
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { ar,createCubeTexture,renderHtml,createBoxGeometryMesh} from "helper";
// 创建纹理(后边用)
const texture = await createCubeTexture();
// 创建场景
const scene = new THREE.Scene();
scene.background = texture; // 给场景添加背景
// 添加坐标轴辅助器,添加到场景中
const axesHelper = new THREE.AxesHelper(70);
scene.add(axesHelper);
// 创建一个物体,添加到场景中
const mesh = createBoxGeometryMesh();
scene.add(mesh);
// 创建相机
const camera = new THREE.PerspectiveCamera(75, ar, 0.1, 1000);
camera.position.set(0, 0, 100);
// 渲染到页面
const renderer = renderHtml(scene, camera);
// 设置相机控件轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener("change", () => {
renderer.render(scene, camera);
});
</script>
</body>
</html>
物体反射环境贴图
比如我的物体是一个不锈钢,或者玻璃,那是不是要物体四周要反映出场景的画面 会更真实一些?
关键点:materials.envMap=texture
还是使用上个例子,我把题图传入进去即可(理论上,物体反射贴图和场景题图保持一致,毕竟反射的就是场景画面)
// 创建一个物体,添加到场景中
const mesh = createBoxGeometryMesh({envMap: texture});
scene.add(mesh);
操作鼠标控制物体
想不想拖拽鼠标等操作,即可控制物体或摄像机呢?
当然是可以的!
相机控件轨道控制器 OrbitControls
import { OrbitControls } from "./lib/OrbitControls.js";
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
renderer.render(scene, camera); //执行渲染操作
});//监听鼠标、键盘事件
其它API
几何相关
旋转
geometry.rotateY(0.01);
模型相关
注意:
几何的位置相关属性更新它会更新所有顶点位置的值,而模型则内在的顶点位置保持不变,你只是作为一个整体旋转网格。
这是两者的区别,一般情况下使用模型属性来操控位置即可!
位置
mesh.position.[x/y/z]
比如,我设置y轴向上移动,然后在设置x轴往右移动,最后再设置z轴距离我更近些
旋转
比如,下边代码的作用是,将物体以y轴为中心旋转
mesh.rotation.y += 0.01;
摄像机
机位
camera.position.[x/y/z]
来控制摄像机位置。
下边代码演示摄像头先向左移动,再向上移动,最后再推近
const reRender = () => {
const handel = () => {
const cpx = Number(camera.position.x);
const cpy = Number(camera.position.y);
const cpz = Number(camera.position.z);
if(cpx<1.6){
camera.position.x = cpx+0.01
}else{
if(cpy<2.3){
camera.position.y = cpy+0.01
}else{
if(cpz>5){
camera.position.z = cpz-0.01
}
}
}
renderer.render(scene, camera);
requestAnimationFrame(handel);
};
handel();
};
reRender();
镜头
摄像头一半都和镜头搭配使用
let x = 0;
let y = 0;
let z = 0;
const reRender = () => {
const handel = () => {
if(x<1){
x+=0.01
}else{
if(y<1){
y+=0.01
}else{
if(z<1){
z+=0.01
}
}
}
camera.lookAt(x,y,z)
renderer.render(scene, camera);
requestAnimationFrame(handel);
};
handel();
};
reRender();