效果:



代码:
xml
<template>
<div>
<!-- other页面
<button @click="goHome">返回首页</button> -->
<div class='shadow_page'>
<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
<div id="loading-text-intro"><p>Loading</p></div>
<div class="content" style="visibility: hidden">
<nav class="header">
<a href="/home" class="active a"><span>首页</span></a>
<a href="/login" class="a"><span>登录</span></a>
<a href="" class="a"><span>作品</span></a>
<a href="" class="a"><span>我的</span></a>
<a href="" class="a"><span>更多</span></a>
<div class="cursor"></div>
</nav>
<section class="section first">
<div class='info'>
<h2 class='name'>DRAGONIR</h2>
<h1 class='title'>THREE.JS ODESSEY</h1>
<p class='description'> </p>
</div>
<canvas id='canvas-container' class='webgl'></canvas>
</section>
<section class="section second">
<div class="second-container">
<ul>
<li id="one" class="active">入门</li>
<li id="two">基础</li>
<li id="three">进阶</li>
</ul>
<p class="text" id="content">昨夜西风凋碧树。独上高楼,望尽天涯路。</p>
</div>
<canvas id='canvas-container-details' class='webgl'></canvas>
</section>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import * as THREE from 'three'
import { Clock, Scene, LoadingManager, WebGLRenderer, sRGBEncoding, Group, PerspectiveCamera, DirectionalLight, PointLight, MeshPhongMaterial } from 'three';
import { default as Tween } from 'three/examples/jsm/libs/tween.module.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; // 加载用blender压缩过的模型
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; // 加载和解析 GLTF(.glb/.gltf)文件
const router = useRouter()
const goHome = ()=>{
router.push('/home')
}
onMounted(()=>{
// 定义渲染尺寸
const section = document.getElementsByClassName('section')[0];
let oldMaterial;
let width = section.clientWidth;
let height = section.clientHeight;
// 初始化渲染器
const renderer = new WebGLRenderer({
canvas: document.querySelector('#canvas-container'),
antialias: true, // 是否开启抗锯齿效果,减少图形边缘的锯齿状和模糊
alpha: true, //指示是否要使渲染的背景透明,以便后面的HTML元素可以显示在Three.js场景的背后
powerPreference: 'high-performance' // 指定WebGL渲染的性能偏好,以便根据设备的性能和电源需求进行优化
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.autoClear = true; // 指定在渲染场景之前是否自动清除渲染目标的颜色缓冲区和深度缓冲区。这个属性通常用于控制场景的渲染顺序和效果
renderer.outputEncoding = sRGBEncoding; //用于指定渲染的输出颜色空间。该属性影响了最终呈现到屏幕上的颜色的显示方式
const renderer2 = new WebGLRenderer({
canvas: document.querySelector('#canvas-container-details'),
antialias: false
});
renderer2.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer2.setSize(width, height);
renderer2.outputEncoding = sRGBEncoding;
// 初始化场景
const scene = new Scene();
// 初始化相机
const cameraGroup = new Group();
scene.add(cameraGroup);
const camera = new PerspectiveCamera(100, width / height, 1,1000)
camera.position.set(19, 1.54, -1);
cameraGroup.add(camera);
// scene.add(camera);
// 相机2
const camera2 = new PerspectiveCamera(100, width / height, 1, 100);
camera2.position.set(5, 8, 8);
camera2.rotation.set(0, 1, 0);
scene.add(camera2);
// 页面缩放事件监听
window.addEventListener('resize', () => {
let section = document.getElementsByClassName('section')[0];
camera.aspect = section.clientWidth / section.clientHeight
camera.updateProjectionMatrix(); // 更新相机的投影矩阵,以确保相机的视锥体和透视投影等参数在发生变化时能够正确地反映在渲染中
camera2.aspect = section.clientWidth / section.clientHeight;
camera2.updateProjectionMatrix();
renderer.setSize(section.clientWidth, section.clientHeight);
renderer2.setSize(section.clientWidth, section.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer2.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// 直射光
const directionLight = new DirectionalLight(0xffffff, .8);
directionLight.position.set(50, 0, 0);
scene.add(directionLight);
// 点光源
const fillLight = new PointLight(0xffffff,5, 100, 1);
// fillLight.position.set(0, 150, 0);
scene.add(fillLight);
// 加载管理
const ftsLoader = document.querySelector('.lds-roller'); // 旋转框
const loadingCover = document.getElementById('loading-text-intro');
const loadingManager = new LoadingManager();
loadingManager.onLoad = () => {
document.querySelector('.content').style.visibility = 'visible';
const yPosition = { y: 0 };
// 隐藏加载页面动画
new Tween.Tween(yPosition)
.to({ y: 100 }, 2000)
.easing(Tween.Easing.Quadratic.InOut)
.start()
.onUpdate(() => { loadingCover.style.setProperty('transform', `translate(0, ${yPosition.y}%)`) })
.onComplete(function () {
loadingCover.parentNode.removeChild(document.getElementById('loading-text-intro'));
Tween.remove(this);
});
// 使用Tween给相机添加入场动画
new Tween.Tween(
camera.position.set(0, 2, 25))
.to({ x: 0, y: 8, z: 10 }, 3500)
.easing(Tween.Easing.Quadratic.InOut)
.start()
.onComplete(function () {
Tween.remove(this);
document.querySelector('.header').classList.add('ended');
document.querySelector('.description').classList.add('ended');
});
// 移除加载旋转框
ftsLoader.parentNode.removeChild(ftsLoader);
window.scroll(0, 0)
}
// 使用 dracoLoader 加载用blender压缩过的模型
// const dracoLoader = new DRACOLoader();
// dracoLoader.setDecoderPath('/public/draco/gltf/');
// dracoLoader.setDecoderConfig({ type: 'js' });
const loader = new GLTFLoader(loadingManager);
// loader.setDRACOLoader(dracoLoader);
// 模型加载
const center = new THREE.Vector3()
loader.load('/public/models/snake_statue.glb', function (gltf) {
const axesHelper = new THREE.AxesHelper(20000) // 创建辅助轴 红:x,绿:y,蓝:z
const boundingBox = new THREE.Box3().setFromObject(gltf?.scene) // 根据对象的几何体或位置计算包围盒
boundingBox.getCenter(center)
let lightPosition = center.clone()
lightPosition.y +=5
fillLight.position.copy(lightPosition)
console.log('center',center.clone())
gltf.scene.traverse((obj) => { // 遍历场景中所有对象
if (obj.isMesh) {
oldMaterial = obj.material;
obj.material = new MeshPhongMaterial({ shininess: 100 });
obj.position.set(0,100,0)
}
});
gltf.scene.add(axesHelper)
scene.add(gltf.scene);
// 释放WebGl内存
oldMaterial.dispose();
renderer.renderLists.dispose();
});
// 鼠标移动时添加虚拟光标
const cursor = { x: 0, y: 0 };
const mouse = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
document.addEventListener('mousemove', event => {
event.preventDefault();
cursor.x = (event.clientX / window.innerWidth) * 2 - 1;
cursor.y = - (event.clientY / window.innerHeight) * 2 + 1;
document.querySelector('.cursor').style.cssText = `left: ${event.clientX}px; top: ${event.clientY}px;`;
}, false);
// 基于容器视图禁用渲染器
let secondContainer = false;
const ob = new IntersectionObserver(payload => {
secondContainer = payload[0].intersectionRatio > 0.05;
}, { threshold: 0.05 });
ob.observe(document.querySelector('.second'));
// 计算光源位置
const updateLightPosition = () => {
// 使用鼠标坐标和距离计算光源位置
const cameraPa = secondContainer ? camera2 : camera
const lightDistance = 5; // 光源距离摄像机的距离 深度值
const zParams = secondContainer ? 6 : 8
const vector = new THREE.Vector3(cursor.x, cursor.y, lightDistance);
vector.unproject(cameraPa);
const dir = vector.sub(cameraPa.position).normalize();
const distance = -cameraPa.position.z / dir.z + zParams;
const lightPosition = cameraPa.position.clone().add(dir.multiplyScalar(distance));
lightPosition.x +=1
fillLight.position.lerp(lightPosition,0.06);
};
// 页面重绘动画
const clock = new Clock()
let previousTime = 0;
const tick = () => {
console.log('secondContainer',secondContainer)
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - previousTime;
previousTime = elapsedTime;
const parallaxY = cursor.y;
const parallaxX = cursor.x
updateLightPosition()
cameraGroup.position.z -= (parallaxY / 3 + cameraGroup.position.z) * 2 * deltaTime;
cameraGroup.position.x += (parallaxX / 3 - cameraGroup.position.x) * 2 * deltaTime;
Tween.update();
secondContainer ? renderer2.render(scene, camera2) : renderer.render(scene, camera);
requestAnimationFrame(tick);
}
tick();
// 鼠标悬浮到菜单动画
const btn = document.querySelectorAll('nav > .a');
function update(e) {
const span = this.querySelector('span');
if (e.type === 'mouseleave') {
span.style.cssText = '';
} else {
const { offsetX: x, offsetY: y } = e;
const { offsetWidth: width, offsetHeight: height } = this;
const walk = 20;
const xWalk = (x / width) * (walk * 2) - walk, yWalk = (y / height) * (walk * 2) - walk;
span.style.cssText = `transform: translate(${xWalk}px, ${yWalk}px);`
}
}
btn.forEach(b => b.addEventListener('mousemove', update));
btn.forEach(b => b.addEventListener('mouseleave', update));
// 相机动画
function animateCamera(position, rotation) {
new Tween.Tween(camera2.position)
.to(position, 1500)
.easing(Tween.Easing.Quadratic.InOut)
.start()
.onComplete(function () {
Tween.remove(this)
})
new Tween.Tween(camera2.rotation)
.to(rotation, 1800)
.easing(Tween.Easing.Quadratic.InOut)
.start()
.onComplete(function () {
Tween.remove(this);
});
}
// 页面Tab点击事件监听
document.getElementById('one').addEventListener('click', () => {
document.getElementById('one').classList.add('active');
document.getElementById('three').classList.remove('active');
document.getElementById('two').classList.remove('active');
document.getElementById('content').innerHTML = '昨夜西风凋碧树。独上高楼,望尽天涯路。';
animateCamera({ x: 5, y: 8, z: 8 }, { y: 1 });
});
document.getElementById('two').addEventListener('click', () => {
document.getElementById('two').classList.add('active');
document.getElementById('one').classList.remove('active');
document.getElementById('three').classList.remove('active');
document.getElementById('content').innerHTML = '衣带渐宽终不悔,为伊消得人憔悴。';
animateCamera({ x: -1, y: 8, z: 10 }, { y: 0.5 });
});
document.getElementById('three').addEventListener('click', () => {
document.getElementById('three').classList.add('active');
document.getElementById('one').classList.remove('active');
document.getElementById('two').classList.remove('active');
document.getElementById('content').innerHTML = '众里寻他千百度,蓦然回首,那人却在灯火阑珊处。';
animateCamera({ x: -5, y: 8, z: 8 }, { y: 0 });
});
})
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
}
@font-face {
font-family: 'abduction';
src: url('/public/fonts/abduction.ttf');
}
html,
body {
overflow: hidden;
background: #000000;
font-family: abduction;
cursor: none;
}
.shadow_page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100%;
scroll-behavior: smooth;
overscroll-behavior: none;
color: #fff;
overflow: hidden auto;
}
.shadow_page .github {
position: fixed;
bottom: 24px;
left: 24px;
z-index: 11;
cursor: pointer;
transition: opacity .25s ease-in-out;
}
.shadow_page .github:hover {
opacity: .6;
}
.shadow_page .content .section {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
flex-direction: column;
align-content: center;
justify-content: center;
align-items: flex-start;
position: relative;
z-index: 1;
height: 100vh;
width: 100vw;
overflow: hidden;
pointer-events: none;
box-sizing: border-box;
}
.shadow_page .content .section .webgl {
height: 100%;
width: 100%;
}
.shadow_page .content .section.first {
pointer-events: none;
font-size: 2em;
letter-spacing: 0.5em;
text-align: center;
width: 100%;
display: flex;
height: 100vh;
align-content: center;
justify-content: flex-end;
align-items: center;
flex-direction: column;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
z-index: 1;
background: linear-gradient(0deg, #050505 20%, rgba(5,5,5,0) 50%);
overflow: hidden;
}
.shadow_page .content .section.first .info {
position: relative;
z-index: 1;
}
.shadow_page .content .section.first .info .name {
font-size: 2em;
font-weight: 100;
letter-spacing: 0.25em;
font-style: italic;
color: #03c03c;
font-family: abduction;
}
.shadow_page .content .section.first .info .title {
margin: 10px 0;
font-weight: 100;
letter-spacing: 0.4em;
font-size: 1.8em;
font-family: abduction;
}
.shadow_page .content .section.first .info .title::after {
content: "";
position: absolute;
margin-top: 105px;
left: calc(50% - 25px);
width: 50px;
height: 2px;
background: rgba(255,255,255,0.439);
}
.shadow_page .content .section.first .info .description {
font-size: 0.45em;
letter-spacing: 0;
// font-family: sans-serif;
width: 80%;
line-height: 28px;
font-weight: lighter;
margin: 32px auto 16px;
color: rgba(201,201,201,0.588);
opacity: 0;
transition: all 3.2s ease-in-out;
}
.shadow_page .content .section.first .info .description.ended {
opacity: 1;
}
.shadow_page .content .section.first .webgl {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
outline: none;
z-index: 0;
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
background: #000;
background: radial-gradient(circle at center center, #171717 0, #050505 58%);
}
.shadow_page .content .section.second {
pointer-events: all;
font-size: 2em;
width: 100%;
display: flex;
height: 100vh;
background: #141414;
z-index: 1;
margin: 0;
padding: 0;
overflow: hidden;
}
.shadow_page .content .section.second .second-container {
pointer-events: all;
width: 100%;
display: flex;
height: 100vh;
margin: 0;
padding: 0 10%;
flex-direction: column;
justify-content: center;
z-index: 2;
background: radial-gradient(circle at 90% center, rgba(5,5,5,0) 30%, #141414 70%);
}
.shadow_page .content .section.second .webgl {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
outline: none;
z-index: 0;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
pointer-events: all;
overflow: hidden;
}
.lds-roller {
width: 80px;
height: 80px;
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
z-index: 5;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: #f9f0ec;
margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
#loading-text-intro {
z-index: 3;
position: absolute;
width: 100vw;
height: 100%;
display: flex;
align-content: center;
justify-content: center;
align-items: center;
font-size: 12px;
color: #f9f0ec;
background: radial-gradient(circle at center center, #5d5d5d 0, #090909 58%);
// font-family: Arial, Helvetica, sans-serif;
}
#loading-text-intro.ended {
transform: translateY(200%);
}
nav {
width: 100%;
padding: 16px;
position: fixed;
z-index: 2;
}
span {
display: inline-block;
pointer-events: none;
transition: transform 0.1s linear;
}
.cursor {
pointer-events: none;
position: fixed;
top: 10px;
left: 10px;
padding: 10px;
background: rgba(255, 255, 255, .3);
backdrop-filter: blur(4px);
border-radius: 50%;
transform: translate(-50%, -50%);
mix-blend-mode: difference;
transition: transform 0.8s ease, opacity 0.6s ease;
z-index: 2;
border: .5px solid rgba(255, 255, 255, .1);
}
.a {
display: inline-block;
color: #fff;
padding: 16px;
margin-right: 64px;
letter-spacing: 8px;
font-size: 18px;
transition: all 0.3s ease, color 0.3s ease;
}
nav.header .a:hover {
cursor: pointer;
color: #afafaf;
transform: scale(1.1);
}
nav.header .a:hover~.cursor {
transform: translate(-50%, -50%) scale(5);
opacity: 0.1;
}
.dg.ac {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
z-index: 2 !important;
}
.header {
position: absolute;
top: -2em;
left: 0;
color: #fff;
font-size: 0.8em;
width: 100%;
text-align: center;
z-index: 2;
opacity: 0;
transition: all 1.8s ease-in-out;
padding: 0;
margin: 0;
}
.header.ended {
top: 3em;
opacity: 1;
}
.header>span {
padding: 0 3.25em;
letter-spacing: 0.4em;
position: relative;
}
.header>span.active:after,
.first {
position: absolute;
left: 50%;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
}
.header>span.active:after {
content: "";
bottom: -10px;
width: 20px;
height: 2px;
background: #fff;
}
.second-container>ul {
list-style: none;
display: inline-flex;
padding: 0px;
margin: 0px 0px 64px 64px;
color: rgba(255,255,255, .11);
z-index: 2;
}
.second-container>ul>li.active:after {
content: "";
top: 20px;
width: 50px;
height: 2px;
background: #fff;
position: relative;
left: 0px;
display: block;
}
.second-container>ul>li {
padding-right: 32px;
transition: all .8s ease-out;
font-size: 1.2em;
}
.second-container>ul>li:hover {
color: #f5f5f5;
pointer-events: all;
cursor: pointer;
}
.second-container>ul>li:hover~nav.header.ended.cursor {
transform: translate(-50%, -50%) scale(5);
opacity: 1;
}
.second-container>ul>li.active {
color: #f5f5f5;
}
.second-container .text {
font-size: 1.4em;
width: 30%;
color: rgba(255, 255, 255, .8);
margin-left: 60px;
height: 300px;
line-height: 2;
letter-spacing: 8px;
}
@media only screen and (max-width: 660px) {
.a {
padding: 10px;
margin-right: 0;
letter-spacing: 4px;
}
.footer {
margin-bottom: 20px;
}
.header>span {
padding: 0 1em;
}
.header {
font-size: 0.6em;
}
.main-section .product-display h3 {
width: 260px;
font-size: 42px;
margin-left: 30px;
line-height: 45px;
}
.first>h1 {
margin: 10px 0;
font-weight: 100;
letter-spacing: 0.2em;
font-size: 13vw;
}
.first>p {
width: 85%;
line-height: 22px;
}
.second-container {
padding: 0;
justify-content: flex-end;
}
.second-container>ul {
margin: 0px 0px 30px 30px;
width: 80%;
}
.second-container>ul>li {
padding-right: 20px;
transition: all 0.8s ease-out;
font-size: 20px;
}
.second-container>p {
width: 85%;
margin-left: 30px;
line-height: 21px;
margin-bottom: 40px;
}
.third>p {
column-count: 1;
}
}
@-moz-keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@-webkit-keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@-o-keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
知识点:
1.three.js引入外部模型
1.可以在sketchfab网站上下载免费的模型

2.模型的加载
将模型文件引入项目中,根据不同的模型使用对应的模型加载器,常见的有:
1. GLTFLoader:
ini
- 用于加载基于glTF格式的3D模型文件,通常有 `.gltf` 和 `.glb` 扩展名。
- 使用方法:
```
javascript
插入代码复制代码
const loader = new THREE.GLTFLoader();
loader.load('model.gltf', (gltf) => {
const model = gltf.scene;
// 添加到场景或进行其他操作
});
```
2. OBJLoader:
csharp
- 用于加载Wavefront OBJ模型文件。
- 使用方法:
```
javascript
插入代码复制代码
const loader = new THREE.OBJLoader();
loader.load('model.obj', (object) => {
// 添加到场景或进行其他操作
});
```
3. FBXLoader:
csharp
- 用于加载Autodesk FBX模型文件。
- 使用方法:
```
javascript
插入代码复制代码
const loader = new THREE.FBXLoader();
loader.load('model.fbx', (object) => {
// 添加到场景或进行其他操作
});
```
4. STLLoader:
ini
- 用于加载STL(Stereolithography)模型文件,通常用于表达三维表面的文件格式。
- 使用方法:
```
javascript
插入代码复制代码
const loader = new THREE.STLLoader();
loader.load('model.stl', (geometry) => {
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const model = new THREE.Mesh(geometry, material);
// 添加到场景或进行其他操作
});
```
5. ColladaLoader:
ini
- 用于加载Collada模型文件。
- 使用方法:
```
javascript
插入代码复制代码
const loader = new THREE.ColladaLoader();
loader.load('model.dae', (collada) => {
const model = collada.scene;
// 添加到场景或进行其他操作
});
```
2.渲染器的powerPreference属性:
是WebGL渲染上下文的一个属性,用于在Three.js中配置渲染器以优化性能和电源使用。这个属性允许你指定WebGL渲染的性能偏好,以便根据设备的性能和电源需求进行优化。
css
高性能偏好:
const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance" });
低功能偏好:
const renderer = new THREE.WebGLRenderer({ powerPreference: "low-power" });
3.渲染器的outputEncoding属性
用于指定渲染的输出颜色空间。该属性影响了最终呈现到屏幕上的颜色的显示方式。
ini
`THREE.LinearEncoding`(默认值):
表示使用线性颜色空间。这是现代图形渲染中常用的颜色空间,适合实现更准确的颜色计算。
`THREE.sRGBEncoding`:
表示使用sRGB颜色空间,适用于一些老旧设备和应用程序,或者需要与传统图形处理软件进行兼容的情况。
`THREE.GammaEncoding`:
表示使用伽马校正。这在一些特殊情况下可能会用到,但一般来说,现代图形渲染更倾向于使用线性颜色空间。
const renderer = new THREE.WebGLRenderer();
renderer.outputEncoding = THREE.sRGBEncoding;
4.three.js的LoadingManager
LoadingManager
是Three.js中的一个类,用于管理资源加载过程中的事件和进度。
LoadingManager
可以用来追踪和管理加载资源的进度,以及在资源加载完成或出错时触发相应的事件。它可以用于加载模型、纹理、音频等各种类型的资源。
javascript
// 创建LoadingManager实例
const loadingManager = new THREE.LoadingManager();
// 注册资源加载完成的回调函数
loadingManager.onLoad = function () {
console.log('All resources loaded successfully');
};
// 注册资源加载进度的回调函数
loadingManager.onProgress = function (url, loaded, total) {
const progress = (loaded / total) * 100;
console.log(`Loading ${url}: ${progress.toFixed(2)}%`);
};
// 注册资源加载出错的回调函数
loadingManager.onError = function (url) {
console.log(`Error loading ${url}`);
};
// 创建加载器
const loader = new THREE.TextureLoader(loadingManager);
// 加载纹理
loader.load(
'texture.jpg',
function (texture) {
// 纹理加载完成后的处理逻辑
},
function (xhr) {
// 纹理加载进度的处理逻辑
},
function (error) {
// 纹理加载出错的处理逻辑
}
);
5.Tween.js插值动画
Tween(插值动画)通常是通过第三方库如TWEEN.js
来实现的,它用于创建平滑的动画效果,如对象的平移、旋转、缩放等。TWEEN.js
库允许你在一定的时间内从一个属性值过渡到另一个属性值,从而创建流畅的动画效果。
scss
// 引入 (我的three.js版本0.157.0 一般都在three/examples/jsm/libs文件夹下,可能导出方式稍有不同)
import { default as TWEEN } from 'three/examples/jsm/libs/tween.module.js';
// 使用
// 创建一个初始状态的对象
const object = { x: 0 };
// 创建一个Tween动画
const tween = new TWEEN.Tween(object)
.to({ x: 100 }, 1000) // 将属性x从0过渡到100,耗时1000毫秒
.easing(TWEEN.Easing.Quadratic.InOut) // 缓动函数,可以根据需要选择不同的缓动效果
.onUpdate(() => {
// 在Tween更新时执行的回调函数
console.log(object.x);
})
.onComplete((=>{
// 在Tween更新完成时的回调
}))
.start(); // 启动Tween动画
// 更新Tween动画
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
}
animate();
6.THREE.Box3
THREE.Box3
是 Three.js 中的一个类,它用于表示一个三维空间中的包围盒,通常用于计算和管理对象的边界框。包围盒是一个立方体,它可以完全包围一个或多个对象,从而允许你进行碰撞检测、相机视锥体裁剪等操作。以下是关于 THREE.Box3
的详解:
markdown
const box3 = new THREE.Box3();
方法和属性
`Box3` 对象提供了一系列方法和属性,以便你进行各种操作:
1. **属性**:
- `min`:包围盒的最小点(Three.js 的 `Vector3` 对象)。
- `max`:包围盒的最大点(Three.js 的 `Vector3` 对象)。
- `isEmpty`:返回一个布尔值,指示包围盒是否为空。
- `getSize(target)`:计算包围盒的尺寸,并将结果存储在 `target` 中。
- `getCenter(target)`:计算包围盒的中心点,并将结果存储在 `target` 中。
- `applyMatrix4(matrix)`:将包围盒应用于给定的变换矩阵。
- `setFromObject(object)`:根据对象的几何体或位置计算包围盒。
- `expandByPoint(point)`:将包围盒扩展以包含给定点。
- `expandByVector(vector)`:将包围盒扩展以包含给定的向量。
- `expandByScalar(scalar)`:将包围盒的尺寸扩展一个标量值。
- `expandToFitBox(box)`:将包围盒扩展以包含另一个 `Box3` 对象。
1. **方法**:
- `set(min, max)`:设置包围盒的最小点和最大点。
- `setFromArray(array)`:从一个点数组中设置包围盒的最小点和最大点。
- `setFromPoints(points)`:从一个点集合中设置包围盒的最小点和最大点。
- `clone()`:创建并返回一个包围盒的克隆。
- `copy(box)`:从另一个 `Box3` 对象复制数据到当前对象。
- `makeEmpty()`:将包围盒设置为空。
- `isEmpty()`:检查包围盒是否为空。
- `translate(offset)`:将包围盒的所有点平移指定的偏移量。
1.使用示例
arduino
const box3 = new THREE.Box3();
box3.expandByPoint(new THREE.Vector3(1, 2, 3)); // 扩展包围盒以包含点 (1, 2, 3)
box3.expandByPoint(new THREE.Vector3(4, 5, 6)); // 扩展包围盒以包含点 (4, 5, 6)
const size = new THREE.Vector3();
box3.getSize(size); // 计算包围盒的尺寸
const center = new THREE.Vector3();
box3.getCenter(center); // 计算包围盒的中心点
console.log(box3.min); // 打印最小点
console.log(box3.max); // 打印最大点
2.setFromObject方法详解
用于根据给定的对象的几何体或位置计算一个包围盒
ini
const box3 = new THREE.Box3();
box3.setFromObject(Object3D); // 计算 cube 对象的包围盒
object
:要计算包围盒的对象,通常是 Three.js 中的 Object3D
对象
7.场景的traverse方法
遍历 Three.js 场景图中的所有对象,包括场景中的子对象和嵌套对象。这个方法通常用于执行操作,例如对象的更新、渲染、查找特定类型的对象等
typescript
// 遍历场景中的所有对象
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
// 在遍历过程中,对每个 Mesh 执行操作
object.rotation.x += 0.01;
object.rotation.y += 0.01;
}
});
8.js的IntersectionObserver方法
IntersectionObserver 是一个 JavaScript API,它提供了一种异步监听元素是否进入或离开视口的方法,可以异步地监听目标元素与其祖先元素或视口之间的交叉状态,并在交叉状态改变时触发回调函数。通过使用 IntersectionObserver,我们可以减少计算量,提高性能,并且可以同时监听多个元素,执行相应的操作,比如加载更多内容、懒加载图片、触发动画等
1.图片懒加载的例子:
首先,我们需要将所有需要懒加载的图片添加到一个数组中,并使用 data-src
属性来存储图片的真实地址。然后,创建一个 IntersectionObserver 实例,指定回调函数和阈值参数:
ini
javascriptCopy Code
const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver(onIntersection, {
threshold: 0.5 // 当图片至少一半进入视口时触发回调函数
});
接下来,将每个需要懒加载的图片作为观察目标,注册到 IntersectionObserver 中:
ini
javascriptCopy Code
images.forEach(image => {
observer.observe(image);
});
最后,在回调函数中判断目标元素与视口或祖先元素的交叉比例是否达到阈值,如果达到则将 data-src
属性的值赋给 src
属性,实现图片加载:
ini
javascriptCopy Code
function onIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
const image = entry.target;
image.src = image.dataset.src;
observer.unobserve(image); // 加载完成后停止观察该图片
}
});
}
这样,当用户滚动页面时,只有当图片至少一半进入视口时才会触发回调函数,实现了图片的懒加载效果。同时,由于使用了 IntersectionObserver,可以避免频繁地监听滚动事件,提高页面性能。
2.使用 IntersectionObserver 的基本流程:
- 创建一个 IntersectionObserver 对象,并指定一个
回调函数
,阈值
、参考节点
。 - 通过调用
observe()
方法,将需要监听的目标元素传递给 IntersectionObserver 对象。 - 当目标元素进入或离开视口时,会触发回调函数,并提供一个包含交叉信息的参数(IntersectionObserverEntry)。
回调函数的参数(IntersectionObserverEntry
)包含了关于目标元素与视口或祖先元素交叉状态的各种信息,例如交叉比例、是否进入视口等。
3.IntersectionObserverEntry 对象具有以下属性
boundingClientRect
:目标元素的矩形区域的信息,包括位置、尺寸等。intersectionRatio
:目标元素与视口或祖先元素的交叉比例,取值范围为 0 到 1。0 表示目标元素完全不可见,1 表示目标元素完全可见。intersectionRect
:目标元素与视口或祖先元素的交叉区域的矩形信息。isIntersecting
:一个布尔值,表示目标元素当前是否与视口或祖先元素交叉。当目标元素进入视口时为 true,离开视口时为 false。rootBounds
:根元素(即视口或祖先元素)的矩形区域信息。target
:被观察的目标元素本身。
4.阈值threshold
threshold(阈值)是 IntersectionObserver 的一个可选参数,用于指定目标元素与视口或祖先元素交叉的阈值。当目标元素的交叉比例达到或超过指定的阈值时,就会触发 IntersectionObserver 的回调函数。取值范围为 0 到 1
ini
const observer = new IntersectionObserver(onIntersection, {
threshold: 0.5 // 当图片至少一半进入视口时触发回调函数
});
5.参考节点root
确定以视口或祖先元素作为参考来计算交叉比例时,可以使用 IntersectionObserver 的 root 参数进行设置。
- 如果要以视口作为参考,则将 root 参数设置为 null 或不进行设置,默认情况下会以视口为参考。
- 如果要以某个祖先元素作为参考,则将 root 参数设置为该祖先元素的 DOM 节点。
ini
const container = document.getElementById('container');
const observer = new IntersectionObserver(callback, {
root: container,
threshold: 0.5
});
9.点光源跟随
1.根据鼠标位置计算出屏幕坐标
ini
document.addEventListener('mousemove', event => {
event.preventDefault();
const cursor = new THREE.Vector2();
// 屏幕坐标方位限制在[-1,1]
cursor.x = (event.clientX / window.innerWidth) * 2 - 1;
cursor.y = - (event.clientY / window.innerHeight) * 2 + 1;
document.querySelector('.cursor').style.cssText = `left: ${event.clientX}px; top: ${event.clientY}px;`;
}, false);
为什么屏幕坐标限制为[-1,-1]
在计算机图形学中,将屏幕坐标范围限定在[-1, 1]是为了方便进行投影变换和裁剪操作。
投影变换是将三维场景投影到二维屏幕上的过程。在透视投影中,远离相机的物体会显得较小,而靠近相机的物体会显得较大。为了表示这种透视效果,需要使用归一化设备坐标(Normalized Device Coordinates,NDC),其范围是[-1, 1]。在NDC坐标系中,物体的可见部分落在一个单位立方体内,位于立方体外的部分将被裁剪掉。
通过将屏幕坐标范围限定在[-1, 1],可以将屏幕上的点映射到NDC坐标系中的单位立方体内。这样,在进行投影变换时,可以根据NDC坐标的范围来确定物体是否可见,以及如何进行裁剪操作。
另外,限定屏幕坐标范围为[-1, 1]还有其他一些好处:
- 方便进行坐标变换:通过将屏幕坐标范围限定在[-1, 1],可以方便地进行坐标变换,例如从屏幕坐标到世界坐标的转换。
- 与其他图形库的兼容性:很多图形库和标准都使用了[-1, 1]的屏幕坐标范围,通过保持一致,可以方便地与这些库进行集成和交互。
总之,将屏幕坐标范围限定在[-1, 1]是为了方便进行投影变换、裁剪操作和坐标变换,并与其他图形库保持兼容性。
2.计算光源位置
ini
// 计算光源位置
const updateLightPosition = () => {
// 使用鼠标坐标和距离计算光源位置
const cameraPa = secondContainer ? camera2 : camera
const lightDistance = 2; // 光源距离摄像机的距离
const zParams = secondContainer ? 6 : 8
const vector = new THREE.Vector3(cursor.x, cursor.y, lightDistance);
vector.unproject(cameraPa);
const dir = vector.sub(cameraPa.position).normalize();
const distance = -cameraPa.position.z / dir.z + zParams;
const lightPosition = cameraPa.position.clone().add(dir.multiplyScalar(distance));
lightPosition.x +=1
fillLight.position.lerp(lightPosition,0.06);
};
1.三维向量的unproject方法将一个屏幕坐标(二维)转换为三维空间中的射线或向量
在 Three.js 中,三维向量的 unproject
方法用于将一个屏幕坐标(二维)转换为三维空间中的射线或向量。这个方法主要用于将鼠标点击位置(或触摸事件位置)从屏幕坐标系转换为世界坐标系中的射线方向或向量。
arduino
const vector = new THREE.Vector3(cursor.x, cursor.y, lightDistance); // 光源距离相机的距离
vector.unproject(camera);
unproject方法的详细解释如下:
vector
:要进行转换的三维向量对象。camera
:相机对象,通常是PerspectiveCamera
或OrthographicCamera
的实例。
具体来说,unproject
方法会根据相机的投影矩阵和视图矩阵,将屏幕坐标转换为裁剪空间坐标(在[-1, 1]范围内),然后再应用相机的逆投影矩阵,将裁剪空间坐标转换为世界坐标系中的射线方向或向量。 原文 地址:juejin.cn/post/714896...