three.js 项目搭建(一)——实现一个3D大西瓜
用到的库:
创建项目
此处通过React脚手架快速创建一个项目(本篇文章并非主要介绍React,故此处快速开始,不做过多描述,不了解的小伙伴可以查看React官方文档)
npx create-react-app tree-test
开始搭建
创建场景
相机、几何体等都需要添加至场景中
```js import * as THREE from 'three';
// 创建场景 const scene = new THREE.Scene(); ```
创建相机
js
// PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
// fov — 摄像机视锥体垂直视野角度
// aspect — 摄像机视锥体长宽比
// near — 摄像机视锥体近端面
// far — 摄像机视锥体远端面
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 10);
// 添加至场景中
scene.add(camera);
添加坐标轴辅助器
js
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
添加物体
使用three.js提供的几何体
js
// 添加物体
// BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
// width — X轴上面的宽度,默认值为1。
// height — Y轴上面的高度,默认值为1。
// depth — Z轴上面的深度,默认值为1。
// widthSegments — (可选)宽度的分段数,默认值是1。
// heightSegments — (可选)高度的分段数,默认值是1。
// depthSegments — (可选)深度的分段数,默认值是1。
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 基础网格材质
const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
// 根据几何体和材质创建物体
const cube = new THREE.Mesh(geometry, material);
// 添加至场景中
scene.add(cube);
在图形学中,任何物体都可以由若干个三角形拼接形成,所以我们也可以通过自己绘制三角形拼接形成图形
js
const geometry = new THREE.BufferGeometry();
// 拼成一个矩形至少需要两个三角形,6个点,每个点三个坐标,共18个坐标
const vertices = new Float32Array([
-1.1, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
1.0, -1.1, -1.0, 1.0,
]);
// BufferAttribute
// .array : TypedArray
// 在 array 中保存着缓存中的数据。
// .count : Integer
// 保存 array 除以 itemSize 之后的大小。
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f });
material.wireframe = true;
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
效果如图:
若material.wireframe = false;
后,图形的前后是有差别的:
从z轴正方向查看:
从z轴负方向查看:
更进一步,我们生成随机半透明的几何图形
js
// 添加物体
for (let i = 0; i < 50; i++) {
// 每个三角形需要三个顶点,每个顶点需要三个值
const geometry = new THREE.BufferGeometry();
// 注意:此处new Float32Array时一定要传入参数,否则图形不会显示
const positionArray = new Float32Array(9);
for (let j = 0; j < 9; j++) {
positionArray[j] = Math.random() * 10 - 5;
}
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positionArray, 3)
);
let color = new THREE.Color(Math.random(), Math.random(), Math.random());
// 注意:此处一定要设置transparent=true后再设置opacity
const material = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 0.5,
});
const mesh = new THREE.Mesh(geometry, material);
// material.wireframe = true;
scene.add(mesh);
}
效果:
初始化渲染器及轨道
```js // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 设置渲染的尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 创建轨道控制器 // OrbitControls( object : Camera, domElement : HTMLDOMElement ) // object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
// domElement: 用于事件监听的HTML元素。 const controls = new OrbitControls(camera, renderer.domElement); // 为控制器设置阻尼,让控制器有真实的效果 controls.enableDamping = true; ```
渲染
本demo采用React Hook,📢注意renderer.domElement
并非React认识的元素,所以需要采用JS原生方式添加子元素(或许是我不知道,很高兴有大佬评论告知其他添加到页面上的写法)
js
useEffect(() => {
const app = document.getElementById('App');
app?.appendChild(renderer.domElement);
// 使用渲染器,通过相机将场景渲染进来
render();
});
const render = () => {
controls.update();
renderer.render(scene, camera);
// 此处采用动画是为了响应轨道滑行变换
requestAnimationFrame(render);
};
动画
此处简单做了个动画示例,更复杂的动画可以查看gsap
js
// 设置动画
gsap.to(cube.position, { x: 5, duration: 5 });
GUI工具的使用
```js import * as dat from 'dat.gui';
// 实例化可视化GUI工具 可以通过按 H 键隐藏GUI面板 const gui = new dat.GUI(); //可传递参数{ closed:true ,width:400 } // gui.hide() //隐藏GUI面板,可通过按两次 H键开启显示
// 往GUI面板添加要显示的对象的参数 // 参数一:对象;参数二:要调整的对象属性;参数三:最小值;参数四:最大值;参数五:调整精度 // 添加配置 cube y 轴坐标 gui.add(cube.position, 'y', -3, 3, 0.01); // 添加配置 cube 是否可见 gui.add(cube, 'visible');
const params = { color: '#ffff00', fn: () => { gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 }); }, }; // 添加一个色盘,颜色改变时改变cube的颜色 gui.addColor(params, 'color').onChange(value => { cube.material.color.set(value); }); // 添加一个名称为”立方体运动“的设置项,控制是否执行params.fn函数 gui.add(params, 'fn').name('立方体运动'); // 创建一个文件夹,用于存放这些设置 const folder = gui.addFolder('设置立方体'); // 设置 cube 是否显示线框 folder.add(cube.material, 'wireframe'); ``` 上述配置之后效果如下图👇🏻
完整代码
```js //app.js
import React, { useEffect } from 'react'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import gsap from 'gsap'; import * as dat from 'dat.gui'; // 实例化可视化GUI工具 可以通过按 H 键隐藏GUI面板 const gui = new dat.GUI(); //可传递参数{ closed:true ,width:400 } // gui.hide() //隐藏GUI面板,可通过按两次 H键开启显示
function App() { // 创建场景 const scene = new THREE.Scene();
// PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number ) // fov — 摄像机视锥体垂直视野角度 // aspect — 摄像机视锥体长宽比 // near — 摄像机视锥体近端面 // far — 摄像机视锥体远端面 const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); // 设置相机位置 camera.position.set(0, 0, 10); scene.add(camera);
// 添加物体 // BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer) // width — X轴上面的宽度,默认值为1。 // height — Y轴上面的高度,默认值为1。 // depth — Z轴上面的深度,默认值为1。 // widthSegments — (可选)宽度的分段数,默认值是1。 // heightSegments — (可选)高度的分段数,默认值是1。 // depthSegments — (可选)深度的分段数,默认值是1。 const geometry = new THREE.BoxGeometry(1, 1, 1); // 基础网格材质 const material = new THREE.MeshBasicMaterial({ color: 0x43ad7f1f }); // 根据几何体和材质创建物体 const cube = new THREE.Mesh(geometry, material); scene.add(cube);
//#region GUI 面板 // 往GUI面板添加要显示的对象的参数 // 参数一:对象;参数二:要调整的对象属性;参数三:最小值;参数四:最大值;参数五:调整精度 gui.add(cube.position, 'y', -3, 3, 0.01); gui.add(cube, 'visible'); const params = { color: '#ffff00', fn: () => { gsap.to(cube.position, { z: 5, duration: 2, yoyo: true, repeat: -1 }); }, }; gui.addColor(params, 'color').onChange(value => { cube.material.color.set(value); }); gui.add(params, 'fn').name('立方体运动'); const folder = gui.addFolder('设置立方体'); folder.add(cube.material, 'wireframe'); //#endregion
// 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 设置渲染的尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 创建轨道控制器 // OrbitControls( object : Camera, domElement : HTMLDOMElement ) // object: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。 // domElement: 用于事件监听的HTML元素。 const controls = new OrbitControls(camera, renderer.domElement); // 为控制器设置阻尼,让控制器有真实的效果 controls.enableDamping = true;
// 添加坐标轴辅助器 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper);
// 将webgl渲染的canvas内容添加到页面上 useEffect(() => { const app = document.getElementById('App'); app?.appendChild(renderer.domElement); // 设置动画 gsap.to(cube.position, { x: 5, duration: 5 }); // 使用渲染器,通过相机将场景渲染进来 render(); });
window.addEventListener('resize', () => { // 更新摄像头 camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机的投影矩阵 camera.updateProjectionMatrix(); // 更新渲染器 renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的像素比 renderer.setPixelRatio(window.devicePixelRatio); }); window.addEventListener('dblclick', () => { const fullScreenElement = document.fullscreenElement; if (fullScreenElement) { document.exitFullscreen(); } else { renderer.domElement.requestFullscreen(); } });
const render = () => { controls.update(); renderer.render(scene, camera); requestAnimationFrame(render); };
return
; }export default App; ```
效果
进一步加工
导入纹理
为了让看到文章的小伙伴也获得贴图,我就直接放掘金上传图片的url作为示例了 ```js // 导入纹理 new THREE.TextureLoader().load( 'http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/172a9f6fc326477f9a328e64127bc8ed~tplv-k3u1fbpfcp-watermark.image?', function (texture) { // in this example we create the material when the texture is loaded const material = new THREE.MeshStandardMaterial({ map: texture, // 粗糙程度 roughness: 0.5, }); // 添加物体 const cubeGeometry = new THREE.SphereGeometry(2, 32, 16); const cube = new THREE.Mesh(cubeGeometry, material); scene.add(cube); },
// 目前暂不支持onProgress的回调
undefined,
// onError回调
function (err) {
console.error('An error happened.', err);
}
); ``` 另外,一些可以获取贴图的网站: - http://www.poliigon.com/textures - http://www.arroway-textures.ch/textures - http://quixel.com/bridge
灯光
js
// 灯光
// 环境光
const light = new THREE.AmbientLight(0x404040);
scene.add(light);
// 直线光
const lineLight = new THREE.DirectionalLight(0xffffff);
lineLight.position.set(10, 10, 10);
scene.add(lineLight);
效果
题外话
村上春树说,世界上存在着不能流泪的悲伤,这种悲伤无法向人解释,它永远一成不变,如无风夜晚的雪花,静静地沉积在心里,当你虽不认同却只能无奈的屈服,当你不满与现状却又看不清出路,当你充满忧虑却又无力改变,当焦虑变成了每一个异乡人的家常便饭,我们又该如何去完成这一堂人生的必修课。