「Deep Sea Trench」の技術解説

2019.8.17

概要

WebGL(three.js)でつくられた作品「Deep Sea Trench」の技術解説をする。
主に、コードの処理について説明文を加えたり、画像を交えながら書いていく。

目次

  • 使用技術
  • 解説
    • メタボール表現について
    • 海底の表現について
    • パーティクル表現について
    • 周りに浮かぶ多面体
    • 描画ループ
  • まとめ

使用技術

  • three.js (0.101.0)
  • Tween.js (17.3.0)
  • vertex / fragment shader

解説

メタボール表現について

この作品でメインに見せているメタボール表現だが、実はthree.jsのリポジトリに存在するMarchingCubes.jsを使っている。

three.jsのexampleとして存在しており、本体には内包されていないため、本作品ではコードを抜粋してそのまま貼り付けて使用している。

THREE.MarchingCubes()インスタンスをsceneに追加

準備としては、MaterialをつくりTHREE.MarchingCubes()インスタンスをsceneに追加するだけ。

const material = new THREE.MeshPhongMaterial({
    color: 0xffcc00
});

const resolution = 48;
marchingCubes = new THREE.MarchingCubes(resolution, material, true, true);

scene.add(marchingCubes);

今後はこのmarchingCubesを使ってメタボールを描く処理を追加していく。

メタボールの描画

blbv2ac3p9f76ruq9i20.png

THREE.MarchingCubesをsceneに追加しただけではメタボールは描画されないので、renderループの中で更新処理を行う必要がある。
コード中のupdateCubes関数の処理だ。

// metaballの更新
const updateCubes = (marchingCubes, time) => {
    // メタボールをリセット
    marchingCubes.reset();

    let i, ballx, bally, ballz, subtract, strength;
    subtract = 18;
    strength = 0.5;

    for (i = 0; i < 30; i++) {
        // メタボールを30個追加
        // xyzの位置を計算(0.0 ~ 1.0, 0.5が中心)
        ballx = Math.sin(i + 1.26 * time * (1.03 + 0.5 * Math.cos(0.21 * i))) * 0.27 + 0.5;  // 有効範囲は0.0 ~ 1.0になるので、この範囲に収まるよう調整
        bally = Math.cos(i + 1.12 * time * Math.cos(1.22 + 0.1424 * i)) * 0.27 + 0.5;
        ballz = Math.cos(i + 1.32 * time * 0.1 * Math.sin((0.92 + 0.53 * i))) * 0.27 + 0.5;
        marchingCubes.addBall(ballx, bally, ballz, strength, subtract);
    }
};

// render loop
const render = () => {
    clock.getDelta();
    const time = clock.elapsedTime;

    updateCubes(marchingCubes, time * updatingCubeSpeedOffset);

    ...

    requestAnimationFrame(render);
};

コードを見ての通り、流れとしては毎回メタボールをリセットして、新たな位置を計算するということをしている。

追加された30個のボールの位置から形状を補完してメタボールにする処理はMarchingCubesが行なっている。

ちなみに、MarchingCubes.addBall()の第1 ~ 3引数の値は0.0 ~ 1.0になるので、ballx, bally, ballz = 0.5;の場合は、メタボールは画面中央になる。
30個のメタボールを描画しているが全て同じ位置にあるので、一つの球に見える。

blbva4s3p9f76ruq9i2g.png

作中ではTHREE.MarchingCubesに指定するMaterialはTHREE.ShaderMaterialを用いてシェーダーでメタボール表面の模様を描くようにしている。

これでメタボールの描画については終わり。

海底の表現について

blbvlgs3p9f76ruq9i3g.png

地面(海底)となる部分の描画について説明する。

Meshの作成

まずは普通のMeshを作ってsceneに追加。
海底の砂っぽいノイズが欲しかったので、テクスチャを貼ることにした。

groundGeometry = new THREE.PlaneBufferGeometry(20000, 20000, 128, 128);
groundGeometry.rotateX(-Math.PI / 2);  // XZ平面になるよう回転

// テクスチャを読み込む
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('https://textureurl/img.jpg');  // NEORTでファイルをアップロードして参照する機能はまだないので、オンラインの画像URLを指定
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(5, 5);

const groundMaterial = new THREE.MeshBasicMaterial({
    color: 0x00E4BB,
    map: texture,
});

groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.position.set(0, -500, 0);
scene.add(groundMesh);

ウニウニさせる

ジオメトリのpointのY座標をアニメーションさせることでウニウニ動く表現をしている。
コード中の関数はupdateGround

const updateGround = (time) => {
    const position = groundGeometry.attributes.position;

    for (let i = 0, len = position.count; i < len; i++) {
           // pointの値をベースにタイミングをずらす
        let y = 30 * Math.sin(i / 2 + (time * 5 + i));

        // こちらも特定の周期で値をずらすことで不規則性を出している
        if (i % 14 === 0) {
            y *= groundVertexOffset;
        }
        position.setY(i, y);
    }

    position.needsUpdate = true;
};

遠方をぼかす(fog)

scene.background = new THREE.Color(0x4623DE);
scene.fog = new THREE.FogExp2(0x4623DE, 0.0003);

背景に使った色を同じ色をTHREE.FogExp2()に指定してあげれば自然なfogができる。

パーティクル表現について

blbvvjs3p9f76ruq9i40.png

宙に舞ってるパーティクル表現。
これはTHREE.PointsTHREE.AdditiveBlendingを指定したTHREE.ShaderMaterialで実現している。

const vertices = [];
const colors = [];
const particleCount = 20000;

const geometry = new THREE.BufferGeometry();
const dist = window.innerWidth * 0.8;

for (let i = 0; i < particleCount; i++) {
    const x = Math.floor(Math.random() * dist - dist / 2);
    const y = Math.floor(Math.random() * dist - dist / 2);
    const z = Math.floor(Math.random() * dist - dist / 2);
    vertices.push(x, y, z);

    colors.push(1, 0, 0);
}

const verticesArray = new Float32Array(vertices);
geometry.addAttribute('position', new THREE.BufferAttribute(verticesArray, 3));

const colorsArray = new Float32Array(colors);
geometry.addAttribute('color', new THREE.BufferAttribute(colorsArray, 3));

const material = new THREE.ShaderMaterial({
    uniforms: uniform,
    vertexShader: particleVertexShader,
    fragmentShader: particleFragmentShader,
    depthWrite: false,  // (2)
    blending: THREE.AdditiveBlending  // (1)
});

particlePoints = new THREE.Points(geometry, material);
scene.add(particlePoints);

(1)blending: THREE.AdditiveBlendingを設定しているが、これをしないと各pointの矩形領域が描画されてしまう。

blc05sk3p9f76ruq9i4g.png

また、(2)depthWrite: falseを指定しているが、これは重なる領域を描画するかどうかの設定で、重なる部分の描画を許可するようにしてある。
これがtrueになっていると奥行きを考慮して重なって背景に位置している部分を描画しなくなるのでコスト的には良いが、今回のようにblending: THREE.AdditiveBlendingを使って矩形領域を透過させている場合は他のパーティクルが前面のパーティクルの矩形領域で切り取られるように見えてしまうので、falseにしておいた。

光のオーブの表現

オーブはfragment shaderで描いている。

float orb = 0.1 / length(uv) * step(0.5, 1.0 - length(uv));
orb = smoothstep(0.0, 1.0, orb);
vec3 color = vec3(orb) * vColor;

なんてことはないシェーダーだが、step(0.5, 1.0 - length(uv))をかけてオーブの縁を敢えてぼかさずにパキッとした光のような感じを出した。

周りに浮かぶ多面体

THREE.IcosahedronGeometry()を使ってMeshをつくり、海底の表現と同様にジオメトリのpoint座標を動かしているだけなので説明は割愛する。

描画ループ

特に変わったことはしておらず、カメラを回したり、それぞれの描画処理を呼び出しているだけなので、ここも割愛。

まとめ

本作品はTokyoDemoFest2018の出展作品で、複数シーンを切り替えたりサウンドシェーダーを入れていたオリジナルから一部抜粋したものになっているため、コードが結構煩雑かもしれない。

技術的には既存のリソースを使わせてもらいつつ、シェーダーで味付けしたりといったオーソドックスな感じなので、特にテクニカルなことをしている訳ではないが、Webでの3D表現としてはまぁまぁ使えるものになってるんじゃないかと思う。

この記事が誰かの役に立てば嬉しいな。

You can support this creator by paying money.

Commercial use NG