滴水石穿,读百篇尽显敏而好学。在写这篇文章之前,大傻在此声明一下,文章的素材来源是B站Up主老陈打码的课程,在此,大傻将自己编码时候的心得做记录,以供大家参考。
前期准备
初始化目录
首先是目录的准备,简单介绍下,首先创建一个基础文件夹,文件夹下有src以及package.Json。src的目录结构依照上述图片所示,接下来首先是style.css。
基础代码填充
* {
margin: 0;
padding: 0;
}
body {
background-color: #1e1a20;
}
::-webkit-scrollbar {
display: none;
}
然后是packageJson
{
"name": "three-js-flyLight"
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@parcel/transformer-glsl": "2.8.3",
"parcel": "^2.4.1"
},
"dependencies": {
"gsap": "^3.11.4",
"three": "^0.139.2"
}
}
最后是我们的顶点着色器和片元着色器
顶点着色器:
// vertex.glsl
precision lowp float;
void main(){
gl_Position=projectionMatrix*viewMatrix*modelMatrix*vec4(position,1.);
}
片元着色器:
// fragment.glsl
precision lowp float;
void main(){
gl_FragColor=vec4(1.,1.,0.,1.);
}
完成引用并安装依赖
在此介绍下,我们使用gsap库进行动画效果的实现,使用parcel进行打包以及快速预览,使用ThreeJs库进行整体的开发。在完成目录创建后,首先我们在index.html中完成对css以及mainJs的引用。
然后我们通过 yarn 或者 npm 完成依赖安装,至此我们的前期准备也算完成了。
中期Three模板开启
初始化mainJs文件
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import vertexShader from "../shader/flylight/vertex.glsl";
import fragmentShader from "../shader/flylight/fragment.glsl";
// 初始化场景
const scene = new THREE.Scene();
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerHeight / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 2);
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
scene.add(camera);
// 加入辅助轴,帮助我们查看3维坐标轴
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 创建着色器材质;
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {},
side: THREE.DoubleSide,
// transparent: true,
});
// 创建平面
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1, 64, 64),
shaderMaterial
);
scene.add(plane);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 将渲染器添加到body
document.body.appendChild(renderer.domElement);
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;
function animate(t) {
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}
animate();
接下来,我们通过命令行,npm run dev 来运行我们的项目,如果配置正常的话,我们页面应该会显示一个如下图所示的页面
至此,我们的初始化阶段已经全部完成,加下来就愉快的进入我们真正的开发阶段了
后期开发阶段
我们期望的效果
(屏幕截的gif有点卡顿)
基于我们的效果,我们来思考下我们需要做什么内容:
- 代码里面引入孔明灯素材
- 代码引入背景素材
- 对孔明灯的UI进行调整
- 孔明灯的移动动画
开始 引入场景
首先我们先引入背景素材,
- 链接:pan.baidu.com/s/1HznTdhwq… 提取码:nteh
我们的背景素材是HDR的高清图,因此我们需要在JS代码中引入纹理加载函数
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync("./assets/2k.hdr").then((texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
})
此处解释下,我们打包后生成了dist目录,因此我们没必要对资源进行打包,所以如下所示
我们直接在dist目录下创建了asset静态资源,并把文件放在该目录下。
当我们引入场景素材后发现
我们确实加载出来了场景,但是和效果图上明显的有一定的差异,这又是为什么呢,分析:差异点在于这个月光的显示,我们引入后曝光度明显太高。那么我们该如何解决呢?
首先是
renderer.outputEncoding = THREE.sRGBEncoding;
这里就不过多的解释属性的意义,感兴趣大家可以根据官网的介绍来进行测试
当加完这条后,我们的图像明显更加清晰了
这时候我们仍然需要对渲染器进行一些处理
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.2;
至此我们已经完成了场景的初始化,接下来就是孔明灯的细节
引入孔明灯
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const gltfLoader = new GLTFLoader();
let LightBox = null;
gltfLoader.load("./assets/model/flyLight.glb", (gltf) => {
console.log(gltf);
LightBox = gltf.scene.children[1];
LightBox.material = shaderMaterial;
scene.add(LightBox)
});
接下来删除我们之前创建的平面的占位元素,我们就得到了这样的效果
当然这里面是对着色器进行了一些编写,首先是顶点着色器
顶点着色器
precision lowp float;
varying vec4 vPosition;
varying vec4 gPosition;
void main(){
vec4 modelPosition=modelMatrix*vec4(position,1.);
vPosition=modelPosition;
gPosition=vec4(position,1.);
gl_Position=projectionMatrix*viewMatrix*modelPosition;
}
每行代码解释:
- precision lowp float; 这句代码代表我们渲染此次使用的是低精度(即通过precision关键字可以批量声明一些变量精度)
- varying vec4 vPosition; varying vec4 gPosition; 这两句代码是我们分别声明了 两个vec4 的变量,其中vec4代表的就是有四个数字的向量如 vec4(1.,1.,1.,1.),在glsl中通常用浮点数表示
- 在main函数中 首先modelMatrix 以及 viewMatrix 和 projectionMatrix 这些都是默认着色器提供给我们的常量(模型矩阵、视图矩阵以及投影矩阵),最终我们通过运算可以拿到 顶点位置以及视图位置。接下来我们讲这些位置传入我们的片元着色器进行着色
片元着色器
precision lowp float;
varying vec4 vPosition;
varying vec4 gPosition;
void main(){
vec4 redColor=vec4(1.,0.,0.,1.);
vec4 yellowColor=vec4(1.,1.,0.,1.);
vec4 minxColor=mix(yellowColor,redColor,gPosition.y/3.);
if(gl_FrontFacing){
gl_FragColor=vec4(minxColor.xyz-vPosition.y/100.-.1,1.);
}else{
gl_FragColor=vec4(minxColor.xyz,1.);
}
}
每行代码解释:
- precision lowp float; 这句代码代表我们渲染此次使用的是低精度(即通过precision关键字可以批量声明一些变量精度)
- varying vec4 vPosition; varying vec4 gPosition; 这两句代码是我们分别引用了 两个vec4 的变量(这里的变量是我们在顶点着色器中创建的),其中vec4代表的就是有四个数字的向量如 vec4(1.,1.,1.,1.),在glsl中通常用浮点数表示
- 在main函数中 我们首先声明了两个基础色 红色和黄色 接下来我们对这两个颜色进行mix混合,混合后我们将颜色注入到我们的着色变量上,即gl_FragColor。而gl_FrontFacing将会告诉我们当前片段是属于正向面的一部分还是背向面的一部分。通过这个属性我们能限制孔明灯内外颜色的变化。比如:里面暗一些,外面亮一些。
至此为止我们已经完成了一个孔明灯的创建,那么怎么做多个呢,当然是循环了。
for (let i = 0; i < 150; i++) {
let flyLight = gltf.scene.clone(true);
let x = (Math.random() - 0.5) * 300;
let z = (Math.random() - 0.5) * 300;
let y = Math.random() * 60 + 25;
flyLight.position.set(x, y, z);
gsap.to(flyLight.rotation, {
y: 2 * Math.PI,
duration: 10 + Math.random() * 30,
repeat: -1,
});
gsap.to(flyLight.position, {
x: "+=" + Math.random() * 5,
y: "+=" + Math.random() * 20,
yoyo: true,
duration: 5 + Math.random() * 10,
repeat: -1,
});
scene.add(flyLight);
}
代码分析:
- 首先我们通过gltf的场景克隆,拿到孔明灯对象,接下来为孔明灯对象随机设置他在3D空间中的位置,最后我们通过gsap库进行动画渲染
- 首先让孔明灯有个自转的效果,其中y为方向、duration为持续时间、repeat为是否重复
- 其次我们让孔明灯有个上下运动的动作,这里x、y都用了+= 因为我们刚刚已经设置好了孔明灯的位置,所以在此处我们做的就是不覆盖刚才位置的前提下让孔明灯油新的运动
- 最后将我们批量生成的孔明灯加入到我们的场景里面,大功告成!
完整代码如下
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import vertexShader from "../shader/flylight/vertex.glsl";
import fragmentShader from "../shader/flylight/fragment.glsl";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
// 初始化场景
const scene = new THREE.Scene();
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerHeight / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 2);
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
scene.add(camera);
// 加入辅助轴,帮助我们查看3维坐标轴
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync("./assets/2k.hdr").then((texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
});
const gltfLoader = new GLTFLoader();
let LightBox = null;
gltfLoader.load("./assets/model/flyLight.glb", (gltf) => {
console.log(gltf);
LightBox = gltf.scene.children[1];
LightBox.material = shaderMaterial;
for (let i = 0; i < 150; i++) {
let flyLight = gltf.scene.clone(true);
let x = (Math.random() - 0.5) * 300;
let z = (Math.random() - 0.5) * 300;
let y = Math.random() * 60 + 25;
flyLight.position.set(x, y, z);
gsap.to(flyLight.rotation, {
y: 2 * Math.PI,
duration: 10 + Math.random() * 30,
repeat: -1,
});
gsap.to(flyLight.position, {
x: "+=" + Math.random() * 5,
y: "+=" + Math.random() * 20,
yoyo: true,
duration: 5 + Math.random() * 10,
repeat: -1,
});
scene.add(flyLight);
}
});
// 创建着色器材质;
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {},
side: THREE.DoubleSide,
// transparent: true,
});
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMapping = THREE.LinearToneMapping;
// renderer.toneMapping = THREE.ReinhardToneMapping;
// renderer.toneMapping = THREE.CineonToneMapping;
renderer.toneMappingExposure = 0.2;
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 将渲染器添加到body
document.body.appendChild(renderer.domElement);
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;
function animate(t) {
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}
animate();
感谢阅读,如果觉得本篇文章对您有帮助,欢迎一键三连,如果有什么做的不当的地方,欢迎评论区讨论