2023-03-14 13:23

[ThreeJS] 噴水粒子

要完成這個效果有三個問題要解決

  1. 自然的粒子分布
  2. 粒子的拋物線移動
  3. 粒子的擴散效果

我採用簡單的方式達成

  1. 用亂數產生分段的粒子團
  2. 用二次貝茲曲線建立拋物線
  3. 用粒子團的放大產生擴散效果

因為我用簡單的亂數分布粒子,粒子團會有立方體現象,球體分布會更好,但多個區段連起來就不明顯了。

二次貝茲曲線最大的困難在計算出結尾座標的位置,透過轉換到世界空間去定位出平面高度的座標,再轉換回物體空間來計算出結尾座標。

動畫用 AnimationMixer 去控制,只要定出每段粒子團的開始播放時間,粒子團就會接續的移動。

circle.png

程式:

/* 初始化渲染器 */
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.getElementById('Container').appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

/* 初始化場景 */
const scene = new THREE.Scene();
scene.background = new THREE.Color('#111'); /* 背景顏色 */
scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */

/* 地面 */
const floorMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshBasicMaterial({ color: '#DDD', depthWrite: false })
);
floorMesh.rotation.x = THREE.MathUtils.degToRad(-90);
scene.add(floorMesh);


/* 初始化鏡頭 */
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 10, 22);

/* 初始化軌道控制,鏡頭的移動 */
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.update();


/* 動畫刷新器 */
const mixers = [];

function newMixer(target) {
    const mixer = new THREE.AnimationMixer(target);
    mixers.push(mixer);
    return mixer;
}

/* 動畫計時器 */
const clock = new THREE.Clock();

/* 渲染週期 */
function renderCycle() {
    const delta = clock.getDelta();
    mixers.forEach(x => x.update(delta));

    renderer.render(scene, camera);
    requestAnimationFrame(renderCycle);
}
renderCycle();



/*---------------------------------------------------------------*/

/* 水管物件,水滴粒子群組會附加在這裡 */
const pipeMesh = new THREE.Mesh(
    new THREE.CylinderGeometry(0.5, 0.5, 9, 16),
    new THREE.MeshBasicMaterial( {color: '#eea236'} )
);
pipeMesh.rotation.x = THREE.MathUtils.degToRad(0);
pipeMesh.position.set(0, 4, 0);
scene.add(pipeMesh);


const dripGroup = new THREE.Group();
dripGroup.position.set(0, 4, 0);
pipeMesh.add(dripGroup);


/* 水滴材質 */
const dripMaterial = new THREE.PointsMaterial({
    map: new THREE.TextureLoader().load('circle.png'),
    color: '#0aa',
    size: 1,
    opacity: 0.7,
    depthWrite: false,
    transparent: true,
});


/* 建立水滴,用亂數建立粒子點 */
for (let i = 0; i < 60; i++) {
    const vertices = [];

    for (let j = 0; j < 40; j++) {
        const x = Math.random() - 0.5;
        const y = Math.random() - 0.5;
        const z = Math.random() - 0.5;
        vertices.push(x, y, z);
    }

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

    const particles = new THREE.Points(geometry, dripMaterial);
    dripGroup.add(particles);

    newMixer(particles); /* 水滴動畫 */
}


/*---------------------------------------------------------------*/

const cycle = 2; /* 循環週期,會影響水流速度 */
const scale = 8; /* 擴散大小 */
const length = 14; /* 水流長度 */


/* 二次貝茲曲線,用來取得拋物線的座標點 */
const curve = new THREE.QuadraticBezierCurve3();
curve.v1.set(0, length, 0); /* 曲線控制點座標 */


/* 計算結尾座標 */
const rootPos = dripGroup.getWorldPosition(new THREE.Vector3());
const quaternion = dripGroup.getWorldQuaternion(new THREE.Quaternion());
const toPos = curve.v1.clone();
toPos.applyQuaternion(quaternion); /* 轉換到世界空間 */

/* 當水流是向上時,增加平面位置 */
if (toPos.y > (length / 3)) {
    toPos.x *= 1.8;
    toPos.z *= 1.8;
}
toPos.y = Math.min(-rootPos.y, toPos.y * 1.5); /* 將結尾拉回平面高度 */
toPos.applyQuaternion(quaternion.conjugate()); /* 轉換回物體空間 */
curve.v2.copy(toPos); /* 曲線結尾點座標 */


/* 建立拋物線及擴散動畫 */
const points = curve.getPoints(10); /* 曲线分段 */

const curveTime = [];
const curvePos = [];
points.forEach((v, i) => {
    curveTime.push(cycle * i / points.length);
    curvePos.push(v.x, v.y, v.z);
});
const posTrack = new THREE.VectorKeyframeTrack('.position', curveTime, curvePos);
const scaleTrack = new THREE.VectorKeyframeTrack('.scale', [0, cycle], [0, 0, 0, scale, scale, scale]);
const clip = new THREE.AnimationClip('scale', cycle, [posTrack, scaleTrack]);

mixers.forEach((mixer, i) => {
    const action = mixer.clipAction(clip);
    action.time = cycle * i / mixers.length;
    action.play();
});

0 回應: