前言:在刚开始学习前端的时候,我们会学习到前端三件套中的JavaScript,可能那时候读者没有觉得JavaScript这个语言有多么的牛逼,本篇文章将会使用一个炫酷的案例来刷新你对JavaScript这个语言的认知与理解。
✨✨✨这里是秋刀鱼不做梦的BLOG
✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客
先让我们看一下最终的结果:
在开始讲解这个炫酷的案例之前,先让我们了解一下本案例所需的前置知识:
Three.js:一个用于创建和显示3D图形的JavaScript库。代码中导入了Three.js的核心库和轨道控制库(OrbitControls),用于处理3D场景的创建和相机控制。
WebGL:用于在网页中绘制3D图形的底层API。Three.js封装了WebGL,使得3D渲染变得更简单。
模块化 JavaScript:使用 ES6 的模块导入语法 (
import
) 引入外部库,使代码结构更加清晰。着色器编程:自定义顶点和片段着色器,通过
onBeforeCompile
方法替换默认着色器,控制点的大小、颜色和运动效果。缓冲几何体:使用
BufferGeometry
来管理大量点的性能,提升渲染效率。动画循环:利用
setAnimationLoop
实现流畅的渲染动画,每帧更新控制器状态和场景渲染。响应式设计:通过
resize
事件监听器,动态调整相机和渲染器的尺寸,以适应浏览器窗口的变化。
——在文章的最后,我们给出了实现这个案例的全部代码,读者可以直接复制后在自己的编译器上执行一下!!!
那么现在正式开始我们的讲解:
目录
1.导入库和清理控制台
开始我们要先导入库和清理控制台:
import * as THREE from "https://cdn.skypack.dev/[email protected]"; // 导入三维模型库
import { OrbitControls } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls"; // 导入轨道控制库
console.clear(); // 清除控制台
注释:这部分代码导入了Three.js库及其轨道控制功能,并清理了控制台,以便后续输出信息更清晰。
2.创建场景和相机
在导入库和清理控制台后,我们就需要创建场景和相机:
let scene = new THREE.Scene(); // 创建场景
scene.background = new THREE.Color(0x160016); // 设置场景背景颜色
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000); // 创建相机
camera.position.set(0, 4, 21); // 设置相机位置
注释:这部分代码创建了一个场景和一个透视相机,设置了相机的位置和背景颜色,为后续的渲染准备基础环境。
3.创建渲染器
其后我们需要为其创建一个渲染器:
let renderer = new THREE.WebGLRenderer(); // 创建渲染器
renderer.setSize(innerWidth, innerHeight); // 设置渲染器大小
document.body.appendChild(renderer.domElement); // 把渲染器加入到页面中
注释:这里创建了一个WebGL渲染器,并设置其大小为窗口的宽高,最后将渲染器的DOM元素添加到网页中,以显示渲染结果。
4. 处理窗口大小变化
在创建完渲染器之后,我们需要对后续的窗口的大小的变化进行处理:
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight; // 更新相机的长宽比
camera.updateProjectionMatrix(); // 更新相机的投影矩阵
renderer.setSize(innerWidth, innerHeight); // 设置渲染器大小
});
注释:这一部分代码监听窗口的大小变化,动态调整相机的长宽比和渲染器的大小,确保在窗口大小变化时,场景仍能正确显示。
5. 控制器设置
处理完窗口大小的变化之后,我们就需要对控制器进行设置了!
let controls = new OrbitControls(camera, renderer.domElement); // 创建控制器
controls.enableDamping = true; // 开启阻尼效果
controls.enablePan; // 禁用平移(此行未完成)
注释:这部分代码创建了一个轨道控制器,允许用户通过鼠标控制相机旋转和缩放,增强用户交互体验。注意,这里 enablePan
需要完整设置。
6. 创建全局uniform变量和点的属性
接下来让哦我们对全局的uniform变量和点的属性进行设置:
let gu = {
time: { value: 0 }
}; // 创建全局uniform变量
let sizes = []; // 点的大小数组
let shift = []; // 点的移动数组
let pushShift = () => { // 创建移动函数
shift.push(
Math.random() * Math.PI,
Math.random() * Math.PI * 2,
(Math.random() * 0.9 + 0.1) * Math.PI * 0.1,
Math.random() * 0.9 + 0.1
); // 随机生成位置信息
};
注释:定义了一个全局uniform变量 gu
用于时间控制,并初始化了两个数组 sizes
和 shift
用于存储点的大小和位置信息。同时,定义了一个函数 pushShift
,用于生成随机的位移数据。
7. 创建点的顶点数组
接下来让我们创建点的顶点数组:
let pts = new Array(50000).fill().map(p => {
sizes.push(Math.random() * 1.5 + 0.5); // 添加随机大小
pushShift(); // 添加位置信息
return new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * 0.5 + 9.5); // 返回随机方向的点
});
注释:这里创建了一个包含5万个点的数组 pts
,每个点都有随机的大小和方向。通过 randomDirection()
方法生成随机方向的向量,模拟球体内的点。
8. 添加围绕的点
创建完点的顶点数组之后,让我们添加其围绕的点:
for (let i = 0; i < 100000; i++) {
let r = 10, R = 40; // 定义内外半径
let rand = Math.pow(Math.random(), 1.5); // 生成随机数
let radius = Math.sqrt(R * R * rand + (1 - rand) * r * r); // 计算随机半径
pts.push(new THREE.Vector3().setFromCylindricalCoords(radius, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 2)); // 生成点
sizes.push(Math.random() * 1.5 + 0.5); // 添加随机大小
pushShift(); // 添加位置信息
}
注释:这部分代码在已有的点基础上,再添加10万个点。使用极坐标系生成点,使其在一定范围内均匀分布,丰富了场景中的点的数量和分布。
9. 创建几何体和材质
接下来让我们设置一下几何体和其材质样式:
let g = new THREE.BufferGeometry().setFromPoints(pts); // 创建几何体
g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1)); // 设置sizes属性
g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4)); // 设置shift属性
let m = new THREE.PointsMaterial({ // 创建点材质
size: 0.125, // 点的大小
transparent: true, // 透明材质
depthTest: false, // 禁用深度测试
blending: THREE.AdditiveBlending, // 添加混合模式
onBeforeCompile: shader => { // 自定义着色器
shader.uniforms.time = gu.time; // 设置uniform变量
shader.vertexShader = `
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`; // 修改顶点着色器
// 更新点的大小、颜色和移动逻辑
shader.vertexShader = shader.vertexShader
.replace(`gl_PointSize = size;`, `gl_PointSize = size * sizes;`)
.replace(`#include <color_vertex>`, `#include <color_vertex> float d = length(abs(position)/vec3(40.,10.,40)); d=clamp(d,0.,1.); vColor = mix(vec3(227.,155.,0.),vec3(100.,50.,255.),d)/255.;`)
.replace(`#include <begin_vertex>`, `#include <begin_vertex> float t = time; float moveT = mod(shift.x + shift.z * t,PI2); float moveS = mod(shift.y + shift.z * t,PI2); transformed += vec3(cos(moveS) * sin(moveT), cos(moveT), sin(moveS) * sin(moveT)) * shift.w;`);
// 修改片元着色器
shader.fragmentShader = `
varying vec3 vColor;
${shader.fragmentShader}
`.replace(`#include <clipping_planes_fragment>`, `#include <clipping_planes_fragment> float d = length(gl_PointCoord.xy - 0.5);`)
.replace(`vec4 diffuseColor = vec4( diffuse, opacity );`, `vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d));`);
}
});
注释:在这部分代码中,创建了一个缓冲几何体并设置了点的大小和移动信息。还定义了一个点材质,使用自定义着色器来控制点的大小、颜色和移动效果,提供了动态的视觉效果。
10. 创建点云并添加到场景
接下来让我们创建点云并添加到场景:
let p = new THREE.Points(g, m); // 创建点云
p.rotation.order = "ZYX"; // 设置旋转顺序
p.rotation.z = 0.2; // 设置初始旋转角度
scene.add(p); // 将点云添加到场景中
注释:这里将创建的几何体和材质结合成一个点云对象,并设置其旋转顺序和初始旋转角度,最后将其添加到场景中以进行渲染。
11. 渲染循环
最后让我们渲染一下环境:
let clock = new THREE.Clock(); // 创建时钟对象
renderer.setAnimationLoop(() => { // 设置渲染循环
controls.update(); // 更新控制器
let t = clock.getElapsedTime() * 0.5; // 获取流逝时间
gu.time.value = t * Math.PI; // 更新时间
p.rotation.y = t * 0.05; // 更新点云的旋转角度
renderer.render(scene, camera); // 渲染场景和相机
});
注释:最后,这段代码设置了渲染循环,利用时钟对象控制动画的时间,并不断更新控制器和点云的旋转,最终渲染场景。
通过上述的分层讲解之后,我们就大致的了解每一步都是如何操作并且为什么这么操作的了,为了使读者能更好的理解上述流程,这里我们在进行总结一下:
导入库:
使用Three.js库和OrbitControls模块,准备进行三维场景的创建和交互。
场景和相机设置:
创建一个三维场景并设置背景颜色。
创建透视相机,设定相机的位置,准备从特定角度观察场景。
渲染器初始化:
创建WebGL渲染器,设置其大小与窗口相同,并将其添加到网页中以显示内容。
窗口大小自适应:
添加事件监听器,以确保在窗口大小变化时,自动调整相机的长宽比和渲染器的大小,保持渲染效果。
控制器设置:
创建轨道控制器,允许用户通过鼠标控制相机的旋转和缩放,增强用户交互体验。
全局变量和点属性初始化:
定义用于控制动画的全局变量和点的大小、位移数组,准备生成点的运动效果。
点的生成:
生成大量随机位置的点,包括中心球体内的点和周围分布的点,以增加视觉复杂度。
几何体和材质设置:
创建缓冲几何体,设置点的大小和位移信息。
定义点的材质,使用自定义着色器来控制点的大小、颜色和移动效果。
点云创建和添加到场景:
将几何体和材质组合成点云对象,并设置初始旋转,最后将其添加到场景中以进行渲染。
渲染循环:
使用时钟对象进行动画控制,在每一帧中更新控制器、点云的旋转和动画时间,并渲染场景。
——至此,我们就讲解完了上述案例的所以步骤了!!!
——全部代码
最后在让我们看一下最终的效果,(JavaScript没有极限!!!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>秋刀鱼不做梦</title>
<style>
body {
overflow: hidden;
margin: 0;
}
</style>
</head>
<body>
<script type="module">
// 导入三维模型库
import * as THREE from "https://cdn.skypack.dev/[email protected]";
// 导入轨道控制库
import { OrbitControls } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls";
// 清除控制台
console.clear()
// 创建场景
let scene = new THREE.Scene()
// 设置场景背景颜色
scene.background = new THREE.Color(0x160016)
// 创建相机
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000)
// 设置相机位置
camera.position.set(0, 4, 21)
// 创建渲染器
let renderer = new THREE.WebGLRenderer()
// 设置渲染器大小
renderer.setSize(innerWidth, innerHeight)
// 把渲染器加入到页面中
document.body.appendChild(renderer.domElement)
// 监听窗口大小变化事件
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight
camera.updateProjectionMatrix()
renderer.setSize(innerWidth, innerHeight)
})
// 创建控制器
let controls = new OrbitControls(camera, renderer.domElement)
// 开启阻尼效果
controls.enableDamping = true
// 禁用面板
controls.enablePan
// 创建全局uniform
let gu = {
time: { value: 0 }
}
// 创建点的大小数组和移动数组
let sizes = []
let shift = []
// 创建移动函数
let pushShift = () => {
shift.push(
Math.random() * Math.PI,
Math.random() * Math.PI * 2,
(Math.random() * 0.9 + 0.1) * Math.PI * 0.1,
Math.random() * 0.9 + 0.1
)
}
// 创建点的顶点数组(中间的球体)
// 创建一个长度为5万的数组pts并y用Array.prototype.map()方法遍历数组并对每个元素进行操作
let pts = new Array(50000).fill().map(p => {
// 每次遍历中,会向sizes数组中添加一个随机大小
sizes.push(Math.random() * 1.5 + 0.5)
// 调用pushShift()函数添加位置信息,并返回一个随机方向的THREE.Vector对象
pushShift()
return new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * 0.5 + 9.5)
//
})
// 添加更多的点(旁边围绕的)
// 先循环生成十万个点
// 每次循环中生成一个随机数rand,再生成一个随机半径radius
for (let i = 0; i < 100000; i++) {
let r = 10, R = 40;
let rand = Math.pow(Math.random(), 1.5);
let radius = Math.sqrt(R * R * rand + (1 - rand) * r * r);
// 使用new THREE.Vector3().setFromCylindricalCoords()生成一个点。
pts.push(new THREE.Vector3().setFromCylindricalCoords(radius, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 2));
sizes.push(Math.random() * 1.5 + 0.5);
pushShift();
}
// 生成一个点g,同时将点的大小和位置信息添加到sizes和shift数组中
let g = new THREE.BufferGeometry().setFromPoints(pts)
// 创建了一个缓冲几何体并设置sizes和shift属性
// 注意这里的F要大写Float32BufferAttribute
g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1))
g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4))
// 创建点材质
let m = new THREE.PointsMaterial({
// 表示点的大小
size: 0.125,
// 设置材质为透明
transparent: true,
// 表示禁用深度测试,使点可以叠加
depthTest: false,
// 使用假发混合模式
blending: THREE.AdditiveBlending,
// 在材质编译之前修改颜色器,在这里,它用来替换顶点着色器和片元着色器,添加uniform
// 和attribute,以鸡自定义颜色和移动
onBeforeCompile: shader => {
shader.uniforms.time = gu.time
// 首先,它为着色器设置了一个uniform变量time,该变量是在点材质中定义的,用来追踪时间
// 然后它定义了两个attribute变量sizes和shift,这两个变量是在缓冲几何体中定义的,用来控制点的大小和移动
// 最后使用replace方法来替换顶点着色器中的代码
shader.vertexShader = `
uniform float time;
attribute float sizes;
attribute vec4 shift;
varying vec3 vColor;
${shader.vertexShader}
`
// 注意上面的 ` 不要漏掉了
// 使用replace来替换着色器中的代码
// 更新点的大小
.replace(
`gl_PointSize = size;`,
`gl_PointSize = size * sizes;`
)
// 更新点的颜色
.replace(
`#include <color_vertex>`,
`#include <color_vertex>
float d = length(abs(position)/vec3(40.,10.,40));
d=clamp(d,0.,1.);
vColor = mix(vec3(227.,155.,0.),vec3(100.,50.,255.),d)/255.;`
)
// 记得加上分号
// 更新点的移动
.replace(
`#include <begin_vertex>`,
`#include <begin_vertex>
float t = time;
float moveT = mod(shift.x + shift.z * t,PI2);
float moveS = mod(shift.y + shift.z * t,PI2);
transformed += vec3(cos(moveS) * sin(moveT),cos(moveT),sin(moveS)*sin(moveT)) * shift.w;
`
)
// 修改片元着色器,用来让点的边缘更加圆滑
// 首先,定义一个varying变量vColor,这个变量是在顶点着色器中定义的,用来传递点的颜色到片段着色器
// 然后使用replace方法来替换片段着色器的代码
shader.fragmentShader = `
varying vec3 vColor;
${shader.fragmentShader}
`.replace(
`#include <clipping_planes_fragment>`,
`#include <clipping_planes_fragment>
float d = length(gl_PointCoord.xy - 0.5);
`
).replace(
// 记得加上空格
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d)/* * 0.5+0.5*/);`
);
}
})
// 创建点云并将其添加到场景中,并设置渲染循环
let p = new THREE.Points(g, m)
// 旋转顺序为"ZYX"
p.rotation.order = "ZYX"
// 旋转角度 0.2
p.rotation.z = 0.2
// 把对象(p)添加到场景(scene)中
scene.add(p)
// 创建一个时钟对象clock
let clock = new THREE.Clock()
// 渲染循环,每次循环中会更新控制器,更新p的旋转角度,更新时间
renderer.setAnimationLoop(() => {
// 更新控制器
controls.update()
// 获取时钟对象(clock)的已经流逝的时间(t)并将他乘0.5
// 先把时钟关了
let t = clock.getElapsedTime() * 0.5
// 将gu.time.value 设置为t*Math.PI
gu.time.value = t * Math.PI
// 将对象(p)的旋转角度y设置为t*0.05
p.rotation.y = t * 0.05
// 渲染场景(scene)和相机(camera)
renderer.render(scene, camera)
})
</script>
</body>
</html>
以上就是本篇文章的全部内容了!!!
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/2302_80198073/article/details/143473333