threejs+blender页面滚动伴随着三维动画
这几天在跟着国外的教程学习在网页中利用建好的动画模型如何呈现出来,经过一周的学习,终于可以自己在网页中操作3D模型了。
现在可以点击模型中某个对象改变这个对象的材质
可以利用GASP控制模型的运动
可以控制模型中某些对象的动画暂停和播放
随着页面滚动,模型切换不同的视角
我一周学完了https://threejsfundamentals.org/这个网址里的所有内容,并在https://sbcode.net/threejs/这个网址上补充学习了模型压缩后的处理方式,基本了解了网页中三维动画的原理,虽然对threejs的模型概念还不是很清楚,相信过一段时间会有更深入的了解。中文文档方面也参考了http://www.yanhuangxueyuan.com/Three.js/ 这个网站。
学完之后,就想着找几个案例来模仿一下。于是,找了下面几个参考案例。
1. https://superfanstudio.greenparksports.com/
这个案例看了一下源码,模型都是骨骼动画,还是有些复杂的,过一段时间再补充说明哈。
2. https://ar.egorovagency.com/
这个案例通过滚动插件来切换不同的模型,同时伴随着各种动画。它的模型都是经过压缩处理的,用到了DRACOLoader这个插件。也是比较容易模仿的。
3. https://circus-inc.com/en/
这个案例里提到的模型gltb格式,没有压缩,直接拿下来按着这个动画做了一遍,还是比较容易的,难点在于控制动画什么时候暂停和播放,模型中某个元件的拖拽。这个后续会继续讲。
下面拿第二个案例描述代码:
代码都在node+webpack环境中运行,引用的js和样式通过import导入:
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {GUI} from 'dat.gui'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import gsap from 'gsap'
关联html的canvas
const canvas = document.querySelector('.webgl');
const renderer = new THREE.WebGLRenderer({canvas,alpha: true,});
renderer.shadowMap.enabled = true;
相机初始化
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.01;
const far = 1200;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
控制器
const controls = new OrbitControls(camera, canvas);
controls.update();
新建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color('#DEFEFF');
环境光
const skyColor = 0xffffff; // light blue
const groundColor = 0xB97A20; // brownish orange
const intensity = 1;
const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
scene.add(light);
模型压缩解压处理
var dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('');
dracoLoader.setDecoderConfig({ type: 'js' });
导入gltf格式的模型文件
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load('bear.gltf', (gltf) => {
//模型初始大小位置
gltf.scene.scale.set(.2,.2,.2)
gltf.scene.position.set(3.3,5.4,3.2)
//模型初始化位移缩放动画
t1.to(gltf.scene.scale,{x:.08,y:.08,z:.08,duration:1})
t1.to(gltf.scene.position,{y:5.2,duration:1})
//调用模型动画
var mixer = new THREE.AnimationMixer( gltf.scene );
AnimationAction = mixer.clipAction( gltf.animations[ 0 ] )
// AnimationAction.loop = THREE.LoopOnce;
// AnimationAction.clampWhenFinished = true;
AnimationAction.time = 0;
//gltf.animations[ 0 ].duration = 18;
AnimationAction.play();
// setTimeout(function(){
// AnimationAction.paused = true
// },9000)
// 加入混合器中
mixers.push( mixer );
scene.add(gltf.scene);
gltf.scene.traverse((obj) => {
if (obj.castShadow !== undefined) {
obj.castShadow = true;
obj.receiveShadow = true;
}
});
//更新
gltf.scene.updateMatrixWorld();
// compute the box that contains all the stuff
// from root and below
const box = new THREE.Box3().setFromObject(gltf.scene);
const boxSize = box.getSize(new THREE.Vector3()).length();
const boxCenter = box.getCenter(new THREE.Vector3());
// set the camera to frame the box
frameArea(boxSize * 0.5, boxSize, boxCenter, camera);
// update the Trackball controls to handle the new size
controls.maxDistance = boxSize;
controls.target.copy(boxCenter);
controls.update();
},
(xhr) => {
console.log((xhr.loaded/xhr.total*100)+'% loaded')
},
(error) => {
console.log(error)
}
);
}
//render
let clock = new THREE.Clock();
function render(time) {
time *= 0.001; // convert to seconds
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
var delta = clock.getDelta();
//console.log(mixers.length)
for ( var i = 0; i < mixers.length; i ++ ) { // 重复播放动画
mixers[ 0 ].update( delta );
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
防锯齿
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
调整中心位置
function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
const halfFovY = THREE.MathUtils.degToRad(camera.fov * .5);
const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
// compute a unit vector that points in the direction the camera is now
// in the xz plane from the center of the box
const direction = (new THREE.Vector3())
.subVectors(camera.position, boxCenter)
.multiply(new THREE.Vector3(1, 0, 1))
.normalize();
// move the camera to a position distance units way from the center
// in whatever direction the camera was from the center already
camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
// pick some near and far values for the frustum that
// will contain the box.
camera.near = boxSize / 100;
camera.far = boxSize * 100;
camera.updateProjectionMatrix();
// point the camera to look at the center of the box
camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
}
点击某个元件
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick( event ) {
//通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera( mouse, camera );
// 获取raycaster直线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects( scene.children,true );
//console.log(intersects);
//将所有的相交的模型的颜色设置为红色,如果只需要将第一个触发事件,那就数组的第一个模型改变颜色即可
for ( var i = 0; i < intersects.length; i++ ) {
intersects[ i ].object.material.color.set( 0xff0000 );
console.log(intersects[ i ].object.name)
if(intersects[ i ].object.name==='ball_pachi'){
AnimationAction.paused = false
}
//intersects[ i ].object.material.color.set( 0xff0000 );
// if(intersects[ i ].object.name=="brain"){
// intersects[ i ].object.material.color.set( 0xff0000 );
// }
}
}
window.addEventListener( 'click', onMouseClick, false );