1. 实现思路
观察现实生活中的昼夜交替,最直观的表现就是太阳和月亮的升起降落。白天太阳位置更高,随着夜幕的降临,太阳会逐渐落下,大地上事物的影子也会变得比白天更长。太阳落下去,月亮升起来,月光比日光更加的柔和,没有日光那么的强烈……
综上述一系列废话,得出昼夜切换效果的实现思路:
1.1 光源的位置移动
光源的高度越低,产生的影子也就越长。
1.2 光源亮度的降低
制造夜间昏暗的氛围。
1.3 补间动画来让光源的移动更平滑
模拟时间推移的平滑过程。
2. 实现过程
2.1 设置渲染器,允许产生阴影
// 允许渲染器产生阴影贴图
renderer.shadowMap.enabled = true;
2.2 创建光源,设置光源产生阴影
// 创建点光源
pointLight = new Three.PointLight(0xffffff, 2.5);
// 设置点光源产生阴影
pointLight.castShadow = true;
// 设置点光源的位置
pointLight.position.set(-10, 10, 0);
// 将点光源添加到场景中
scene.add(pointLight);
2.3 创建一些物体,模拟地面,和地面上的物体
// 创建一个平面,模拟地面
const planeGeometry = new Three.PlaneGeometry(50, 50);
const planeMaterial = new Three.MeshStandardMaterial({
color: 0xf5f5f5,
});
const plane = new Three.Mesh(planeGeometry, planeMaterial);
// 翻转平面90度,使其平行于X0Z平面
plane.rotateX(-0.5 * Math.PI);
// 设置平面接受阴影
plane.receiveShadow = true;
scene.add(plane);
// 创建一个六面体,模拟地面上的各种事物
const boxGeometry = new Three.BoxGeometry(5, 5, 5);
const boxMaterial = new Three.MeshStandardMaterial({
color: 0x0f3f0f,
});
const box = new Three.Mesh(boxGeometry, boxMaterial);
box.position.set(0, 2.5, 0);
// 设置物体产生影子
box.castShadow = true;
scene.add(box);
2.4 阴影效果
2.5 处理光源的移动
2.5.1 代码
// 当前光源的位置以及光的亮度
const curLightPosition = pointLight.position;
// 光源要移动到的位置,和光源要改变为的强度
const targetPosition = this.isInDay ? new Three.Vector3(-15, 6, 0) : new Three.Vector3(-10, 10, 0);
// 创建TWEEN对象
let lightTween = new TWEEN.Tween({
x: curLightPosition.x, y: curLightPosition.y, z: curLightPosition.z,
}).to({
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
}, 1000).easing(TWEEN.Easing.Quadratic.InOut);
lightTween.onUpdate((obj) => {
// 随着duration的推移,将光源更新到”此刻“变化到的位置和发光强度
pointLight.position.set(obj.x, obj.y, obj.z);
});
lightTween.onComplete(() => {
// 结束后,不再在每帧渲染时更新TWEEN
this.logoutRenderFunc('day-night');
lightTween = null;
// 更新标志位
this.isInDay = !this.isInDay;
});
// 开始补间动画(注意配合每帧渲染时的TWEEN.update()更新动画)
lightTween.start();
this.registerRenderFunc('day-night', () => {
// 每帧渲染时更新补间动画
TWEEN && lightTween && TWEEN.update();
});
2.5.2 实现效果
2.6 处理光照强度的改变
2.6.1 代码
// 当前光源的位置以及光的亮度
const curLightPosition = pointLight.position;
const curLightIntensity = pointLight.intensity;
// 光源要移动到的位置,和光源要改变为的强度
const targetPosition = this.isInDay ? new Three.Vector3(-15, 6, 0) : new Three.Vector3(-10, 10, 0);
const targetIntensity = this.isInDay ? 1 : 2.5;
// 创建TWEEN对象
let lightTween = new TWEEN.Tween({
x: curLightPosition.x, y: curLightPosition.y, z: curLightPosition.z,
intensity: curLightIntensity
}).to({
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
intensity: targetIntensity
}, 1000).easing(TWEEN.Easing.Quadratic.InOut);
lightTween.onUpdate((obj) => {
// 随着duration的推移,将光源更新到”此刻“变化到的位置和发光强度
pointLight.position.set(obj.x, obj.y, obj.z);
pointLight.intensity = obj.intensity;
});
lightTween.onComplete(() => {
// 结束后,不再在每帧渲染时更新TWEEN
this.logoutRenderFunc('day-night');
lightTween = null;
// 更新标志位
this.isInDay = !this.isInDay;
});
// 开始补间动画(注意配合每帧渲染时的TWEEN.update()更新动画)
lightTween.start();
this.registerRenderFunc('day-night', () => {
// 每帧渲染时更新补间动画
TWEEN && lightTween && TWEEN.update();
});
2.6.2 效果
3. 使用库版本
"three": "0.142.0"
"vue": "^3.2.13"
4. 关键代码
/**
* 昼夜切换
*/
turnDayNight() {
// 当前光源的位置以及光的亮度
const curLightPosition = pointLight.position;
const curLightIntensity = pointLight.intensity;
// 光源要移动到的位置,和光源要改变为的强度
const targetPosition = this.isInDay ? new Three.Vector3(-15, 6, 0) : new Three.Vector3(-10, 10, 0);
const targetIntensity = this.isInDay ? 1 : 2.5;
// 创建TWEEN对象
let lightTween = new TWEEN.Tween({
x: curLightPosition.x, y: curLightPosition.y, z: curLightPosition.z,
intensity: curLightIntensity
}).to({
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
intensity: targetIntensity
}, 1000).easing(TWEEN.Easing.Quadratic.InOut);
lightTween.onUpdate((obj) => {
// 随着duration的推移,将光源更新到”此刻“变化到的位置和发光强度
pointLight.position.set(obj.x, obj.y, obj.z);
pointLight.intensity = obj.intensity;
});
lightTween.onComplete(() => {
// 结束后,不再在每帧渲染时更新TWEEN
this.logoutRenderFunc('day-night');
lightTween = null;
// 更新标志位
this.isInDay = !this.isInDay;
});
// 开始补间动画(注意配合每帧渲染时的TWEEN.update()更新动画)
lightTween.start();
this.registerRenderFunc('day-night', () => {
// 每帧渲染时更新补间动画
TWEEN && lightTween && TWEEN.update();
});
},
/**
* 创建地面上的事物
*/
createThings() {
// 创建一个平面,模拟地面
const planeGeometry = new Three.PlaneGeometry(50, 50);
const planeMaterial = new Three.MeshStandardMaterial({
color: 0xf5f5f5,
});
const plane = new Three.Mesh(planeGeometry, planeMaterial);
// 翻转平面90度,使其平行于X0Z平面
plane.rotateX(-0.5 * Math.PI);
// 设置平面接受阴影
plane.receiveShadow = true;
scene.add(plane);
// 创建一个六面体,模拟地面上的各种事物
const boxGeometry = new Three.BoxGeometry(5, 5, 5);
const boxMaterial = new Three.MeshStandardMaterial({
color: 0x0f3f0f,
});
const box = new Three.Mesh(boxGeometry, boxMaterial);
box.position.set(0, 2.5, 0);
// 设置物体产生影子
box.castShadow = true;
scene.add(box);
},
/**
* 生成线性颜色
* @param colorStr 颜色字符串
* @returns {Color}
*/
lineColor(colorStr) {
const tmp = new Three.Color(colorStr);
tmp.convertSRGBToLinear();
return tmp;
},
/**
* 注册渲染中执行的方法
* @param name 设定函数名称
* @param func 函数方法体
*/
registerRenderFunc(name, func) {
this.renderFunc[name] = func;
},
/**
* 注销渲染中执行的方法
* @param name 要注销的函数名称
*/
logoutRenderFunc(name) {
const old = this.renderFunc[name];
if (old) {
delete this.renderFunc[name];
}
},
/**
* 初始化3d场景、渲染器、相机等部件
*/
init() {
const parent = document.querySelector('#canvas-box');
this.parent = document.querySelector('#canvas-box');
const maxWith = parent.clientWidth - 31;
const maxHeight = parent.clientHeight - 63;
scene = new Three.Scene();
// 创建点光源
pointLight = new Three.PointLight(0xffffff, 2.5);
// 设置点光源产生阴影
pointLight.castShadow = true;
// 设置点光源的位置
pointLight.position.set(-10, 10, 0);
// 将点光源添加到场景中
scene.add(pointLight);
// 创建网格辅助器
const grid = new Three.GridHelper(2000, 50, 0xffffff, 0xffffff);
grid.material.opacity = 0.2;
grid.material.transparent = true;
scene.add(grid);
// 创建相机
camera = new Three.PerspectiveCamera(45, maxWith / maxHeight, 1, 2000);
camera.position.set(0, 300, -700);
// 创建渲染器
renderer = new Three.WebGLRenderer({
antialias: true, // 消除锯齿
logarithmicDepthBuffer: true, // 对数深度缓冲区
});
renderer.setSize(maxWith, maxHeight);
renderer.shadowMap.enabled = true;
renderer.setClearColor(0x000000);
parent.appendChild(renderer.domElement);
// 创建控制器
orbitControls = new OrbitControls(camera, renderer.domElement);
// 改变窗口大小,更新相机画面大小和渲染器大小
window.addEventListener('resize', () => {
camera.aspect = (this.parent.clientWidth - 31) / (this.parent.clientHeight - 63);
camera.updateProjectionMatrix();
renderer.setSize(this.parent.clientWidth - 31, this.parent.clientHeight - 63);
});
// 创建完毕,开始执行每帧渲染
this.render();
},
/**
* 开始渲染
*/
render() {
const _this = this;
// 动画循环渲染
function animate() {
try {
// 预约下一帧的渲染动作
requestAnimationFrame(animate);
try {
// 让渲染器渲染一帧相机捕捉到的场景
renderer.render(scene, camera);
} catch (e) {
console.log(e);
}
// 更新控制器
orbitControls.update();
// 获取时间差
const delta = clock.getDelta();
// 执行registerRenderFunc方式注册进来的渲染操作
const funcNames = _this.renderFunc && Object.keys(_this.renderFunc);
if (funcNames && funcNames.length > 0) {
funcNames.forEach((funcName) => {
try {
// 不太放心,try-catch一下,保证出现意外也能继续执行后面的内容
_this.renderFunc[funcName](delta);
} catch (e) {
console.error(
'render func error, func name: ',
funcName,
', error message:',
e.message,
);
}
});
}
} catch (e) {
console.error('render animate error, error message: ', e.message);
}
}
animate();
},
- 初始化
mounted() {
this.$nextTick(() => {
// vue实例挂载完成后,初始化3D场景资源
this.init();
this.createThings();
});
},
- 昼夜切换时调用
<el-button type="success" @click="turnDayNight">切换</el-button>