threejs学习笔记(五)

threejs在页面内的应用

透明背景

默认情况下,使用threejsrenderer渲染出来的背景是黑色的,可以通过给renderer设置alpha: true来让其背景透明

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha: true
})

通常情况下我们应该给页面html、body元素设置背景色或background-img,而不是在threejs里给scene设置背景色或背景图片,否则可能会某些用户的电脑上出现滚动bug

页面滚动摄像机跟随

在这里插入图片描述
核心代码:

let scrollY = window.scrollY;
window.addEventListener("scroll", () => { // 1. 监听滚动事件
    scrollY = window.scrollY;
});

const tick = () => {
  // 按比例移动摄像机位置
  camera.position.y = (-scrollY / window.innerHeight) * objectDistance;
  // Render
  renderer.render(scene, camera);
  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};
  1. 监听滚动事件,记录下滚动的像素
  2. 计算滚动的比例,移动摄像机位置

objectDistance代表几何体与几何体之间的距离,初始生成几何体时,它们均坐落在原点位置,上例中,修改几何体的位置为:

第一个几何体:(2, 0, 0)
第二个几何体:(-2, -4, 0)
第三个几何体:(2, -8, 0)

所以,随着页面滚动,摄像机移动的数值应为-已滚动的像素 / 视口高度 * 几何体间距

初始时,摄像机在Y轴的位置为0,页面滚动的值是从0 -> ∞的,所以滚动行为一开始,就要对摄像机移动的距离取负数

物体进入视野判断

物体滚入视野后添加一段旋转动画在这里插入图片描述
在当前案例中,几何体之间的间距是固定的,在页面上的体现约等于视口的高度,所以要判断屏幕滚动到了第几个几何体,可以通过对滚动距离 / 视口高度进行四舍五入来判断,如:

// ...
const sectionMeshes = [mesh1, mesh2, mesh3];
scene.add(mesh1, mesh2, mesh3);

let scrollY = window.scrollY;
let before = 0
window.addEventListener("scroll", () => {
    scrollY = window.scrollY;
    // 四舍五入判断滚动到了第几个几何体
    const current=Math.round(scrollY /sizes.height)
    if (current !== before) {
        before = current
        // 使用gsap库来滚动对应的几何体
        gsap.to(sectionMeshes[current].rotation, {
            duration: 1.5,
            ease: 'power2.inOut',
            x: "+=6",
            y: "+=3",
        })
    }
});

鼠标移动摄像机跟随

鼠标移动时,场景跟随移动
在这里插入图片描述
鼠标移动的同步也是通过修改摄像机的位置来实现的,但是上例中已经在每一帧里修改了camera.position,如果鼠标移动也修改camera.position就无法兼容同时滚动且鼠标移动的场景

要解决这个问题,可以将摄像机放进一个Group里,在鼠标移动时,修改这个Group的位置,即可避免页面滚动时修改摄像机位置的冲突,如:

const camera = new THREE.PerspectiveCamera(
  35,
  sizes.width / sizes.height,
  0.1,
  100
);
camera.position.z = 6;
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
scene.add(cameraGroup);

然后到核心实现,要实现鼠标移动的同步,本质就是监听mousemove事件,将屏幕的鼠标位移转为threejs坐标系内的移动幅度,如:

const cursor = {};
cursor.x = 0;
cursor.y = 0;
window.addEventListener("mousemove", (event) => {
  cursor.x = event.clientX / sizes.width - 0.5;
  cursor.y = event.clientY / sizes.height - 0.5;
});

在页面中,鼠标从左上角开始,水平和垂直方向的移动都是从0到正,但在threejs坐标系中,从左上角开始水平和垂直方向应该是负到正,所以应该用鼠标在屏幕上的位移 / 视口宽高获取到鼠标移动的幅度(0到1),减去0.5即为鼠标在threejs坐标系内实际移动的幅度

所以,每一帧中要处理的代码如下

const tick = () => {
  const parallaxX = cursor.x;
  const parallaxY = -cursor.y;
  cameraGroup.position.x = parallaxX 
  cameraGroup.position.y = parallaxY 
  
  // Render
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

注意在Y轴方向的取负数操作,因为我们移动的是摄像机,所以相对于原点而言,水平方向鼠标往左 = 摄像机往左 = 物体往右移动

假如Y轴不取反,相对于原点而言,就等于垂直方向鼠标往上 = 摄像机往下 = 物体往上移动

所以为了便于用户理解,要么对X轴取反,要么对Y轴取反,但是对Y轴取反才更符合人对于视角的认知

平滑移动

按照上面的代码实现后会发现,视角移动起来比较“僵硬”,如图:
在这里插入图片描述

在现实生活中,人移动摄像机或者将摄像机架在滑轨上移动都不可能实现一步到位的效果,甚至会因为惯性的存在,让摄像机移动后有一点滞后性的效果

所以,为了优化用户体验,我们可以优化摄像机在每一帧移动的实现,比如:

let previousTime = 0;
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - previousTime;
  previousTime = elapsedTime;
  const parallaxX = cursor.x;
  const parallaxY = cursor.y;
  cameraGroup.position.x += (parallaxX - cameraGroup.position.x) *  deltaTime;
  cameraGroup.position.y += (parallaxY - cameraGroup.position.y) *  deltaTime;
  //   cameraGroup.position.x = parallaxX 
  //   cameraGroup.position.y = parallaxY 
  // Render
  renderer.render(scene, camera);
  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};
tick();

对代码的解释如下:

首先,平滑移动其实就是在移动速度上呈现出先快后慢,越来越慢的效果

deltaTime是上一帧到当前帧的时间差,基本上等于一个固定值,通过控制台可以看到其变化:
在这里插入图片描述
计算的基准量是parallexX - cameraGroup.position.x,开始时,parallexX = 0.5cameraGroup.position.x = 0,此时(parallaxX - cameraGroup.position.x) * deltaTime;计算出来的值最大,摄像机在第一帧要加上的移动的距离最大

随着逐帧的计算进行,因为每次加上的值是一个“差值”,差值会越来愈小,所以每帧移动的幅度会越来越小,从控制台打印也可以看出这种趋势:
在这里插入图片描述
先快后慢,越来越慢的效果实现了,但是快与慢的区别还不够明显,所以这里可以再调整一下变化的幅度,通过乘上一个倍数来实现,比如5倍:

  cameraGroup.position.x += (parallaxX - cameraGroup.position.x) *  deltaTime * 5;
  cameraGroup.position.y += (parallaxY - cameraGroup.position.y) *  deltaTime * 5;

效果是很明显的:
在这里插入图片描述

完整的代码实现:

import * as THREE from "three";
import GUI from "lil-gui";
import gsap from "gsap";
/**
 * Debug
 */
const gui = new GUI();

const parameters = {
  materialColor: "#ffeded",
};

gui.addColor(parameters, "materialColor").onChange(() => {
  material.color.set(parameters.materialColor);
  // particleMaterial.color.set(parameters.materialColor)
});

/**
 * Base
 */
// Canvas
const canvas = document.querySelector("canvas.webgl");

// Scene
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(30));

// TEXTURE
const textureLoader = new THREE.TextureLoader();
const gradientTexture = textureLoader.load("textures/gradients/3.jpg");
gradientTexture.magFilter = THREE.NearestFilter;

// Objects
const objectDistance = 4;
const material = new THREE.MeshToonMaterial({
  color: parameters.materialColor,
  gradientMap: gradientTexture,
});

const mesh1 = new THREE.Mesh(new THREE.TorusGeometry(1, 0.4, 16, 60), material);
const mesh2 = new THREE.Mesh(new THREE.ConeGeometry(1, 2, 32), material);
const mesh3 = new THREE.Mesh(
  new THREE.TorusKnotGeometry(0.8, 0.35, 100, 16),
  material
);

mesh1.position.x = 2;
mesh2.position.x = -2;
mesh3.position.x = 2;

mesh1.position.y = -objectDistance * 0;
mesh2.position.y = -objectDistance * 1;
mesh3.position.y = -objectDistance * 2;

const sectionMeshes = [mesh1, mesh2, mesh3];
scene.add(mesh1, mesh2, mesh3);

// particles

const particlesCount = 200;
const positions = new Float32Array(particlesCount * 3);
const colors = new Float32Array(particlesCount * 3);
for (let i = 0; i < particlesCount; i++) {
  const i3 = i * 3;
  positions[i3] = (Math.random() - 0.5) * 10;
  positions[i3 + 1] =
    objectDistance * 0.5 -
    Math.random() * objectDistance * sectionMeshes.length;
  positions[i3 + 2] = (Math.random() - 0.5) * 10;

  colors[i3] = Math.random();
  colors[i3 + 1] = Math.random();
  colors[i3 + 2] = Math.random();
}
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute(
  "position",
  new THREE.BufferAttribute(positions, 3)
);
particleGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const particleMaterial = new THREE.PointsMaterial({
  // color: parameters.materialColor,
  sizeAttenuation: true,
  size: 0.2,
  vertexColors: true,
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);

// lights
const directionalLight = new THREE.DirectionalLight("#ffffff", 1);
directionalLight.position.set(1, 1, 0);
scene.add(directionalLight);
scene.add(new THREE.DirectionalLightHelper(directionalLight));

/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

window.addEventListener("resize", () => {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  35,
  sizes.width / sizes.height,
  0.1,
  100
);
camera.position.z = 6;
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
scene.add(cameraGroup);

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  alpha: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

/**
 * Animate
 */
const clock = new THREE.Clock();

// cursor
const cursor = {};
cursor.x = 0;
cursor.y = 0;
window.addEventListener("mousemove", (event) => {
  cursor.x = event.clientX / sizes.width - 0.5;
  cursor.y = event.clientY / sizes.height - 0.5;
});

// scroll
let scrollY = window.scrollY;
let before = 0
window.addEventListener("scroll", () => {
    scrollY = window.scrollY;
    const current=Math.round(scrollY /sizes.height)
    if (current !== before) {
        before = current
        gsap.to(sectionMeshes[current].rotation, {
            duration: 1.5,
            ease: 'power2.inOut',
            x: "+=6",
            y: "+=3",
        })
    }
});
let previousTime = 0;
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - previousTime;
  previousTime = elapsedTime;
  camera.position.y = (-scrollY / sizes.height) * objectDistance;

  const parallaxX = cursor.x;
  const parallaxY = cursor.y;
  cameraGroup.position.x += (parallaxX - cameraGroup.position.x) *  deltaTime * 5;
  cameraGroup.position.y += (parallaxY - cameraGroup.position.y) *  deltaTime * 5;

  // Render
  renderer.render(scene, camera);
  sectionMeshes.forEach((mesh) => {
    mesh.rotation.x += deltaTime * 0.1;
    mesh.rotation.y += deltaTime * 0.12;
  });
  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();

物理效果

一般的物理效果都可以理解成物体的位置以某种规律或运动曲线进行运动,从而看起来像是现实世界中的物理效果

所以,页面上的物理效果实际上就是在每一帧更新物体的位置从而实现物理效果,关键就在于如何获取到每一帧物体的位置

目前市面上成熟的方案是模拟一个物理世界,在物理世界中模拟物体的碰撞、运动等行为,然后在每一时刻获物体模拟取该行为运动的位置,同步更新到threejs的场景中以实现

在这里插入图片描述
比如在上面这张图中,左边是threejs中的场景,右边是假想的一个物理世界,我们在物理世界中模拟小球从空中落下,砸到平面后停止的行为。在这段时间中,我们可以通过物理第三方库提供的API获取到每一帧或每一时刻小球的位置,将位置更新到我们实际的threejs场景中,即可实现所谓的物理效果

下面是常用的一些3D或2D物理效果的库:
3D物理效果:
Ammo.js:没文档,https://github.com/kripken/ammo.js

Cannon.js(推荐):有文档,但是全英,体积小,基于Ammojsthreejs实现的物理库,https://schteppe.github.io/cannon.js/

Oimo.js:没文档,https://lo-th.github.io/Oimo.js/#basic

2D物理效果:
Matter.js(推荐): 比较完善且轻量,全英文档 https://brm.io/matter-js/
P2.js
Planck.js
Box2D.js

3D - 小球落到平面

使用Cannon.js作为示例,实现以下效果:
在这里插入图片描述
首先,按照其实现原理,场景中初始时有一个平面当作”地板“,有一个位于(0,0.5,0)的小球,其半径是0.5(这样就可以让小球初始时处于平面之上),如图:
在这里插入图片描述
然后,按照原理,需要一个物理世界来模拟物体运动,项目中执行npm install cannon安装cannon.js,然后进行初始化:

import CANNON from "cannon";
// 创建一个物理效果世界
const world = new CANNON.World();
// 声明y轴方向的重力,取负数是因为Y轴向下为负
world.gravity.set(0, -9.82, 0);

这里创建了一个物理世界,然后给物理世界设置垂直方向也就是Y轴的加速度为9.82,取负数是因为Y轴向下为负

然后在物理世界中需要创建我们的物理对象,和threejs的场景一样,也需要一个平面和小球,代码如下:

// Sphere
// 创建一个半径0.5的球形状
const sphereShape = new CANNON.Sphere(0.5);
// 使用球形状创建球体
const sphereBody = new CANNON.Body({
  mass: 1, // 球的质量
  position: new CANNON.Vec3(0, 3, 0), // 球的初始位置
  shape: sphereShape,
});
// 将球体放到世界中
world.addBody(sphereBody);

// physic Floor
// 创建一个平面形状
const floorShape = new CANNON.Plane();
// 创建平面“体”
const floorBody = new CANNON.Body({
  mass: 0, // 质量为0 相当于一堵空气墙
  shape: floorShape,
});
world.addBody(floorBody);

现在,在屏幕中,就存在一个threejs的场景(肉眼可见)和一个物理世界了(不可见)

然后,我们需要threejs渲染的每一帧里,在物理世界中触发运动,添加如下代码:

const clock = new THREE.Clock();
let oldElapsedTime = 0;
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - oldElapsedTime;
  oldElapsedTime = elapsedTime;
  // update physics world
  world.step(1 / 60, deltaTime, 3);
  console.log('sphereBody.position', sphereBody.position.y)
  controls.update();
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};

tick();

world.step用于推进物理世界的状态,第一个参数表示固定的逻辑时间步长,即每次step调用之间模拟的时间间隔,通常设为1 / 60,表示每秒60次更新,即符合屏幕的刷新率

第二个参数代表实际经过的时间,通常设为上一帧与当前帧的时间差,可以用来更准确地模拟物理状态的变化

第三个参数是可选的, 表示在每一帧内进行的子步骤数量,增加这个数量可以在物理模拟过程中提供更高的精度

通过打印物理世界中小球的位置(sphereBody.position.y)可以看到具体数值的变化,它的值从3一直无线递减:
在这里插入图片描述
这是因为当前物理世界中,并没有东西将他挡住或,所以它会无限地朝着虚空进行下落,虽然我们之前创建了一个地板floor,但是floor默认情况是下垂直于X轴的,这个和threejs是一样的(初始化创建的plane也是竖直的)

所以,我们需要在物理世界中对屏幕进行旋转,使其能够平行于X轴,添加代码如下:

// 创建一个平面形状
const floorShape = new CANNON.Plane();
// 创建平面“体”
const floorBody = new CANNON.Body({
  mass: 0, // 质量为0 相当于一堵空气墙
  shape: floorShape,
});
// 旋转平面使其水平(初始时默认是垂直的)
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI / 2);

在物理世界旋转物体比较复杂,需要使用四元数来实现,不懂暂时也没事,这时候再打印就会发现数值是从3到0.5左右保持了:
在这里插入图片描述
最后,将这个数值和threejs里的数值关联起来,即可实现小球的掉落效果了:

 // update physics world
 world.step(1 / 60, deltaTime,3);
 // 根据物理世界的值更新threejs里球的位置
 sphere.position.copy(sphereBody.position);
好的,我会为您解答关于WebGL three.js的阴影与实现物体动画的问题。首先,让我们来了解一下WebGL three.js是什么。 WebGL three.js是一款基于WebGL的JavaScript 3D库,可以帮助我们快速搭建3D场景和应用。接下来我们来讲解阴影和实现物体动画的方法。 一、阴影 阴影是模拟物体之间的阴影效果,让3D场景更加真实。在three.js中,我们可以通过设置Mesh的castShadow和receiveShadow属性来实现阴影效果。 1. 首先,我们需要在场景中添加光源,例如SpotLight或DirectionalLight。 2. 然后,在需要投射阴影的物体上设置castShadow为true。 3. 最后,在需要接收阴影的物体上设置receiveShadow为true。 代码示例: ```javascript // 添加光源 const light = new THREE.SpotLight(0xffffff); light.position.set(0, 100, 0); light.castShadow = true; scene.add(light); // 添加需要投射阴影的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.castShadow = true; scene.add(cube); // 添加需要接收阴影的物体 const plane = new THREE.Mesh(new THREE.PlaneGeometry(200, 200, 1, 1), new THREE.MeshLambertMaterial({ color: 0xffffff })); plane.receiveShadow = true; plane.rotation.x = -Math.PI / 2; scene.add(plane); ``` 二、物体动画 在three.js中,我们可以通过Tween.js库来实现物体的动画效果。Tween.js是一款JavaScript动画库,可以帮助我们实现非常丰富的动画效果。 1. 首先,我们需要在HTML文件中引入Tween.js库文件。 2. 然后,在需要动画的物体上设置初始状态。 3. 最后,通过Tween.js库来设置物体的目标状态和动画效果,例如缓动动画(ease)或弹跳动画(bounce)。 代码示例: ```javascript // 引入Tween.js库文件 <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.min.js"></script> // 添加需要动画的物体 const cube = new THREE.Mesh(new THREE.BoxGeometry(10, 10, 10), new THREE.MeshLambertMaterial({ color: 0xff0000 })); cube.position.set(0, 0, 0); scene.add(cube); // 设置初始状态 const start = { x: 0, y: 0, z: 0 }; // 设置目标状态 const end = { x: 50, y: 50, z: 50 }; // 设置动画效果 const tween = new TWEEN.Tween(start) .to(end, 1000) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { cube.position.set(start.x, start.y, start.z); }) .start(); ``` 以上是关于WebGL three.js阴影与实现物体动画的方法,希望能够对您有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值