Three.js灯光阴影与动画交互

目录


灯光

在Three.js中,灯光(Lights)是模拟光照效果的关键元素,它们能够极大地增强场景的真实感和深度。

环境光(AmbientLight):

javascript 复制代码
   const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // 白色环境光,强度为0.5
   scene.add(ambientLight);

环境光均匀地照亮整个场景,没有方向性,适合为场景提供基础照明。

点光源(PointLight):

javascript 复制代码
   const pointLight = new THREE.PointLight(0xffffff, 1, 100); // 白色,强度1,最大距离100
   pointLight.position.set(10, 10, 10); // 设置光源位置
   scene.add(pointLight);

点光源从一个点向所有方向发射光线,随着距离增加,光照强度逐渐减弱。

聚光灯(SpotLight):

javascript 复制代码
   const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4); // 白色,强度1,最大距离100,锥角45度
   spotLight.position.set(0, 10, 10);
   spotLight.target.position.set(0, 0, 0); // 设置目标位置,灯光指向该点
   scene.add(spotLight);
   scene.add(spotLight.target); // 注意也要添加目标到场景

聚光灯有方向性,模拟手电筒或舞台灯的效果,具有内锥和外锥,可以设置其方向和焦点。

平行光(DirectionalLight):

javascript 复制代码
   const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // 白色,强度1
   directionalLight.position.set(0, 1, 1).normalize(); // 设置光源方向(向量需要归一化)
   scene.add(directionalLight);

平行光模拟太阳光,所有光线都视为平行的,没有衰减,常用于户外场景。

半球光(HemisphereLight):

javascript 复制代码
   const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.5); // 上部颜色(天空),下部颜色(地面),强度
   hemisphereLight.position.set(0, 10, 0); // 通常放在场景上方
   scene.add(hemisphereLight);

半球光模拟天空和地面的自然光照,上半部分和下半部分可以设置不同颜色。

在使用灯光时,还需要考虑以下几个方面:

  • 衰减:某些光源(如点光源和聚光灯)可以设置衰减属性,模拟真实世界中光随距离减弱的现象。
  • 阴影:通过设置light.castShadow = true并配置相关阴影属性,可以让灯光投射阴影,增加场景的真实感。
  • 强度:灯光的强度可以通过构造函数的第二个参数或.intensity属性调整。
  • 颜色:灯光颜色由构造函数的第一个参数确定,使用十六进制颜色值。

平行光阵列(DirectionalLightShadow):

javascript 复制代码
   const dLight = new THREE.DirectionalLight(0xffffff, 1);
   dLight.castShadow = true; // 开启阴影
   dLight.shadow.mapSize.width = 1024; // 阴影贴图大小
   dLight.shadow.mapSize.height = 1024;
   dLight.shadow.camera.top = 10;
   dLight.shadow.camera.bottom = -10;
   dLight.shadow.camera.left = -10;
   dLight.shadow.camera.right = 10;
   scene.add(dLight);

这种灯光可以用于创建更高质量的阴影,通过调整阴影相机的属性控制阴影的可见区域。

自定义光源(LightProbe):

javascript 复制代码
   const lightProbe = new THREE.LightProbe();
   lightProbe.intensity = 1;
   lightProbe.environment = cubeEnvMap; // 使用立方体贴图作为环境光源
   scene.add(lightProbe);

光源探针用于全局光照计算,可以捕捉环境中的光照信息,适用于环境映射和物理渲染。

光晕效果(LightHalo):

javascript 复制代码
   const halo = new THREE.Halo(light, 100, 100, 100);
   halo.color.copy(light.color);
   scene.add(halo);

光晕效果通常用于模拟光源周围的光环,不是内置的灯光类型,而是通过第三方库如three-halo实现。

光跟踪器(TrackballControls):

javascript 复制代码
   const controls = new THREE.TrackballControls(camera, renderer.domElement);
   controls.update();

虽然这不是灯光类型,但TrackballControls可以帮助你交互式地调整光源的位置和方向,这对于调试和预览光照效果很有帮助。

光强度渐变(Fog):

javascript 复制代码
   scene.fog = new THREE.Fog(0x000000, 1, 100); // 黑色雾,近裁剪平面1,远裁剪平面100

阴影

在Three.js中,阴影的实现涉及阴影投射和阴影接收。阴影投射是指光源能否投射出阴影,而阴影接收则指物体是否能显示投射在其上的阴影。

开启光源的阴影投射:

对于支持阴影的光源类型(如点光源、聚光灯或平行光),需要设置castShadow属性为true

javascript 复制代码
   const light = new THREE.DirectionalLight(0xffffff, 1);
   light.castShadow = true;
   light.shadow.mapSize.width = 1024; // 阴影贴图大小
   light.shadow.mapSize.height = 1024;
   // ... 其他光源设置

设置阴影参数:

阴影贴图的大小会影响阴影的质量和性能,可以根据需要调整。

开启阴影接收:

为了使物体能够接收阴影,需要设置其材质的receiveShadow属性为true

javascript 复制代码
   const material = new THREE.MeshStandardMaterial({ color: 0x000000, receiveShadow: true });
   const geometry = new THREE.BoxGeometry(1, 1, 1);
   const mesh = new THREE.Mesh(geometry, material);
   scene.add(mesh);

阴影相机和投影矩阵:

阴影的计算需要一个单独的相机,通常由光源对象管理。光源的shadow.camera属性可以进一步调整,以适应场景的需要。

javascript 复制代码
   light.shadow.camera.left = -10;
   light.shadow.camera.right = 10;
   light.shadow.camera.top = 10;
   light.shadow.camera.bottom = -10;
   light.shadow.camera.near = 0.5;
   light.shadow.camera.far = 50;

阴影贴图更新:

在每一帧渲染之前,确保阴影贴图被正确更新。

javascript 复制代码
   light.shadow.map.needsUpdate = true;

渲染器设置:

渲染器也需要开启阴影支持。

javascript 复制代码
   const renderer = new THREE.WebGLRenderer();
   renderer.shadowMap.enabled = true;
   renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 可选,可以使用更平滑的阴影

阴影贴图类型:

默认情况下,Three.js使用PCF(Percentage Closest Filtering)阴影贴图。还可以选择其他方法,如THREE.PCFSoftShadowMap(软阴影)或THREE.VSMShadowMap(垂直偏移阴影贴图)。

javascript 复制代码
   renderer.shadowMap.type = THREE.PCFSoftShadowMap;

阴影偏移:

有时阴影可能会出现错位,可以通过调整光源的shadow.bias属性来微调。

javascript 复制代码
   light.shadow.bias = -0.001;

阴影模糊:

通过设置renderer.shadowMapblur,可以调整阴影边缘的模糊程度。

javascript 复制代码
   renderer.shadowMap.blur = true;

阴影贴图大小:

阴影贴图的大小影响阴影的清晰度。更大的贴图意味着更高的质量,但也会消耗更多的GPU资源。

javascript 复制代码
   light.shadow.mapSize.width = 2048;
   light.shadow.mapSize.height = 2048;

阴影距离:

可以调整光源的阴影距离,以限制阴影的可见范围。

javascript 复制代码
   light.shadow.camera.far = 50;

阴影深度测试:

默认情况下,Three.js使用深度贴图进行阴影计算。如果场景中存在复杂的几何结构,可以尝试使用深度纹理(Depth Texture)来提高性能。

javascript 复制代码
   light.shadow.map = new THREE.WebGLRenderTarget(1024, 1024, {
     format: THREE.DepthFormat,
     type: THREE.FloatType,
   });

优化阴影:

  • 分层阴影:如果场景中有大量物体,可以考虑将物体分组,为每组使用独立的光源和阴影贴图,以减少计算量。
  • 阴影可见性:只在需要的时候开启阴影,例如,当相机靠近某个物体时才开启其阴影。

自定义阴影着色器:

如果需要更复杂的阴影效果,可以编写自定义的着色器来处理阴影纹理。

基本动画

在Three.js中,创建基本动画通常涉及以下几个步骤。

创建场景(Scene):

javascript 复制代码
   const scene = new THREE.Scene();

添加相机(Camera):

javascript 复制代码
   const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
   camera.position.z = 5; // 设置相机位置

创建渲染器(Renderer):

javascript 复制代码
   const renderer = new THREE.WebGLRenderer();
   renderer.setSize(window.innerWidth, window.innerHeight);
   document.body.appendChild(renderer.domElement);

加载模型或几何体(Geometry):

javascript 复制代码
   const geometry = new THREE.BoxGeometry(1, 1, 1); // 创建一个立方体

应用材质(Material):

javascript 复制代码
   const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // 绿色材质

创建网格(Mesh):

javascript 复制代码
   const mesh = new THREE.Mesh(geometry, material);
   scene.add(mesh); // 将网格添加到场景

动画部分:

创建AnimationMixer用于管理动画:

javascript 复制代码
     const mixer = new THREE.AnimationMixer(mesh);

如果你有一个 AnimationClip,你可以将其添加到mixer中:

javascript 复制代码
     const clip = ...; // 获取或加载 AnimationClip
     mixer.clipAction(clip).play(); // 播放动画

更新循环以每帧更新mixer:

javascript 复制代码
     function animate() {
       requestAnimationFrame(animate);
       mixer.update(delta); // delta是时间差,通常从THREE.Clock获取
       renderer.render(scene, camera);
     }
     animate();

窗口大小改变事件:

为了响应窗口大小变化,通常会调整渲染器的大小:

javascript 复制代码
   window.addEventListener('resize', onWindowResize, false);

   function onWindowResize() {
     camera.aspect = window.innerWidth / window.innerHeight;
     camera.updateProjectionMatrix();
     renderer.setSize(window.innerWidth, window.innerHeight);
   }

创建关键帧动画:

Three.js允许你使用关键帧动画来控制对象的属性随时间变化。首先,你需要创建一个KeyframeTrack,然后用它来构建AnimationClip。

javascript 复制代码
   // 定义关键帧
   const times = [0, 1, 2]; // 时间轴上的关键帧时刻
   const values = [0, 1, 0]; // 对应的关键帧值,例如旋转角度
   const track = new THREE.NumberKeyframeTrack(
     '.rotation.x', // 路径,这里是对象的rotation.x属性
     times,
     values
   );

   // 创建动画片段
   const clip = new THREE.AnimationClip('rotateX', -1, [track]); // -1表示无限循环

应用关键帧动画:

如之前所述,将clip添加到mixer并播放。

javascript 复制代码
   mixer.clipAction(clip).play();

动态更新动画:

如果需要在运行时修改动画,可以使用clipAction的方法。例如,如果你想改变动画的速度,可以这样做:

javascript 复制代码
   mixer.clipAction(clip).setEffectiveTimeScale(2); // 放慢速度

停止或重置动画:

当你想停止或重置动画时,可以使用以下方法:

javascript 复制代码
   mixer.clipAction(clip).stop(); // 停止动画
   mixer.clipAction(clip).reset(); // 重置到初始状态

在Three.js中,还可以使用Object3D的内置方法来创建简单的动画效果,例如平移、旋转或缩放。这里是一个使用Object3DtranslateOnAxis()rotateOnAxis()scale()方法的例子:

javascript 复制代码
function animate(time) {
  requestAnimationFrame(animate);

  // 平移
  mesh.position.x += 0.01;

  // 旋转
  const axis = new THREE.Vector3(0, 1, 0); // 围绕Y轴旋转
  const angle = time * 0.01; // 旋转角度与时间成比例
  mesh.rotateOnAxis(axis, angle);

  // 缩放
  mesh.scale.set(1 + (time % 2) * 0.1, 1 + (time % 2) * 0.1, 1); // 在X和Y轴上交替放大缩小

  renderer.render(scene, camera);
}
animate();

在这个例子中,我们没有使用AnimationClipAnimationMixer,而是直接在animate函数中更新了对象的属性。这种方法对于简单的动画效果来说足够了,但如果你需要更复杂的交互或序列动画,那么使用AnimationMixer会更有优势,因为它提供了更多的控制和混合选项。

Tween.js动画

Tween.js是一个独立的JavaScript库,用于创建平滑的过渡效果,它可以与Three.js结合使用来制作对象的动画。

Tween.js动画基础

javascript 复制代码
// 引入Tween.js
import { Tween } from 'three/examples/jsm/libs/tween.module.js';

// 创建一个Three.js对象,例如一个立方体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);

// Tween.js动画
const tween = new TWEEN.Tween(cube.position) // 选择要动画化的属性
  .to({ x: 5, y: 5, z: 5 }, 2000) // 动画的目标位置,持续时间2秒
  .easing(TWEEN.Easing.Quadratic.InOut) // 选择缓动函数,这里是InOut二次方
  .onUpdate(() => { // 更新回调,在每一帧时调用
    scene.remove(cube);
    scene.add(cube); // 重新添加对象以触发渲染
  })
  .start(); // 开始动画

// 更新循环
function animate() {
  requestAnimationFrame(animate);
  TWEEN.update(); // 更新Tween.js动画
  renderer.render(scene, camera);
}

animate();

在这个例子中:

  1. 首先,我们创建了一个立方体,并将其添加到场景中。
  2. 接下来,我们创建了一个Tween实例,指定要动画化的对象(这里是立方体的位置)和目标位置。to方法接收一个目标属性的对象和动画的持续时间。
  3. 我们选择了Quadratic.InOut缓动函数,这是一种常见的缓动模式,使得动画在开始和结束时慢速,中间快速。
  4. onUpdate回调函数会在每一帧动画更新时调用。在这里,我们通过移除并重新添加立方体来强制Three.js重新渲染,因为仅改变对象的位置并不会自动触发渲染。
  5. 最后,我们设置了动画循环,每帧调用TWEEN.update()来更新所有活动的Tween对象。
  6. 请注意,如果你使用的是最新的Three.js版本,可能需要确保Tween.js与Three.js的模块系统兼容,因此可能需要导入tween.module.js而不是旧版的tween.min.js

旋转动画(Rotation)

对于旋转动画,我们可以使用Quaternion来控制对象的旋转。Quaternion是一种数学概念,用于表示3D空间中的旋转。以下是如何动画化旋转的示例:

javascript 复制代码
// 初始旋转
const startRotation = new THREE.Quaternion();

// 目标旋转
const endRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI * 2); // 顺时针旋转一周

// Tween.js动画
new TWEEN.Tween(startRotation)
  .to(endRotation, 2000) // 持续2秒
  .easing(TWEEN.Easing.Linear.None) // 使用线性缓动
  .onUpdate(() => {
    cube.quaternion.copy(startRotation); // 将新的旋转应用于立方体
    scene.remove(cube);
    scene.add(cube); // 强制渲染
  })
  .start();

颜色动画(Color)

动画化颜色可以使用Color对象。以下是如何改变对象颜色的示例:

javascript 复制代码
// 初始颜色
const startColor = new THREE.Color(0x00ff00); // 绿色
cube.material.color.copy(startColor);

// 目标颜色
const endColor = new THREE.Color(0xff0000); // 红色

// Tween.js动画
new TWEEN.Tween(startColor)
  .to(endColor, 2000) // 持续2秒
  .easing(TWEEN.Easing.Linear.None) // 使用线性缓动
  .onUpdate(() => {
    cube.material.color.copy(startColor); // 将新的颜色应用于材质
    scene.remove(cube);
    scene.add(cube); // 强制渲染
  })
  .start();

动画颜色时,需要确保材质支持颜色更改。在这个例子中,我们假设cube的材质是可变颜色的,如MeshBasicMaterialMeshStandardMaterial

鼠标与键盘控制

在Three.js中,为了实现鼠标和键盘控制,通常会使用OrbitControls来处理鼠标的旋转、平移和缩放,以及自定义事件监听器来处理键盘输入。

1. 引入和设置 OrbitControls

首先,确保你的项目中包含了OrbitControls.js,这通常是Three.js的一个额外文件,位于examples/jsm/controls/OrbitControls.js

javascript 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// 创建场景、相机、渲染器等...
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 创建 OrbitControls 实例并关联到相机和渲染器的DOM元素
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼效果,让旋转更加平滑
controls.enablePan = true; // 允许平移
controls.enableZoom = true; // 允许缩放

2. 键盘控制

为了处理键盘输入,可以使用window.addEventListener来监听键盘按下和释放事件。下面是一个简单的示例,演示如何在按下箭头键时改变立方体的位置:

javascript 复制代码
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;

document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);

function onKeyDown(event) {
  switch (event.keyCode) {
    case 87: // W
      moveForward = true;
      break;
    case 83: // S
      moveBackward = true;
      break;
    case 65: // A
      moveLeft = true;
      break;
    case 68: // D
      moveRight = true;
      break;
  }
}

function onKeyUp(event) {
  switch (event.keyCode) {
    case 87: // W
      moveForward = false;
      break;
    case 83: // S
      moveBackward = false;
      break;
    case 65: // A
      moveLeft = false;
      break;
    case 68: // D
      moveRight = false;
      break;
  }
}

// 在渲染循环中应用键盘控制逻辑
function animate() {
  requestAnimationFrame(animate);

  if (moveForward) camera.position.z -= 1; // 向前移动
  if (moveBackward) camera.position.z += 1; // 向后移动
  if (moveLeft) camera.position.x -= 1; // 向左移动
  if (moveRight) camera.position.x += 1; // 向右移动

  controls.update(); // 必须在每次渲染前更新 controls
  renderer.render(scene, camera);
}
animate();

这段代码中,onKeyDownonKeyUp函数分别处理键盘按键按下和释放的事件,而animate函数在每一帧更新时检查这些标志并相应地移动相机。注意,为了使OrbitControls与自定义的键盘控制协同工作,需要在每次渲染前调用controls.update()

3. 平滑移动效果

为了使相机移动更加平滑,我们可以引入Tween.js来动画化相机的位置变化,替代直接修改相机位置。

javascript 复制代码
import { Tween, Easing } from 'gsap'; // 或者使用 'tween.js',但推荐使用gsap的TweenMax/TimelineMax,功能更强大

function updateCameraPosition(newPosition, duration = 900) {
  const currentPos = { x: camera.position.x, y: camera.position.y, z: camera.position.z };
  const targetPos = { x: newPosition.x, y: newPosition.y, z: newPosition.z };

  new Tween(currentPos)
    .to(targetPos, duration)
    .easing(Easing.Linear.None)
    .onUpdate(() => {
      camera.position.set(currentPos.x, currentPos.y, currentPos.z);
    })
    .start();
}

然后在键盘事件处理中使用这个函数来平滑移动相机:

javascript 复制代码
if (moveForward) updateCameraPosition(new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z - 5));
// 类似地处理其他方向
4. 限制相机移动范围
为了防止相机超出预设区域,可以在updateCameraPosition函数中添加边界检查:

javascript
function updateCameraPosition(newPosition, duration = 900) {
  // 添加边界检查
  newPosition.x = Math.max(minX, Math.min(maxX, newPosition.x));
  newPosition.y = Math.max(minY, Math.min(maxY, newPosition.y));
  newPosition.z = Math.max(minZ, Math.min(maxZ, newPosition.z));

  // 剩余代码保持不变
}

5. 处理鼠标滚轮事件

为了处理鼠标滚轮缩放,可以通过监听wheel事件来实现:

javascript 复制代码
document.addEventListener('wheel', onWheel, { passive: false });

function onWheel(event) {
  event.preventDefault(); // 阻止默认的滚动行为
  const delta = -event.deltaY * 0.01; // 调整缩放速度
  camera.position.z += delta; // 根据滚轮方向调整相机位置
}

6. 综合示例

将以上改进整合,最终的动画循环和事件监听部分可能看起来像这样:

javascript 复制代码
function animate() {
  requestAnimationFrame(animate);

  controls.update(); // 更新OrbitControls
  renderer.render(scene, camera);
}

function onWheel(event) {
  event.preventDefault();
  const delta = -event.deltaY * 0.01;
  camera.position.z += delta;
  // 可能需要更新OrbitControls的缩放参数
}

// 初始化事件监听和开始动画循环
document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);
document.addEventListener('wheel', onWheel, { passive: false });
animate();

上述代码示例中使用了gsap库的TweenEasing,这是一个强大的动画库,可以替代原生的Tween.js,提供更多的动画控制选项。根据个人偏好和项目需求,可以选择适合的动画库。

相关推荐
有梦想的咕噜2 分钟前
Electron 是一个用于构建跨平台桌面应用程序的开源框架
前端·javascript·electron
yqcoder5 分钟前
electron 监听窗口高端变化
前端·javascript·vue.js
Python私教21 分钟前
Flutter主题最佳实践
前端·javascript·flutter
GDAL39 分钟前
HTML入门教程7:HTML样式
前端·html
生命几十年3万天1 小时前
解决edge浏览器无法同步问题
前端·edge
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的校园美食分享平台
java·开发语言·前端·vue.js·spring boot·java-ee·美食
API199701081102 小时前
京东平台接口技术详解及示例代码
开发语言·前端·python
前端热爱者2 小时前
axios post请求body为字符串时的解决方法
开发语言·前端·javascript
Monly212 小时前
JS:JSON操作
前端·javascript·json
zerotower3 小时前
nextjs 构建自己的react组件库(一) - 项目的初始化配置和部署
前端·next.js