2023-03-14

[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();
});

1 則留言:

  1. Here all content so useful and helpful for beginner and experience both.This site is so amazing,This sites gives good knowledge of Threejs.This is very helpful for me.

    回覆刪除

你好!歡迎你在我的 Blog 上留下你寶貴的意見。