「Deep Sea Trench」の技術解説
2019.8.17
WebGL(three.js)でつくられた作品「Deep Sea Trench」の技術解説をする。
主に、コードの処理について説明文を加えたり、画像を交えながら書いていく。
この作品でメインに見せているメタボール表現だが、実は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
を使ってメタボールを描く処理を追加していく。
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個のメタボールを描画しているが全て同じ位置にあるので、一つの球に見える。
作中ではTHREE.MarchingCubes
に指定するMaterialはTHREE.ShaderMaterial
を用いてシェーダーでメタボール表面の模様を描くようにしている。
これでメタボールの描画については終わり。
地面(海底)となる部分の描画について説明する。
まずは普通の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;
};
scene.background = new THREE.Color(0x4623DE);
scene.fog = new THREE.FogExp2(0x4623DE, 0.0003);
背景に使った色を同じ色をTHREE.FogExp2()
に指定してあげれば自然なfogができる。
宙に舞ってるパーティクル表現。
これはTHREE.Points
とTHREE.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の矩形領域が描画されてしまう。
また、(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