# Threejs **Repository Path**: yin_baoer/threejs ## Basic Information - **Project Name**: Threejs - **Description**: Threejs学习案例以及笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2024-08-02 - **Last Updated**: 2025-09-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Three.js学习 ### 1-1 使用Parcel搭建three.js开发环境 #### parcel:极速零配置Web应用打包工具 - 安装:`npm install parcel-bundler --save-dev` - 配置运行命令: ``` {    "script": {        ## your entry file:一般是src/index.html        "dev": "parcel ",        "build": "parcel build "   } } ``` - 运行:`npm run dev` - 安装three.js:`npm i three --save` - 文件结构 ![1720410800833.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/025936e97ed045fb9bb2979302ebee65~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=BVIF7k2hgHejhkBraNl0U5CJ6Ng%3D) - index.html ```        Document     ``` - css/style.css ``` * {  padding: 0;  margin: 0; } ​ body {  background: skyblue; } ``` - main/main.js ``` import * as THREE from "three"; ​ console.log(THREE); ``` ![1720411361615.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f7f2f8c10ac34973b6f5a3aaf946e511~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=rhB8gcI6g72EJjIPpJUeqzftUHM%3D) ### 1-2 使用three.js渲染第一个场景和物体 - 使用:相机+场景+渲染器 - 目标:了解three.js的基础内容 - 透视相机:`PerspectiveCamera(fov: Number, aspect: Number, near: Number, far: Number)` - fov — 摄像机视椎体垂直视野角度 - aspect — 摄像机视椎体长宽比 - near — 摄像机视椎体进端面 - far — 摄像机视椎体远端面 这些参数一起定义了摄像机的viewing frustum(视椎体) - 完整渲染场景的案例 ``` import * as THREE from "three"; ​ // 1 创建场景 const scene = new THREE.Scene(); ​ // 2 创建相机 const camera = new THREE.PerspectiveCamera(  75,  window.innerWidth / window.innerHeight,  0.1,  1000 ); ​ // 3 设置相机位置 camera.position.set(0, 0, 10); ​ // 4 将相机添加到场景当中 scene.add(camera); ​ // 5 添加物体 // 创建几何体对象(BoxGeometry: 立方体) const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); ​ // 设置几何体的材质(MeshBasicMaterial: 基础网格材质) const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); ​ // 根据几何体和材质创建物体(Mesh: 网格) const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); ​ // 添加几何体至场景中 scene.add(cube); ​ // 6 渲染 // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 设置渲染器的尺寸和大小 renderer.setSize(window.innerWidth, window.innerHeight); ​ // 7 将webgl渲染的canvas内容添加到body当中 document.body.appendChild(renderer.domElement); ​ // 8 使用渲染器通过相机将场景渲染 renderer.render(scene, camera); ``` - 结果 ![1720438642582.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/bd9642e5d9ec4d79ba6e7f475df22bd6~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=HBPigy7%2FctEdYJqw8Cstpvxq5PI%3D) ### 1-3 如何处理运行搭建Three环境出现的问题 ![1720438740566.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/513bec228221464e8e7d8984d9121659~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ZiE2wWRkVyQdpnSRjSiT74Ue%2Fl8%3D) 由于版本更新造成的。 1-将 `.cache` `.parcel-caceh` `node_modules` `yarn.lock` `package-json.lock` 等都删除 2-查看parcel版本 ``` yarn add --dev -arcel ``` ### 1-4 结合vue开发three.js ``` 1 vue create threeapp 2 npm i three / yarn add three 3 启动项目 ``` `APP.vue文件` ``` ``` - 运行项目即可 ### 2-1 轨道控制器(OrbitControls)查看物体 上次实现的是一个平面的,现在开始进行立体的学习。可以使用轨道控制器。 - 轨道控制器:使得 `相机` 围绕 `目标` 进行轨道运动 - 目标:使用控制器查看3D物体 - 代码实现-鼠标可以操作物体 ``` // 1 导入轨道控制器 import { OrbitControls } from "three/examples/jsm/controls/orbitcontrols"; ​ // 2 创建轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); ​ // 3 设置渲染函数 function render() {  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ​ // 4 初始调用渲染函数 render(); ``` ![1720440106383.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d8ec33de0d9544e5a0420edaa14e3dad~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=RQp8MkwXM40sZ%2BPCLa6sZbulyo0%3D) ![1720440174221.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/8ac94e87bc3748fa827f278841a6082e~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=NDnxZVkBt%2FctqsM0UYkK1gWb4Lk%3D) ### 2-2 添加坐标轴辅助器 - AxesHelper - 用于简单模拟3个坐标轴的对象 - 红色代表X轴,绿色代表Y轴,蓝色代表Z轴 - 示例 ``` const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); ``` - 构造函数 - AxesHelper(size: Number) - size -- (可选的)表示坐标轴的线段长度,默认为1 ![1720440498990.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/85c4fcaf96ab4c45898098cfe388ecf4~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=lDtM6q7dUftK4nSZ8SQ5WEEH8UE%3D) ### 2-3 设置物体移动 - 目标:控制3D物体移动 ``` console.log(cube); ``` ![1720440773291.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c5e909c03b304058b3cefd1c0a2068f9~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=q5ZGaVinmU1A86fJtQ0Ct9ALq98%3D) > 可以通过position修改物体的位置 - position:Vector3(三维向量对象)。默认值(0, 0, 0) - set(x: Float, y: Float, z: Float): this 设置该向量的x、y和z分量 - 代码示例 ``` // 将cube的位置修改到(5, 0, 0) // 1 使用set cube.position.set(5, 0, 0); ​ // 2 直接修改position的x值 cube.position.x = 5; ``` - 利用render函数使物体自动以每一步0.01的距离移动,当超过最大值5的时候,将其重置为0 ``` // 渲染函数 function render() {  cube.position.x += 0.01;  if (cube.position.x > 5) {    cube.position.x = 0; }  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` ### 2-4 物体的缩放与旋转 ##### 设置物体的缩放 ![1720441698198.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5820b8036db04e57a9eed8374ba65310~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=WCJxa46LLY0EGypEX4xjpVk83rg%3D) - scale:Vector3 ``` // 缩放 cube.scale.set(3, 2, 1); ​ // 或者 cube.scale.x = 3; ``` ##### 设置物体的旋转 ![1720441966672.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/742a7cd971d04deebcce5906aa9d210e~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=AcnYWNRl%2BmPX8dqVDnpeS5nwd8E%3D) - Rotation:Euler(欧拉角对象) - Euler(x: Float, y: Float, z: Float, order: String) - 物体的局部旋转,以弧度来表示 - 属性(均为可选): - order:String - 设置旋转顺序:XYZ(默认)、ZXY、YXZ等等,必须是大写字母 - x:Float - y:Float - z:Float - 通过set方式直接修改: - set(x: Float, y: Float, z: Float, order: String) ``` // 旋转 cube.rotation.set(Math.PI / 4, 0, 0, "XYZ"); ​ // 或者 cube.rotation.x = Math.PI / 4; ​ // 也可以在render函数中形成动画 // 渲染函数 // 从坐标轴看旋转是逆时针,从原点看是顺时针 function render() {  cube.position.x += 0.01;  cube.rotation.x += 0.01;  if (cube.position.x > 5) {    cube.position.x = 0; }  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` ![微信截图_20240709120357.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c865f060966d44c39da6b4c8e16b2402~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=IelLBquhzWyNflR2QatU1BzfFP8%3D) ### 2-5 应用requestAnimationFrame正确处理动画运动 > 优化性能,渲染速度以及时间间隔是否一致? ``` // 调用render函数会自动传一个time,打印一下time function render(time) {  console.log(time);  cube.position.x += 0.01;  if (cube.position.x > 5) {    cube.position.x = 0; }  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` - 打印结果 ![1720498336640.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9e006568dab8455191c4617edd2009b7~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=l5D543Ijswxg89dN8JLhikcTOjw%3D) > 上面打印的是每一帧渲染的毫秒数,每一帧之间间隔的时间不一致,这样就渲染的时快时慢 > > 可以根据时间去走:这里我的理解是,过了多少时间,按时间的数量走距离 ``` function render(time) {  // 时间一直在走,大于最大距离5的时候,对时间取余,距离也就从0开始  let t = (time / 1000) % 5;  cube.position.x = t * 1;    renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` ### 2-6 通过Clock跟踪时间处理动画 - Clock对象:用于跟踪时间 - Clock(autoStart: Boolean) autoStart——(可选)是否要自动开启时钟。默认true - 属性 - autoStart: Boolean true:在第一次update是开启时钟,默认true - startTime: Float 存储时钟最后一次调用start方法的时间 - oldTime: Float 存储时钟最后一次调用start, getElapsedTime或getDelta方法的时间 - elapsedTime: Float 保存时钟运行的总时长 - running: Boolean 判断时钟是否在运行 - 方法 - start(): null 启动时钟。同时将startTime和oldTime设置为当前时间。设置elapsedTime为0,且设置running为true - stop(): null 停止时钟。同时将oldTime设置为当前时间 - getElapsedTime(): Float 获取自时钟启动后的秒数,同时将oldTime设置为当前时间 如果autoStart设置为true且时钟未运行,则该方法同时启动时钟 - getDelta(): Float 获取自oldTime设置后到当前的秒数。同时将oldTime设置为当前时间 如果autoStart设置为true且时钟未运行,则该方法同时启动时钟 - 获取一下时间(打印的都是秒数) ``` const clock = new THREE.Clock(); ​ // 渲染函数 function render() {  // 获取时钟运行的总时长  // let time = clock.getElapsedTime();  // 获取时间间隔  let delta = clock.getDelta();  // console.log(time);  console.log('获取时间间隔:', delta); ​  // let t = (time / 1000) % 5;  // cube.position.x = t * 1; ​  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` ![1720501904485.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ad0cb738ecbe4fab8bb5276f1115deb5~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=vUwjY%2BAZ9oLTVgjI9eg0hN9WbRI%3D) - 实现上一个案例实现的效果 ``` const clock = new THREE.Clock(); ​ // 渲染函数 function render() {  // 获取时钟运行的总时长(秒)  let time = clock.getElapsedTime();  let t = time % 5;  cube.position.x = t * 1; ​  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` ### 2-7 Gsap动画库基本使用与原理(Gsap-补间动画) - 目标:掌握各种补间动画效果 - 安装 ``` npm install gsap ``` - 使用动画库 ``` // 1 导入gsap import gsap from 'gsap'; ​ // 2 设置动画 // cube 由0移动到5,持续5秒 gsap.to(    cube.position, // 动画目标   {        x: 5, // 动画目标的属性        duration: 5, // 动画持续时长        ease: "power1.inOut" // 动画运行速率 慢-快-慢   } ); // cube 旋转360度,持续5秒 gsap.to(    cube.rotation,   {        x: 2 * Math.PI,        duration: 5,        ease: "power1.inOut"   } ); ``` ### 2-8 Gsap控制动画属性与方法 - Gsap控制动画的属性 - duration:动画持续时间 - ease:动画运行速率 - repeat:动画重复次数(-1为无数次) - yoyo:往返运动 - delay:延迟时长 - 方法 - onStart:动画开始时执行 - onComplete:动画结束时执行 - 代码实现 ``` // 设置cube的运动 gsap.to(cube.position, {  x: 5,  duration: 5,  ease: "power1.inOut",  repeat: -1,  yoyo: true,  delay: 2, ​  onStart: () => {    console.log("动画开始"); },  onComplete: () => {    console.log("动画完成"); }, }); ``` - 实现鼠标双击暂停和恢复动画 ``` var animation1 = gsap.to(……); ​ window.addEventListener("dblclick", () => {  // 如果动画正在运行,则停止 isActive() 获取动画当前状态  if (animation1.isActive()) {    animation1.pause(); // pause()方法可以停止动画 } else {    animation1.resume(); // resume()方法可以恢复动画 } }); ``` ### 2-9 根据尺寸变化实现自适应画面 - 设置控制器的阻尼效果 - enableDamping: Boolean 将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值false。 请注意,如果该值被启用,将必须在动画循环中调用.update()。 ``` // 1 设置控制器属性 // (之前写的)9 创建轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); // 设置控制器阻尼,使控制器更有真实的效果 controls.enableDamping = true; ​ // 2 在动画循环中调用 // 渲染函数 function render() {  controls.update(); // 调用控制器的更新  renderer.render(scene, camera);  // 渲染下一帧的时候就会调用render函数  requestAnimationFrame(render); } ``` - 实现屏幕尺寸变化自适应 - 不是自适应屏幕大小变化的时候 ![1720581697450.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/95da2be6754545b4a5b0813ef10f2c60~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=qW8yOjG0vMGcHPer%2FGtBOWqpF7U%3D) - 实现屏幕自适应 ``` // 监听画面变化,更新渲染画面 window.addEventListener("resize", () => {  // 1 更新摄像头-视椎体的长宽比  camera.aspect = window.innerWidth / window.innerHeight; ​  // 2 更新摄像机的投影矩阵  camera.updateProjectionMatrix(); ​  // 3 更新渲染器  renderer.setSize(window.innerWidth, window.innerHeight); ​  // 4 设置渲染器的像素比例  renderer.setPixelRatio(window.devicePixelRatio); }); ``` 这样就不会出现上面的问题 > Camera在大多数属性发生改变后,需要调用.updateProjectionMatrix使得这些改变生效 ### 2-10 调用js接口控制画布全屏和退出全屏 ``` // 双击全屏 window.addEventListener("dblclick", () => {    const fullScreenElement = document.fullScreenElement;    if (fullScreenElement) {        // 让画布对象进入全屏        renderer.domElement.requestFullscreen();   } else {        // 直接使用文档元素退出全屏        domElement.exitFullscreen();   } }); ``` ### 2-11 应用图形用户界面更改变量 (1)安装gui库 `npm install --save dat.gui` (2)导入gui `import * as dat from "dat.gui";` (3)初始化gui界面 `const gui = new dat.GUI();` (4)创建gui设置 ``` /** * 给cube的位置的x坐标设置gui * (1)设置最小值为0 * (2)设置最大值为5 * (3)设置步进为0.01 * (4)设置名称 * (5)值被修改时的回调函数 */ gui   .add(cube.position, "x")   .min(0)   .max(5)   .step(0.01)   .name("移动x轴坐标")   .onChange((value)=> {   console.log("值被修改为:", value); }).onFinishChange((value) => {   console.log("完全停下来", value) }); ​ // 修改物体的颜色 const params = {    color: "#ffff00",    fn: () => {        // 让立方体运动起来        gsap.to(cube.position, {x: 5}, duration: 2, yoyo: true, repeat: -1);   } } gui.addColor(params, "color").onChange((value) => {    console.log("值被修改", value);    cube.material.color.set(value); }); ​ // 设置选项框 gui.add(cube, "visible").name("是否显示"); ​ // 点击触发某个事件 // gui.add(params, "fn").name("立方体运动"); ​ // 设置文件夹 var folder = gui.addFolder("设置立方体"); folder.add(cube.material, "wireframe"); folder.add(params, "fn").name("立方体运动"); ``` ### 3-1 掌握几何体顶点_UV_法向属性 - 几何体(以BoxGeometry为例) - 打印几何体的信息 ``` console.log(cubeGeometry); console.log(cube); ``` ![1720583220646.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/269c057262da42a2bf9885c6a025d23e~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=PzYvwKn8ctzlLjsRbiaVPPubS0k%3D) > 以上可以看到: > > BoxGeometry对象也可以通过cube.geometry来获取 > > BoxGeometry有一些attributes(属性) - 几何体的属性 - position ![1720583763229.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/61c820e03095477ab3785fa3ff2d283c~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=2a%2F%2BfYjGQYw8jm%2Bk7%2BkvzlPjkaU%3D) - uv ![1720584032689.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1d2e821f751341058a7d2ddbd62bb1ab~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=cs0LgNP6es%2FPx%2F19JI1ze7elzNI%3D) ![1720584295093.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5e571d21a7834139ab32d14ff829405f~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=On122VBaqxEZsmQPaGqAdlcXe5s%3D) - normal 法向 在光打过来的时候,我们需要知道立方体面得朝向,法向表述的就是面得朝向 - 细分程度 ![1720584510328.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7cd7808cffb44d62a330fa649f33cdb6~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=Ibl2jPJuANG9CjQEba7DWUszKgQ%3D) ![1720584522956.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0876694df99045e6ba32be3dde6fec49~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=x%2BH7IKWvv%2Bi%2BdMgrnxr41StRK1E%3D) > 3个顶点可以组成一个面,立方体都是由多个三角形组成的,可以将面细分成多个三角形,然后根据三角形的顶点,可以选择让某个面去凹陷 ### 3-2 BufferGeometry设置顶点创建矩形 ``` // 添加物体 const geometry = new THREE.BufferGeometry(); ​ // 创建顶点(用两个三角形组成一个面) const vertices = new Float32Array([  -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.0, -1.0, -1.0, 1.0, ]); ​ // 将创建的顶点设置为物体的位置属性 geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); ​ // 设置几何体的材质 const Material = new THREE.MeshBasicMaterial({color: 0xffff00}); ​ // 设置物体 const mesh = new THREE.Mesh(geometry, Material); ​ // 将物体添加至场景中 scene.add(mesh); ``` ![1720598522157.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f9d6ad6f0f25428cbceb6a2bdee35da7~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=rkazXAaKp9ZGKWOLHAkYMyf8inA%3D) ### 3-3 生成酷炫自定义三角形科技物体 将创建三角形面的操作放在循环中 ``` for (let i = 0; i < 50; i++) {  // 每一个三角形需要3个顶点,每个顶点需要3个值  const geometry = new THREE.BufferGeometry();  // 每一个三角形面的顶点数据有9个值,Float32Array的参数必须要设置为9,代表数组长度为9,否则设置不上  const positionArray = new Float32Array(9);  for (let j = 0; j < 9; j++) {    // 设置图形的横跨区域为-5~5    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());  // 设置几何体的材质(MeshBasicMaterial: 基础网格材质)  const material = new THREE.MeshBasicMaterial({    color: color,    // 设置透明度时,transparent的值必须为true    transparent: true,    // 透明度的值    opacity: 0.5, });  // 创建物体  const mesh = new THREE.Mesh(geometry, material);  // 添加物体至场景  scene.add(mesh); } ``` ![1720603625404.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d12bbf52749f425ebf9cb8808ce5b9d9~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=0OPvbc2zGQT2lp0gqH%2BwSnkHiEc%3D) ### 3-4 常用网格几何体 管网查看: ### 4-1 初识材质与纹理 - 基础网格材质(MeshBasicMaterial) - 立方体有可能是一个包装盒,一个烟盒等等,他们表面的`材质`与`纹理`是不一样的 - 纹理图片 ![1720607525111.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d33e491f5c16431c8058ea1881e7d641~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ifWY1CGC4QLvEPA6MKICeM3%2Bn1Q%3D) - 为立方体设置材质与纹理 ``` // 6 创建纹理加载器 const textureLoader = new THREE.TextureLoader(); // 7 加载纹理文件 const doorColorTexture = textureLoader.load("./textures/door/color.jpg"); ​ // 1 添加物体 const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); // 2 将物体设置为基础网格材质 const basicMaterial = new THREE.MeshBasicMaterial({  color: "#ffff00",  // 8 将加载的纹理设置到材质上  map: doorColorTexture, }); // 3 根据立方体和材质创建物体 const mesh = new THREE.Mesh(cubeGeometry, basicMaterial); // 4 将物体添加至场景中 scene.add(mesh); ``` - 实现效果 ![1720607559525.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/57cb38b6898d487285c8463351e9321d~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=CixFpLsN0AlmBQ%2F758xMU7xewwI%3D) ### 4-2 纹理_偏移_旋转_重复 - 偏移 ``` doorColorTexture.offset.x = 0.5; doorColorTexture.offset.y = 0.5; ​ // 也可写为 doorColorTexture.offset.set(0.5, 0.5); ``` ![1720607949933.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/12431e0c8d3d4c49bc711718b2647db2~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=tPjHyaV2rIneY9thWT7beW4t2Xs%3D) - 旋转 ``` // 设置旋转的原点为中心位置 doorColorTexture.center.set(0.5, 0.5); ​ doorColorTexture.rotation = Math.PI / 4; ``` - 旋转 ![1720667715123.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f07c0341cfc841b28daa9d6e438a5bc0~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=%2BC%2BFAeKz5NPml6u7ak2RSzGpqwo%3D) - 中心位置旋转 ![1720667753425.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1df624778b5e47c5942cedf017476637~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ya%2FVKLyP7oRQxNodVhbLJvecou0%3D) - 重复 - repeat: Vector2 决定纹理在表面的重复次数,两个方向分别表示U和V - 代码实现 ``` // 设置重复 doorColorTexture.repeat.set(2, 3); ``` ![1720668647223.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/951ce1b518694167af400b5cc9292968~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=JRhFgImiM0SD5VfDB9QLaRAxg%2BY%3D) - 如果repeat的重复次数在任何方向上设置了超过1的数值, 对应的Wrap需要设置为[THREE.RepeatWrapping](https://threejs.org/docs/index.html#api/zh/constants/Textures)或者[THREE.MirroredRepeatWrapping](https://threejs.org/docs/index.html#api/zh/constants/Textures)来 达到想要的平铺效果 - wrapS: number 定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应U默认值`THREE.ClampToEdgeWrapping`,即纹理边缘将被推到外部边缘的纹素 其它两个值分别为`THREE.RepeatWrapping(重复)` `THREE.MirroredRepeatWrapping(镜像重复)` - wrapT: number 定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于**V** ``` // 设置重复 doorColorTexture.repeat.set(2, 3); // 设置纹理重复的模式 doorColorTexture.wrapS = THREE.RepeatWrapping; doorColorTexture.wrapT = THREE.RepeatWrapping; ``` ![1720668921689.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/77fba8675bf34bebb994744f45f6aae9~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=lmcsyR5P%2FHfj%2FRlYAbqMLSQ%2B8kk%3D) ### 4-3 设置纹理显示算法与mipmap - 设置纹理效果 ``` const texture = textureLoader.load("./textures/minecraft.png"); ​ const basicMaterial = new THREE.MeshBasicMaterial({  color: "#ffff00",  map: texture, }); ``` ![1720683338701.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/132892421d1e4d208f216b7d24c2df4b~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=TXTigDhM4TXtk8t%2BdFbMkL%2FvPYM%3D) - magFilter: number 当一个纹素覆盖大于一个像素时,贴图将如何采样 默认值为[THREE.LinearFilter](https://threejs.org/docs/index.html#api/zh/constants/Textures), 它将获取四个最接近的纹素,并在他们之间进行双线性插值 [THREE.NearestFilter](https://threejs.org/docs/index.html#api/zh/constants/Textures),它将使用最接近的纹素的值 - minFilter: number 当一个纹素覆盖小于一个像素时,贴图将如何采样 默认值为[THREE.LinearMipmapLinearFilter](https://threejs.org/docs/index.html#api/zh/constants/Textures), 它将使用mipmapping以及三次线性滤镜 - 将纹理的采样值均换成 `THREE.NearestFilter` ``` texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; ``` ![1720683418733.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b9e2a4de2ce740b9aa6b2c2e2a8ca36d~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=dvn6rqWfw6L35Qh86zVPmfUhYqQ%3D) ### 4-4 透明材质与透明纹理 - 为4-1中的门设置透明区域 下图(alpha.jpg)白色区域为不透明,黑色区域为透明,灰色的为半透明 ![alpha.jpg](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/038978b8bc39418b8724a55627e7fb8b~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=yra33xp11mLAV%2FeCMpRri2kBMpA%3D) - 设置透明材质(个人理解:类似于设计中所说的蒙版,白色的为蒙版区域) - alphaMap: Texture alpha贴图是一张灰度纹理,用于控制整个表面的不透明度。(黑色:完全透明;白色:完全不透明)。 默认值为null - transparent: Boolean 定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处理,并在非透明对象之后渲染 设置为true时,通过设置材质的opacity属性来控制材质透明的程度 设置为true时,透明材质才会起效果 ``` // 加载透明材料 const doorAlphaTexture = textureLoader.load("./textures/door/alpha.jpg"); // 为材质添加透明设置 const basicMaterial = new THREE.MeshBasicMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  transparent: true }); ​ // 创建一个平面 const plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), basicMaterial); plane.position.set(3, 0, 0); scene.add(plane); ``` ![1720750363016.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/93b74720fee34484a59547f29c149286~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=4OtP4laQ7C7W1d5b4GH3o%2BtIXkM%3D) - 设置side - side: Integer 定义将要渲染哪一面 - 正面,背面或两者 THREE.FrontSide 正面(默认) THREE.BackSide 背面 THREE.DoubleSide 双面 ``` // 加载透明材料 const doorAlphaTexture = textureLoader.load("./textures/door/alpha.jpg"); // 为材质添加透明设置 const basicMaterial = new THREE.MeshBasicMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide, // 设置为双面的  transparent: true }); ​ // 创建一个平面 const plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), basicMaterial); plane.position.set(3, 0, 0); scene.add(plane); ``` ![1720750392282.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/57640d712a9b46c2b4eb87eb44fef391~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=%2FG4Z0a7%2FZ3h9ebxwx46Lv2L%2Fqkk%3D) > 设置side等属性,也可单独设置。以side为例: > > ``` > basicMaterial.side = THREE.DoubleSide; > ``` ### 4-5 环境遮挡贴图与强度 - 使用图片 ![ambientOcclusion.jpg](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9213cdda7afd42b783eff6014921a418~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=V3%2BBoIMHIAdY0vUpzkB7wI3h2m8%3D) 和透明材质的原理差不多,只不过白色是透明的,黑色是不透明的。 - 代码实现 ``` // 1 导入环境遮挡贴图 const doorAoTexture = textureLoader.load(  "./textures/door/ambientOcclusion.jpg" ); ​ // 材质 const basicMaterial = new THREE.MeshBasicMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture, // 2 环境遮挡贴图  aoMapIntensity: 1, // 2 环境遮挡贴图强度 }); ​ const cube = new THREE.Mesh(cubeGeometry, basicMaterial); scene.add(cube); // 3 为cube设置第二组UV cubeGeometry.setAttribute(  "uv2",  new THREE.BufferAttribute(cubeGeometry.attributes.uv.array, 2) ); ``` ![1721009417097.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/8346f1e9dfd943f197f20916424b6707~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=huddF8atQp2ECe%2FTGi8pqMsRQfE%3D) ### 4-6 详解PBR物理渲染 ##### 1 什么是PBR - 基于物理渲染 - 以前的渲染是在模仿灯光的外观 - 现在是模仿光的实际行为 - 使图形看起来更真实 ##### 2 PBR的组成部分 ![1721010430532.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/33737d8b6497497b89fe1c8456fededc~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=kRCUcE2JfI84XX6RPVZ2jn%2FDOTo%3D) - **灯光属性**:直接照明、间接照明、直接高光、间接高光、阴影、环境光闭塞 - **表面属性**:基础色、法线、高光、粗糙度、金属度 ##### 3 灯光属性 ###### 1、光线类型 ![1721010715592.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9263de57acb946e8b123679bcfbc6ac9~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=YokCqHZSmMclQ8dxBHTFHcZTReI%3D) - 入射光 - 直接照明:直接从光源反射阴影物体表面的光 - 间接照明:环境光和直接光经过反弹第二次进入的光 - 反射光 - 镜面光:在经过表面反射聚焦在同一方向上进入人眼的高亮光 - 漫反射:光被散射并沿着各个方向离开表面 ###### 2、光与表面相互作用类型 ![1721014223434.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f9ee733943f347f29941795b44ab4413~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=4rw30BqTZQODHTJRcykHIeRZEps%3D) - 直接漫反射:从源头到四面八方散发出来的直接高光 - 直接高光:直接来自光源并被集中反射的光 - 间接漫反射:来自环境的光被表面散射的光 - 间接高光:来自环境光并被集中反射的光 (1)间接漫反射 - ![1721014993803.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d68169eabad2448ea73bdb8197ab972b~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=i4uAnHocqIUzfXrC6Vu41rcCrZQ%3D) - 直接来自光源的光 - 撞击表面后散落在各个方向 - 在着色器中使用简单的数学计算 (2)直接高光 - ![1721015134989.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/26868cde334d4df48ee026f8ceb580f6~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=0LNAhf1Re4KAA2%2FHCcGsH9K1%2FKE%3D) - 直接来自光源的光 - 反射在一个更集中的方向上 - 在着色器中使用简单的数学计算 *直接镜面反射的计算成本比漫反射低很多* (3)间接漫反射 - ![1721015244646.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/00a03bd649914f0d92e6141e9d429b54~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=Xl1ayywWHsBi8gOMX38rAuPCpm4%3D) - 来自环境中各个方向的光 - 撞击表面后散落在各个方向 - 因为计算昂贵,所以引擎的全局照明解决方案通常胡离线渲染,并被烘焙成灯光地图 (4)镜面反射 - ![1721015347571.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f2dcd69cab7948a9b48742b4000d1a95~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=pUBFmrXYILz2RPdQtni2Iz7S8B0%3D) - 来自环境中各个方向的光 - 反射在一个更集中的方向上 - 引擎中使用反射探头,平面反射,SSR,或射线追踪计算 ##### 4 灯光属性 ###### 1、基础色 - ![1721015503040.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/da776054bdf54222b325744348e9b508~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ruLti9pdZjjnkxuWO4jt9r%2FcUxA%3D) - 定义表面的漫反射颜色 - 真实世界的材料不会比20暗或比240 sRGB亮 - 粗糙表面具有更高的最低 ~ 50 sRGB - 超出范围的值不能正确发光,所以保持在范围内是至关重要的 - ![1721015639652.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a8cfe989c300499196940c6eb9bb5929~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=%2B4V%2Bur5NNN9CzHSY0yg%2B9cQq1i8%3D) - ***基础色贴图制作注意点:*** - 不包括任何照明或阴影 - 基本颜色纹理看起来应该非常平坦 - 使用真实世界的度量或获取最佳结果的数据 ###### 2、法线 - ![1721015943674.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/fd47bcd1a57e4f33a317e54554ad49a8~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=vg6Urw1ztBLA9PWpzQNUTGhi3Rc%3D) - 定义曲面的形状每个像素代表一个矢量 - 该矢量指示表面所面对的方向即使网格是完全平坦的 - 法线贴图会使表面显得凹凸不平 - 用于添加表面形状的细节,这里的三角形是实现不了的 - 因为它们表示矢量数据,所以法线贴图是无法手工绘制的 ###### 3、镜面 - ![1721016158206.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/028764a86b3c4b7385a29c3f41ce84bc~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=XX2W6lL2vq733%2FEdR3%2BiK50JEso%3D) - 用于直接和间接镜面照明的叠加 - 当直视表面时,定义反射率 - 非金属表面反射约4%的光 - 0.5代表4%的反射 - 1.0代表8%的反射但对于大多数物体来说太高了 - 在掠射角下,所有表面都是100%反射的,内置于引擎中的菲涅耳项 - ![1721016350181.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/06f219374bb34e98a7d47a965411f720~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=%2BnAXCXZ6aEbUGka9q1exf%2FvXKAo%3D) - ***镜面贴图制作注意点:*** - 高光贴图应该大多在0.5 - 使用深色的阴影来遮盖不应该反光的裂缝 - 一个裂缝贴图乘以0.5就是一个很好的高光贴图 ###### 4、粗糙度 - ![1721016498390.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7b62e152c510431aa2ec60743c6a2989~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=cG7jgCEo5%2FE3NcZGQhcy2ixOLhc%3D) - ***粗糙度贴图制作注意点:*** - 没有技术限制-完全艺术的选择 - 艺术家可以使用这张地图来定义表面的“特征”,并展示它的历史 - 考虑一下被打磨光滑、磨损或老化的表面 ###### 5、金属度 - ![1721016533634.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1c6757c6a8114bc695ee34b6bde1345e~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=IY5UmOkuy4ETFHt4egq4FjRPTOc%3D) - 两个不同的着色器通过金属度混合他们 - 基本色变成高光色而不是漫反射颜色 - 金属漫反射是黑色的 - 在底色下,镜面范围可达100% - 大多数金属的反光性在60%到100%之间 - 确保对金属颜色值使用真实世界的测量值,并保持它们明亮 - 当金属为1时,镜面输入将被忽略 ![1721017028123.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/76e33ae1c97a4232b5fa6546a9914ad0~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=4a545sz1VJ19opem8dNKdgLeiXM%3D) ***粗糙度贴图制作注意点:*** - 将着色器切换到金属模式 - 灰度值会很奇怪,最好使用纯白色或黑色 - 当金属色为白色时,请确保使用正确的金属底色值 - 没有黑暗金属这回事 - 所有金属均为180srgb或更亮 ##### 5 金属和非金属 - ![1721017144670.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/917e0fed26154fd8ac8a11373746aea8~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=0fFPLQ3znamcki3DSoYSJAllAYE%3D) **非金属** - 基础颜色=漫反射 - 镜面反射=0-8% **金属** - 基础颜色=0-100%的镜面反射 - 镜面=0% - 漫反射总是黑色的 ##### 6 总结 1.PBR是基于物理渲染的着色模型,PBR着色模型分为***材质*** 和 ***灯光*** 两个属性 2.材质部分由: ***基础色***、***法线***、***高光***、***粗糙度***、***金属度*** 来定义材质表面属性的 3.灯光部分是由: ***直接照明***、***间接照明***、***直接高光***、***间接高光***、***阴影***、***环境光闭塞*** 来定义照明属性的 4.通常我们写材质的时候只需要关注材质部分的属性即可,灯光属性都是引擎定义好的直接使用即可 5.PBR渲染模型不但指的是PBR材质,还有灯光,两者缺一不可 ### 4-7 标准网格材质与光照物理效果 (1)MeshStandardMaterial - 标准网格材质 - 特点 - 之前 MeshBasicMaterial(基础网格材质)用到的属性均有 - 必须要有光照才能显示,否则显示的是黑色的 - 将之前的门改为标准网格材质 ``` // 标准材质 const material = new THREE.MeshStandardMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture,  aoMapIntensity: 1, }); ​ // 添加物体 const cube = new THREE.Mesh(cubeGeometry, material); scene.add(cube); ``` ![1721026574702.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9795c273743040179dac6b42d411e15c~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=BKpMfuzIU1ehNBKY9uON2rsZJY0%3D) > 能看到黑色的影子,但是看不清是什么 (2)DirectionalLight - 直线光/平行光 - 特点: - 平行光是沿着特定方向发射的光 - 第二个参数是光的强度,默认是1 - 光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果 - 只能从一个方向照射,从其它方向看可能就看不见光 - position:设置平行光的方向 - 为门设置平行光 ``` // 创建平行光 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // 设置平行光位置 directionalLight.position.set(10, 10, 10); // 添加平行光 scene.add(directionalLight); ``` - 正面 ![1721026942515.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/69f62ab85f90498d8bcb4a5566711277~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=DWC0f7Ncx%2FmEnPUDk9MO7Hrtra4%3D) - 背面 ![1721026960702.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/bfe5b9a0c24343dea1ebabb80b877457~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ifZE7zPlhQCU2GZ8d1MHrLZir78%3D) (3)Ambient - 环境光 - 特点 - 会均匀的照亮场景中的所有物体 - 环境光不能用来投射阴影,因为它没有方向 - 为门创建环境光 ``` // 设置环境光 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); ``` - 正面 ![1721027158996.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/43d0fccc120249cc9e9c271f2c0000f8~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=Sd9%2B7pUA%2B%2B2YQtiX1BdUnJT060A%3D) - 背面 ![1721027170123.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7b8ff8cb56bc497d8fa1340fad4557ea~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=T%2FzX4lP7xnKoEBHZeTzDkFEwHw0%3D) ### 4-8 置换贴图与顶点细分设置 - displacementMap:Texture - 位移贴图会影响网格顶点的位置,与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象, 以及充当真实的几何体。位移纹理是指:网格的所有顶点被映射为图像中每个像素的值(白色是最高的),并且被重定位 - 本案例中用于设置门表面的高低不平 - displacementScale:Float - 位移贴图对网格的影响程度(黑色是无位移,白色是最大位移) - 如果没有设置位移贴图,则不会应用此值。默认值为1 - 为门设置置换贴图 ``` // 1 导入置换纹理 const doorHeightTexture = textureLoader.load("./textures/door/height.jpg"); ​ // 3 设置物体的细分程度 const cubeGeometry = new THREE.BoxGeometry(1, 1, 1, 100, 100, 100); ​ // 材质 const material = new THREE.MeshStandardMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture,  aoMapIntensity: 1,  displacementMap: doorHeightTexture, // 2 设置置换贴图(只设置这个顶点不会凸起,门还是平的)  displacementScale: 0.1, // 4 设置位移贴图对网格的影响程度(默认是1,因为顶点有100份,所以设置0.1) }); ``` ![1721028619162.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a87dd69a021a4fd2849576bf009f28c2~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=scYCLUvxRcwTK7uSC6BH6wJnzOk%3D) ### 4-9 设置粗糙度与粗糙度贴图 - roughness: Float - 材质的粗糙程度 - 0.0表示平滑的镜面反射,1.0表示完全漫反射。默认值为1.0 - 如果还提供roughnessMap,则两个值相乘 - roughnessMap: Float - 该纹理的绿色通道用于改变材质的粗糙度 - 为门设置粗糙度贴图 ``` // 1 导入粗糙度纹理 const roughnessTexture = textureLoader.load("./textures/door/roughness.jpg"); ​ const material = new THREE.MeshStandardMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture,  aoMapIntensity: 1,  displacementMap: doorHeightTexture,  displacementScale: 0.1,  roughness: 1, // 2 设置粗糙程度  roughnessMap: roughnessTexture, // 3 设置粗糙度纹理 }); ``` ![1721029511464.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5cd43e4af8a64cad8805386372d65d73~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=PUWViN%2FTIIaaosNg0Sx540JhnNw%3D) > 门反射光,金属锁扣部分反射光 ### 4-10 设置金属度与金属贴图 - metalness: Float - 材质与金属的相似度 - 非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值。 默认值为0.0 - 0.0到1.0之间的值可用于生锈金属的外观。如果还提供了metalnessMap,则两个值相乘 - metalnessMap: Texture - 该纹理的蓝色通道用于改变材质的金属度 - 设置金属贴图 ``` // 1 导入金属纹理 const metalnessTexture = textureLoader.load("./textures/door/metalness.jpg"); ​ const material = new THREE.MeshStandardMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture,  aoMapIntensity: 1,  displacementMap: doorHeightTexture,  displacementScale: 0.1,  roughness: 1,  roughnessMap: roughnessTexture,  metalness: 1, // 2 设置金属度  metalnessMap: metalnessTexture, // 3 设置金属贴图 }); ``` ![1721030100835.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5c7f7a44571643a0a508ba1942c12c42~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=yQSdCD4r4QcJuUeh6fUHZ2BUZXA%3D) ### 4-11 法线贴图应用 - normalMap: Texture - 创建法线贴图的纹理 - RGB值会影响每个像素片段的曲面法线,并更改颜色照亮的方式 - 法线贴图不会改变曲面的实际形状,只会改变光照 - 设置法线贴图 ``` // 1 导入法线贴图文件 const normalTexture = textureLoader.load("./textures/door/normal.jpg"); ​ const material = new THREE.MeshStandardMaterial({  color: "#ffff00",  map: doorColorTexture,  alphaMap: doorAlphaTexture,  side: THREE.DoubleSide,  transparent: true,  aoMap: doorAoTexture,  aoMapIntensity: 1,  displacementMap: doorHeightTexture,  displacementScale: 0.1,  roughness: 1,  roughnessMap: roughnessTexture,  metalness: 1,  metalnessMap: metalnessTexture,  normalMap: normalTexture, // 2 设置发现纹理 }); ``` ![1721033810955.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ab969b5ba25b43d59f069bcd8cfe38c8~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=ueV2bXZ6kh19ga49VzPKcalIf3g%3D) ### 4-12 如何获取各种类型纹理贴图 [poliigon.com](https://www.poliigon.com) 需要用clash加速:[3dtextures.me](https://3dtextures.me/) 需要打开pigcha:[arroway-textures.ch](https://www.arroway-textures.ch/) Quixel Bridge:需要注册虚幻引擎账号,被游戏公司收购,有虚幻引擎账号,资源免费试用 ### 4-13 纹理加载进度情况 - 单张纹理加载进度 ``` const doorColorTexture = textureLoader.load(  "./textures/door/color.jpg", (onload = () => {    console.log("加载完成"); }), (onprogress = (e) => {    console.log(e);    console.log("开始加载"); }), (onerror = (e) => {    console.log(e);    console.log("加载失败"); }) ); // 或者 const event = {}; event.onload = () => {  console.log("加载完成"); }; event.onprogress = () => {  console.log("加载中"); }; event.onerror = () => {  console.log("加载失败"); }; const doorColorTexture = textureLoader.load(  "./textures/door/color.jpg",  event.onload,  event.onprogress,  event.onerror ); ``` > 由于单张纹理加载得太快了,所以加载中显示不出来 - 多张纹理加载进度 - 加载管理器(LoadingManager) - 代码实现 ``` const event = {}; event.onload = () => {  console.log();  console.log("加载完成"); }; event.onprogress = (e) => {  console.log(e);  console.log("加载中"); }; event.onerror = (e) => {  console.log(e);  console.log("加载失败"); }; ​ // 设置加载管理器 const loadingManager = new THREE.LoadingManager(  event.onload,  event.onprogress,  event.onerror ); ​ // 将加载管理器设置给纹理加载器,可以检测到多个纹理的加载进度 const textureLoader = new THREE.TextureLoader(loadingManager); ``` ![1721095642874.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1e1a870b05124726a7a7de22cb7eb539~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=UeFcm%2B1q4pRXVqwbdZUsBBAecEo%3D) ### 4-14 详解环境贴图 - CubeTextureLoader - 立方体纹理加载器 - 纹理设置要设置6个面的,px,py,pz,nx,ny,nz(p正面方向,n负面方向) - 加载的 CubeTexture 采用 sRGB 色彩空间。这意味着 colorSpace 属性默认设置为 THREE.SRGBColorSpace - 实现环境贴图 之前的代码,除了基础设置(场景、相机、灯光、渲染器、轨道控制器、坐标轴辅助器、渲染函数、监听页面变化)外,均删除 ``` // 1 设置cube纹理加载器 const cubeTextureLoader = new THREE.CubeTextureLoader(); // 2 加载纹理 const envMapTexture = cubeTextureLoader.load([  "./textures/environmentMaps/1/px.jpg",  "./textures/environmentMaps/1/nx.jpg",  "./textures/environmentMaps/1/py.jpg",  "./textures/environmentMaps/1/ny.jpg",  "./textures/environmentMaps/1/pz.jpg",  "./textures/environmentMaps/1/nz.jpg", ]); ​ // 3 设置物体(球) const sphereGeometry = new THREE.SphereGeometry(1, 20, 20); const material = new THREE.MeshStandardMaterial({  metalness: 1,  roughness: 0.1,  envMap: envMapTexture, }); const sphere = new THREE.Mesh(sphereGeometry, material); scene.add(sphere); ``` ![1721097302177.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/aaedcba1f5d143a48b62b801c85d930a~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=tW9T6JEh23i%2BPKB7tgJ4KCjFOqk%3D) ### 4-15 经纬线映射贴图 - 设置环境背景/给场景添加背景 ``` // 给场景添加背景 scene.background = envMapTexture; ​ // 给场景所有的物体添加默认的环境贴图 scene.enviornment = envMapTexture; ​ const material = new THREE.MeshStandardMaterial({  metalness: 1,  roughness: 0.1,  // envMap: envMapTexture, // 给场景中所有的物体添加环境贴图时,可以不给材质单独设置环境贴图 }); ``` ![1721380535850.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/4186b4d5b4fc4f63a65cea8f8fd75e19~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=OgTSzGA85NgiRhGvt00e%2FQRmclQ%3D) - HDR - 高动态范围显示技术 HDR技术是一种改善动态对比度的技术,HDR就是高动态范围技术,如其名字一样,HDR技术增加了亮度范围,同时提升最亮和最暗画面的对比度,从而获得更广泛的色彩范围,除了明显改善灰阶,也带来了更黑或更白的颜色效果。这样用户就可以看到更多的细节,当然前提是你放映的片源也要支持HDR技术才可以,目前市面上使用HDR技术录制的视频还很少。 - HDR处理的照片效果 HDR技术使得图像看上去效果更好,图像充满活力,而不是洗白或偏色的图像,使得整体画质表现力有较大的提升。从技术角度来看,其的确对于用户来说是意义的,但是其实HDR技术和3D技术在某种意义上有着相同的尴尬,那就是这种技术到底能不能有用武之地。 - HDR图一般比较大 - 经纬线贴图 EquirectangularReflectionMapping 和 EquirectangularRefractionMapping 用于等距圆柱投影的环境贴图,也被叫做经纬线映射贴图。等距圆柱投影贴图表示沿着其水平中线360°的视角,以及沿着其垂直轴向180°的视角。贴图顶部和底部的边缘分别对应于它所映射的球体的北极和南极。 ``` import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; ​ const rgbeLoader = new RGBELoader(); rgbeLoader.loadAsync("textures/hdr/002.hdr").then((texture) => {  texture.mapping = THREE.EquirectangularReflectionMapping;  scene.background = texture;  scene.environment = texture; }); ``` ![1721382199007.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/effbd4dbd68a440db6f3fb50940e0516~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=kted2TKfzI1igV5iPLXVA%2BRjeH0%3D) ### 4-16 清除物体_几何体_材质_纹理_保证性能和内存不泄露 - 在每一次渲染后清除内容,以便下一次渲染 - dispose() 清除 - 详情看创建canvas材质和render函数 ``` import * as THREE from "three"; // 导入轨道控制器 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; ​ // 1 创建场景 const scene = new THREE.Scene(); ​ // 2 创建相机 const camera = new THREE.PerspectiveCamera(  75,  window.innerWidth / window.innerHeight,  0.1,  1000 ); ​ // 3 设置相机位置 camera.position.set(0, 0, 10); ​ // 4 将相机添加到场景当中 scene.add(camera); ​ // 设置物体 const sphereGeometry = new THREE.SphereGeometry(1, 20, 20); const material = new THREE.MeshStandardMaterial({  metalness: 1,  roughness: 0.1,  // envMap: envMapTexture, }); const sphere = new THREE.Mesh(sphereGeometry, material); ​ scene.add(sphere); ​ // 追加灯光 // 环境光 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); ​ // 平行光 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // 设置位置 directionalLight.position.set(10, 10, 10); scene.add(directionalLight); ​ // 6 渲染 // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); // 设置渲染器的尺寸和大小 renderer.setSize(window.innerWidth, window.innerHeight); ​ // 7 将webgl渲染的canvas内容添加到body当中 document.body.appendChild(renderer.domElement); ​ // 8 使用渲染器通过相机将场景渲染 renderer.render(scene, camera); ​ // 9 创建轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); // 设置控制器阻尼,使控制器更有真实的效果 controls.enableDamping = true; ​ // 10 添加坐标轴辅助器 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); ​ window.addEventListener("dblclick", () => {}); ​ // 创建canvas材质 function createImage() {  const canvas = document.createElement("canvas");  canvas.width = 256;  canvas.height = 256;  const ctx = canvas.getContext("2d");  ctx.fillStyle = `rgb(${Math.random() * 255},${Math.random() * 255},${    Math.random() * 255  })`;  ctx.fillRect(0, 0, 256, 256);  return canvas; } ​ function render() {    // 创建物体    const sphereGeometry = new THREE.SphereGeometry(        1,        Math.random() * 64,        Math.random() * 32   ); ​    // 创建canvas纹理贴图    const texture = new THREE.CanvasTexture(createImage());    const sphereMaterial = new THREE.MeshBasicMaterial({        map: texture,        // color: Math.random() * 0xffffff,   }); ​    const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); ​    scene.add(sphere); ​    controls.update();    renderer.render(scene, camera);    // 渲染下一帧的时候调用render函数    requestAnimationFrame(render); ​    // 清除场景中物体    scene.remove(sphere); ​    // 渲染完画面清除物体(几何体、材质、纹理贴图)    sphereGeometry.dispose();    sphereMaterial.dispose();    texture.dispose(); } ​ render(); ​ // 监听画面变化,更新渲染画面 window.addEventListener("resize", () => {    // 更新摄像头    camera.aspect = window.innerWidth / window.innerHeight;    // 更新摄像机的投影矩阵    camera.updateProjectionMatrix();    // 更新渲染器    renderer.setSize(window.innerWidth, window.innerHeight);    // 设置渲染器的像素比    renderer.setPixelRatio(window.devicePixelRatio); }); ​ ``` ### 5-1 灯光与阴影的关系与设置 - 阴影 - 环境光不产生因阴影 - 平行光会产生阴影(eg: 太阳光) - 点光源产生阴影 - 聚光灯产生阴影 - 平面光源不支持阴影(窗户的光) - 材质 - 基础网格材质不受光照影响 - 标准网格材质受光源影响,基于物理的渲染(PBR)——常用 - Lambert网格材质是一种非光泽表面材质,没有镜面高光(未经处理过的木材或石材) - Phong网格材质是一种具有镜面高光的光泽表面的材质(漆面木材或石材) - 物理网格材质(比基础网格材质更逼真的物理) - MeshToonMaterial一种实现卡通着色的材质 - 灯光阴影目标 - 材质要满足能够对光照有反应 - 设置渲染器开启引阴影的计算 `renderer.shadowMap.enabled = true;` - 设置光照投射阴影 `directionalLight.castShadow = true;` - 设置物体投射阴影`sphere.castShadow = true;` - 设置物体接收阴影 `plane.receiveShadow = true;` ``` // 设置球体 const sphereGeometry = new THREE.SphereGeometry(1, 20, 20); const material = new THREE.MeshStandardMaterial(); const sphere = new THREE.Mesh(sphereGeometry, material); scene.add(sphere); sphere.castShadow = true; // 设置物体投射阴影 ​ // 创建平面 const planeGeometry = new THREE.PlaneGeometry(10, 10); const plane = new THREE.Mesh(planeGeometry, material); plane.position.set(0, -1, 0); plane.rotation.x = -Math.PI / 2; plane.receiveShadow = true; // 设置物体接收阴影 scene.add(plane); ​ // 平行光 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(5, 5, 5); scene.add(directionalLight); directionalLight.castShadow = true; // 设置光照投射阴影 ​ // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); renderer.shadowMap.enabled = true; // 设置渲染器开启引阴影的计算 ``` ![1721449640624.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c221d5fc6efe49c39c39c41835641291~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=yMeGe7oIxNGgXYPJadS6FnCFRaA%3D) ### 5-2 平行光阴影属性与阴影相机原理 - shadow.radius - 设置阴影模糊度(将此值设置为大于1的值将模糊阴影的边缘) ``` directionalLight.shadow.radius = 20; ``` ![1721450139107.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/31934dbce1d04e05a793096d20bfe148~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=kfIOZP2IiydBtI2goCFWvRhVVgQ%3D) - mapSize: Vector2 阴影贴图的分辨率 默认512*512,设置较大,阴影会较细致 ``` directionalLight.shadow.mapSize.set(4096, 4096); ``` ![1721725814892.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0dc7f3776b0244ef84fb008114cf7bef~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=draLGaYSvZpuz8dk%2FLA6J9W4mPg%3D) - 设置平行光投射相机的属性 ``` directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 500; directionalLight.shadow.camera.top = 5; directionalLight.shadow.camera.bottom = -5; directionalLight.shadow.camera.left = -5; directionalLight.shadow.camera.right = 5; ``` - updateProjectionMatrix:更新相机的投影矩阵 ``` // 使用gui查看阴影 // 导入dat.gui import * as dat from "dat.gui"; ​ //创建gui对象 const gui = new dat.GUI(); ​ gui .add(directionalLight.shadow.camera, "near") .min(0) .max(10) .step(0.1) .onChange(() => {    directionalLight.shadow.camera.updateProjectionMatrix(); }); ``` ![1721728109174.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c8ed39e0908b4052b1ce5350fbe385c4~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=%2F9mCkxJD7KSkQwhl2kEaLk%2BZKeU%3D) ### 5-3 详解聚光灯各种属性与应用 - SpotLight:聚光灯 - color:十六进制光照颜色 - intensity:光照强度 - distance:从光源发出光的最大距离,其强度根据光源的距离线性衰减 0为不衰减 - angle:光线散射角度,最大为Math.PI/2。默认值为Math.PI/3 - penumbra:聚光锥的半影衰减百分比。在0和1之间的值。默认为0 - decay:沿着光照距离的衰减量 ``` // 创建点光源并进行基本设置 const spotLight = new THREE.SpotLight(0xffffff, 0.5); // 设置 spotLight.position.set(5, 5, 5); spotLight.castShadow = true; ​ spotLight.shadow.radius = 20; spotLight.shadow.mapSize.set(4096, 4096); ​ scene.add(spotLight); ``` ![1721728655241.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/bcad6f06270c454f92d53cbd8551a189~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=kg%2BJGCCqHUaXH2FyyeU81amntH8%3D) - 移动聚光灯的位置看效果 ``` // 将之前创建的平面面积改大一点 const planeGeometry = new THREE.PlaneBufferGeometry(50, 50); // 大小设置为50, 50 ​ // 使用gui监听position的x gui.add(sphere.position, "x").min(-5).max(5).step(0.1); ``` ![1721729079504.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b156f45b77a74484b39c6e871c587d13~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=yCNAyCxK1Mbj5Xd2KvMK%2Fsu0%2BeA%3D) - 设置聚光灯的角度并监听 ``` spotLight.angle = Math.PI / 6; // 30度 ​ gui .add(spotLight, "angle") .min(0) .max(Math.PI / 2) .step(0.01); ``` ![1721729267298.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/beb23b23d90e49f9ae7bfd91aea09317~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=7oXE%2BYSz8bs5cTYBct8xuAO1dlI%3D) > 聚光灯的效果类似于透视相机,即可以设置camera的属性和透视相机一样 - 设置衰减效果 ``` spotLight.distance = 0; ​ gui .add(spotLight, "distance") .min(0) .max(10) .step(0.01); ``` ![1721729924182.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b7d9ada79a87492abb3ce826e5158601~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=mpr5viqqsEXWg5aheczAMFyQg6A%3D) - 设置聚光锥的半影衰减百分比 ``` spotLight.penumbra = 0; ​ gui .add(spotLight, "penumbra") .min(0) .max(1) .step(0.01); ``` ![1721730105864.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/13c5abaf08824215b4649be0f20e36b5~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=5sVQlqY%2FzkDNIDA5LROoIp5Yn7s%3D) - 设置decay ``` spotLight.decay = 0; ​ gui .add(spotLight, "decay") .min(0) .max(5) .step(0.01); ​ // 这里需要设置渲染器为physicallyCorrect才能起效果 renderer.physicallyCorrectLights = true; ``` ![1721807209001.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/50003746d19c4d86ae257a41e7c108e8~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=6Yz28Xos0iPkRFjz6vmtgTpMjFw%3D) - 调节亮度 ``` spotLight.intensity = 2; ``` ### 5-4 详解点光源属性与应用 - 设置点光源 ``` // 点光源 const pointLight = new THREE.PointLight(0xff0000, 1); // 设置 pointLight.position.set(2, 2, 2); pointLight.castShadow = true; ​ pointLight.shadow.radius = 20; pointLight.shadow.mapSize.set(4096, 4096); pointLight.intensity = 2; ​ scene.add(pointLight); ​ gui.add(pointLight.position, "x").min(-5).max(5).step(0.1); gui.add(pointLight, "distance").min(0).max(10).step(0.01); gui.add(pointLight, "decay").min(0).max(5).step(0.01); ``` ![1721808161711.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/3d27beed17a7404f9997f00f6b920a43~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=Y1w4LL7iTDHGCV9PskmPWdhdkDQ%3D) - 创建一个小球,模拟点光源的位置 ``` // 创建小球 const smallBall = new THREE.Mesh(  new THREE.SphereGeometry(0.1, 20, 20),  new THREE.MeshBasicMaterial({ color: 0xff0000 }) ); smallBall.position.set(2, 2, 2); // 将小球设置到点光源的位置 ​ // 将点光源的位置设置 和 添加点光源至场景的操作 给注释掉 // pointLight.position.set(2, 2, 2); // scene.add(pointLight); ​ // 将点光源作为子元素添加到小球上 smallBall.add(pointLight); // 将小球添加到场景中 scene.add(smallBall); ``` ![1721808903339.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6165faae107e47cc9a78bb97a7da6b16~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=hXE5q6w65zntPtHyB0%2BRI8Q3AxY%3D) - 让小球围绕着物体转,并且上下运动 ``` // 设置时钟 const clock = new THREE.Clock(); ​ function render() {  let time = clock.getElapsedTime();  smallBall.position.x = Math.sin(time) * 3; // 设置小球左右来回移动  smallBall.position.z = Math.cos(time) * 3; // 设置小球绕着物体转圈  smallBall.position.y = Math.abs(Math.sin(time * 10)) * 2; // 设置小球上下运动 ​  controls.update();  renderer.render(scene, camera); ​  // 渲染下一帧的时候调用render函数  requestAnimationFrame(render); } ``` ![1721809654778.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2a6d8d95a0894b53b3a65c449513a7d4~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191440&x-orig-sign=5yVcZqGm9Jq3rWYXnzX8AJZ1Xls%3D) ### 6-1 初识Points与点材质 - 创建一个球,展示方式为网格,颜色为红色 ``` const sphereGeometry = new THREE.SphereGeometry(3, 20, 20); const material = new THREE.MeshBasicMaterial({  color: 0xff0000,  wireframe: true, }); const sphere = new THREE.Mesh(sphereGeometry, material); scene.add(sphere); ``` ![1721810638991.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/dee571c92a874ab48141e932137351ee~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=S4Zh4WlEydL%2ButJgWyA6hS9%2Bw58%3D) - 创建点材质 ``` const sphereGeometry = new THREE.SphereGeometry(3, 20, 20); // const material = new THREE.MeshBasicMaterial({ //   color: 0xff0000, //   wireframe: true, // }); // const sphere = new THREE.Mesh(sphereGeometry, material); // scene.add(sphere); ​ // 设置点材质(大小) const pointsMaterial = new THREE.PointsMaterial(); pointsMaterial.size = 0.1; // 创建点堆物体 const points = new THREE.Points(sphereGeometry, pointsMaterial); scene.add(points); ``` ![1721810744427.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6df18123b8104cf29eb82f15acf47932~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=EliDJ6IIR6NTz6eQg%2BzEjIaxwbE%3D) ### 6-2 深度解析点材质属性 - 设置点材质颜色 ``` pointsMaterial.color.set(0xfff000); ``` ![1721811277734.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/30f9e24a6348499ab2fcd57239d88a7d~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=GLpWHZTMs1XsQY7phGxZ8umTxeY%3D) - 设置是否按相机深度衰减:默认true,一般不进行设置(上图中的方块近大远小,设置为false每个点会一样大) ``` pointsMaterial.sizeAttenuation = false; ``` ![1721811392948.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/3b43933768a54ea1a824febacd5fe1db~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=5CuG4XpHneqcY8%2FeTLkyDpN6Ltg%3D) - 为点材质设置纹理,点材质设置纹理的时候是对整个物体进行纹理设置,而不是物体上面的每一个点设置 ``` // 注掉相机深度衰减 // pointsMaterial.sizeAttenuation = false; ​ // 载入纹理 const textureLoader = new THREE.TextureLoader(); const texture = textureLoader.load("./textures/particles/1.png"); pointsMaterial.map = texture; pointsMaterial.alphaMap = texture; pointsMaterial.transparent = true; ``` ![1721813727633.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9ed6818b14f7463b8945e67f3c095b61~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=v1eQYCzB%2BR6LDcM1lG6TiioIMKU%3D) > threejs版本:0.152.2 和 0.166.0 > > - 设置map时,材质给整个物体区域设置纹理 > > threejs版本:0.137.5 > > - 设置map时,材质给整个物体区域内的每一个点设置纹理 - 设置点材质后,材质图案有黑边的时候,前面的点会挡到后面的点 ![1721877566212.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5b0b1a1d1d7043329199fa9a4e851234~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=tOHkEc1rXZ4%2FvFbAMRfZRtfDcI4%3D) > 可以通过设置透明度贴图来解决 ``` pointsMaterial.alphaMap = texture; pointsMaterial.transparent = true; ``` ![1721877639548.png] - depthWrite:Boolean 渲染此材质是否对缓冲区有任何影响。默认为true。 ``` pointsMaterial.depthWrite = false; ``` ![1721877937698.png] - blending:Blending - 使用此材质显示对象时要使用何种混合 - 必须将其设置为[CustomBlending](https://threejs.org/docs/index.html#api/zh/constants/Materials)才能使用自定义blendSrc, blendDst 或者 [page:Constant blendEquation]。 混合模式所有可能的取值请参阅[constants](https://threejs.org/docs/index.html#api/zh/constants/Materials)。默认值为[NormalBlending](https://threejs.org/docs/index.html#api/zh/constants/Materials)。 - 混合模式 ``` THREE.NoBlending  // 无混合 THREE.NormalBlending  // 正常混合 THREE.AdditiveBlending  // 叠加混合 THREE.SubtractiveBlending  // 减法混合 THREE.MultiplyBlending  // 乘法混合 THREE.CustomBlending  // 自定义混合 ``` ![1721878478547.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/67e7dd5b0af7421d97eb0a37867c5fe2~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=0bQJXY4LlvzSh%2BEnkZwpbm2Q3xA%3D) ### 6-3 应用定点着色器打造绚丽多彩的星空 - 创建星空效果 ``` // 不要之前的球体范围了,保留材质等的设置 ​ // 创建缓冲物体 const particlesGeometry = new THREE.BufferGeometry(); //设置5000个点 const count = 5000; // 设置缓冲区数组 const positions = new Float32Array(count * 3); // 设置顶点 for (let i = 0; i < count * 3; i++) {  positions[i] = (Math.random() - 0.5) * 30; } // 设置各个点的位置 particlesGeometry.setAttribute(  "position",  new THREE.BufferAttribute(positions, 3) ); ``` ![1721879114207.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c031385c8e324b67a7d6fa919a14b05d~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=97lPdvYbnFTR2bUlcv9PcK4AA1w%3D) - 创建五彩的星空效果 ``` // 设置每一个顶点的颜色 const colors = new Float32Array(count * 3); // 设置顶点 for (let i = 0; i < count * 3; i++) {  positions[i] = (Math.random() - 0.5) * 30;  colors[i] = Math.random(); // 设置颜色值 Math.random() 默认0-1,对于颜色来说就是0-255 } particlesGeometry.setAttribute(  "position",  new THREE.BufferAttribute(positions, 3) ); // 设置顶点颜色 particlesGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); ​ /** * 以上设置不会启效果,需要启用顶点颜色设置 */ pointsMaterial.vertexColors = true; ``` ![1721879562317.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d57f285c80614d5ea5009c7b5392f20f~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=AaRp6AI0cK%2FbDvWCenynU9uwkF0%3D) ### 6-4 通过封装与相机裁剪实现漫天飞舞的雪花 - 素材网站: - [KENNEY](https://www.kenney.nl/assets/particle-pack) ![1721879765330.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b0340d93cd464c7f8cd2f31ab3408c08~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=n6X4%2FxSn1D6lMcfCidesLENo5zU%3D) - iconfont - 爱给网 - 实现漫天飞舞的雪花 ![snow.png] ``` // 将纹理替换为雪花的纹理 const texture = textureLoader.load("./textures/particles/snow.png"); ​ // 将顶点的颜色值设置为1 for (let i = 0; i < count * 3; i++) {  positions[i] = (Math.random() - 0.5) * 30;  colors[i] = 1; } ​ // 将之前设置的黄颜色注释掉 // pointsMaterial.color.set(0xfff000); ​ // 在渲染函数中设置旋转值 function render() {  let time = clock.getElapsedTime();  points.rotation.x = time * 0.05; ​  controls.update();  renderer.render(scene, camera); ​  // 渲染下一帧的时候调用render函数  requestAnimationFrame(render); } ``` ![1721890390612.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d741e0bd51ea4cf6a93231c2dc5c9323~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=1Nf13pIJF0Y4DOa42zDo2wm9iWE%3D) - 将创建雪花封装成函数 ``` // 创建物体 function createPoints(url, size = 0.5, color) {  // 创建物体  const particlesGeometry = new THREE.BufferGeometry();  const count = 10000; ​  // 设置缓冲区数组  const positions = new Float32Array(count * 3);  // 设置每一个顶点的颜色  const colors = new Float32Array(count * 3);  // 设置顶点  for (let i = 0; i < count * 3; i++) {    positions[i] = (Math.random() - 0.5) * 30;    if (color) {      colors[i] = color;   } else {      colors[i] = Math.random();   } }  particlesGeometry.setAttribute(    "position",    new THREE.BufferAttribute(positions, 3) );  particlesGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); ​  // 设置点材质(大小)  const pointsMaterial = new THREE.PointsMaterial();  pointsMaterial.size = size; ​  // 载入纹理  const textureLoader = new THREE.TextureLoader();  const texture = textureLoader.load(`./textures/particles/${url}.png`);  pointsMaterial.map = texture;  pointsMaterial.alphaMap = texture;  pointsMaterial.transparent = true;  pointsMaterial.depthWrite = false;  pointsMaterial.blending = THREE.AdditiveBlending;  // 启用顶点颜色设置  pointsMaterial.vertexColors = true; ​  // 创建点堆物体  const points = new THREE.Points(particlesGeometry, pointsMaterial);  scene.add(points);  return points; } ​ // 创建三个不同的粒子 const pointsSnow = createPoints("snow", 0.05, 1); const pointsStart = createPoints("9", 0.1, 1); const points08 = createPoints("8", 0.1, 1); ​ // 设置三个不同粒子的运动方向 function render() {  let time = clock.getElapsedTime();  pointsSnow.rotation.x = time * 0.05;  pointsStart.rotation.x = time * 0.05;  pointsStart.rotation.y = time * 0.01;  points08.rotation.x = time * 0.01;  points08.rotation.y = time * 0.01; ​  controls.update();  renderer.render(scene, camera); ​  // 渲染下一帧的时候调用render函数  requestAnimationFrame(render); } ``` > 上述方式设置完后,可以看到有的粒子往相反的方向(上)运动,这个时候可以设置相机的近端面和远端面 > > ``` > // 创建相机 > const camera = new THREE.PerspectiveCamera( >  75, >  window.innerWidth / window.innerHeight, >  0.1, >  10 // 将远端面设置的和相机的位置一样,就可以看到粒子都是往下运动的 > ); > ​ > // 设置相机位置 > camera.position.set(0, 0, 10); > ``` > > ![1721895058035.png] ### 6-5 运用数学知识打造复杂形状臂旋星系01 - 目标:给x轴上面设置点 ``` // 创建纹理加载器并加载纹理 const textureLoader = new THREE.TextureLoader(); const texture = textureLoader.load("./textures/particles/1.png"); ​ // 设置参数 const params = {  count: 100,  size: 0.1,  radius: 5,  branch: 3,  color: "#ffffff", }; ​ let geometry = null; let material = null; let points = null; // 生成点的函数 const generateGalaxy = () => {  // 生成顶点  geometry = new THREE.BufferGeometry();  // 随机生成位置  const positions = new Float32Array(params.count * 3);  // 设置顶点颜色  const colors = new Float32Array(params.count * 3);  // 循环生成顶点(count个顶点,只给x设置值,y和z应该是0)  for (let i = 0; i < params.count; i++) {    const current = i * 3;    positions[current] = Math.random() * params.radius;    positions[current + 1] = 0;    positions[current + 2] = 0; }  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); ​  // 设置点参数  material = new THREE.PointsMaterial({    color: new THREE.Color(params.color),    size: params.size,    sizeAttenuation: true,    depthWrite: false,    blending: THREE.AdditiveBlending,    map: texture,    alphaMap: texture,    transparent: true,    // vertexColors: true, });  points = new THREE.Points(geometry, material);  scene.add(points); }; generateGalaxy(); ``` ![1721896723737.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b8cafffbb97441a49adc2390da320a6c~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=Nag%2BwR9dJYUR6Zf324N%2BUyZ1SNc%3D) ### 6-6 运用数学知识打造复杂形状臂旋星系02 - 目标:总共设置三个分支,在三个分支都生成点 ``` // 循环生成顶点 for (let i = 0; i < params.count; i++) {    // 当前的点应该在哪一条分支的角度上(分支值 * 两个分支之间的角度)    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    // 当前点距圆心的位置    const distance = Math.random() * params.radius;    const current = i * 3;    positions[current] = Math.cos(branchAngle) * distance;    positions[current + 1] = 0;    positions[current + 2] = Math.sin(branchAngle) * distance; } ``` > 将设置的count数更改为1000,则有以下效果 ![1721897627075.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/78efdde3fef04720bc682605eac6884b~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=7WzAkOe5nx1uVQ8o2NuiG6AbCFM%3D) ### 6-7 运用数学知识打造复杂形状臂旋星系03 - 目标1:为三个分支设置一定的轨迹 ``` const params = {  count: 1000,  size: 0.1,  radius: 5,  branch: 3,  color: "#ffffff",  rotateScale: 0.3, // 设置点的旋转角度 }; ​ // 循环生成顶点 for (let i = 0; i < params.count; i++) {    // 当前的点应该在哪一条分支的角度上    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    // 当前点距圆心的位置    const distance = Math.random() * params.radius;    const current = i * 3;    // 为每一个点的x, y, z分别加上一个[0-1]的随机数    const randomX = Math.random();    const randomY = Math.random();    const randomZ = Math.random(); ​    // 让近原点的点旋转角度少一点,远原点的点旋转角度多一点,再加上一点偏移    positions[current] =        Math.cos(branchAngle + distance * params.rotateScale) * distance +        randomX;    positions[current + 1] = 0 + randomY;    positions[current + 2] =        Math.sin(branchAngle + distance * params.rotateScale) * distance +        randomZ; } ``` > 这个时候的相机远面端要大于相机所在位置,否则看不全整个图像 > > ``` > const camera = new THREE.PerspectiveCamera( >  75, >  window.innerWidth / window.innerHeight, >  0.1, >  20 // 值要大于相机所在位置的值 > ); > ​ > camera.position.set(0, 0, 10); > ``` ![1721899240165.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/53a89c494ca240409eb81acc071817b7~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=reWgmN7YRK9M6EU9vQRCPFFiDxw%3D) - 目标2:将各个分支上的点集中一下 ``` // 先将点的数量增加至10000 ​ for (let i = 0; i < params.count; i++) {    // 当前的点应该在哪一条分支的角度上    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    // 当前点距圆心的位置    const distance = Math.random() * params.radius;    const current = i * 3; ​    // 因为x, y, z值有正有负,因此用的是三次方    const randomX = Math.pow(Math.random() * 2 - 1, 3); // -1~1    const randomY = Math.pow(Math.random() * 2 - 1, 3);    const randomZ = Math.pow(Math.random() * 2 - 1, 3); ​    positions[current] =        Math.cos(branchAngle + distance * params.rotateScale) * distance +        randomX;    positions[current + 1] = 0 + randomY;    positions[current + 2] =        Math.sin(branchAngle + distance * params.rotateScale) * distance +        randomZ; } ``` ![1721900109096.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/28eec65e56a64ff3b77a2eaad4de74ec~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=d%2BPcoz%2BKTjlQwAJN9cMoHt0gqho%3D) - 目标3:将集中点后的分支上的点设置为——靠近原点位置多,远离原点位置少 ``` for (let i = 0; i < params.count; i++) {    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    // 当前点距圆心的位置(将此处乘的半径在进行一次[0-1]的随机)    const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);    const current = i * 3; ​    const randomX = Math.pow(Math.random() * 2 - 1, 3); // -1~1    const randomY = Math.pow(Math.random() * 2 - 1, 3);    const randomZ = Math.pow(Math.random() * 2 - 1, 3); ​    positions[current] =        Math.cos(branchAngle + distance * params.rotateScale) * distance +        randomX;    positions[current + 1] = 0 + randomY;    positions[current + 2] =        Math.sin(branchAngle + distance * params.rotateScale) * distance +        randomZ; } ``` ![1721900477189.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0d27d35f23494cf3a33a65b95275de20~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=eUuJJ1A7m2tAmErRBVkqGrXw9H0%3D) ![1721900488736.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2e670b0632b249ecbfa87f286f4a5cfc~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=apuJ5oaCcVQRlESihwY%2BRueot70%3D) - 目标4:现在上下是平的,上下也做成靠近中间的点多,其它位置少 ``` for (let i = 0; i < params.count; i++) {    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    // 当前点距圆心的位置    const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);    const current = i * 3;    // 给每一个点加的随机数乘以(半径减去点距圆心的距离),再除以5——离得越远的点半径减去点距圆心的距离越短,即x, y, z偏移的越少,就形成了集中的效果    const randomX =         (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;    const randomY =         (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;    const randomZ =         (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5; ​    positions[current] =        Math.cos(branchAngle + distance * params.rotateScale) * distance +        randomX;    positions[current + 1] = 0 + randomY;    positions[current + 2] =        Math.sin(branchAngle + distance * params.rotateScale) * distance +        randomZ; } ``` ![1721900968631.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1c63b07a429a4724bb266d0006494f0c~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=r6ZQ2gkbB1evG1JHujqpJspdAkU%3D) ![1721901000392.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2d40580b44404cdb9701b1fd38dc9195~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=aqLVQMG06VldkesMa4neQ6vRF2w%3D) - 可以设置分支个数 ``` // 设置参数 const params = {  count: 10000,  size: 0.1,  radius: 5,  branch: 6, // 设置分支为6个  color: "#ffffff",  rotateScale: 0.3, }; ``` ![1721901766590.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/fb0f13893d6b429cbd87502bef0d1f90~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=VkJev9mXnrlBSW5GIR4U%2BsXltFc%3D) ### 6-8 运用颜色收敛方法设置星系臂旋渐变 - 目标:给星系上色,从中间向四周扩散,中间颜色亮,越往外颜色越暗 - lerp(color: Color, alpha: Float): Color color - 用于收敛的颜色 alpha - 介于0-1的数字 - 生成目标效果 ``` // 设置颜色和结束颜色 const params = {  count: 10000,  size: 0.1,  radius: 5,  branch: 3,  color: "#ff6030", // 颜色:红色  rotateScale: 0.3,  endColor: "#1b3894", // 结束颜色:蓝色 }; ​ // 创建颜色对象 const centerColor = new THREE.Color(params.color); const endColor = new THREE.Color(params.endColor); ​ const generateGalaxy = () => {  geometry = new THREE.BufferGeometry();  const positions = new Float32Array(params.count * 3);  const colors = new Float32Array(params.count * 3);  for (let i = 0; i < params.count; i++) {    const branchAngle = (i % params.branch) * ((2 * Math.PI) / params.branch);    const distance = Math.random() * params.radius * Math.pow(Math.random(), 3);    const current = i * 3; ​    const randomX =     (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;    const randomY =     (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5;    const randomZ =     (Math.pow(Math.random() * 2 - 1, 3) * (params.radius - distance)) / 5; ​    positions[current] =      Math.cos(branchAngle + distance * params.rotateScale) * distance +      randomX;    positions[current + 1] = 0 + randomY;    positions[current + 2] =      Math.sin(branchAngle + distance * params.rotateScale) * distance +      randomZ; ​    // 生成混合颜色,形成渐变色——先克隆颜色,再和结束颜色进行混合,最后设置点的rgb颜色值    const mixColor = centerColor.clone();    mixColor.lerp(endColor, distance / params.radius);    colors[current] = mixColor.r;    colors[current + 1] = mixColor.g;    colors[current + 2] = mixColor.b; }  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));  // 将生成的颜色设置给缓冲区  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); ​  material = new THREE.PointsMaterial({    // color: new THREE.Color(params.color), // 将材质颜色给注掉,否则会影响混合颜色的显示    size: params.size,    sizeAttenuation: true,    depthWrite: false,    blending: THREE.AdditiveBlending,    map: texture,    alphaMap: texture,    transparent: true,    vertexColors: true, // 设置启用顶点颜色 });  points = new THREE.Points(geometry, material);  scene.add(points); }; ``` ![1721902972027.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/610529f4d52846a6b17d0b0d32de9aad~tplv-73owjymdk6-jj-mark:0:0:0:0:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzYzNTQ1NDcwMjAwMjc0NyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1723191516&x-orig-sign=R4o9T%2FmcQKawjTvAMr6ndcPHuqw%3D)