Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
79
Help us understand the problem. What is going on with this article?
@ukonpower

GPUラーメンを作ろう

More than 1 year has passed since last update.

概要

スクリーンショット 2019-03-14 3.04.29.png

デモはこちら
Gitはこちら

Three.jsでラーメンを作ります。
ラーメンの主な成分はGPU Trailですが、こちらは前の記事でベースを作ったのでそちらも参照してください。だいたい同じ内容です。
Three.jsでGPU Trails

今回はこいつにメッシュを貼って食べ応えのあるラーメンに仕上げていきたいと思います。

GPUComputationRendererの初期化

GPUComputationRendererを初期化します。

BoxTrails.js
initComputeRenderer(){        
        this.computeRenderer = new GPUComputationRenderer(this.length,this.num,this.renderer);

        let initPositionTex = this.computeRenderer.createTexture();
        let initVelocityTex = this.computeRenderer.createTexture();

        this.initPosition(initPositionTex);

        this.comTexs.position.texture = this.computeRenderer.addVariable("texturePosition",comShaderPosition,initPositionTex);
        this.comTexs.velocity.texture = this.computeRenderer.addVariable("textureVelocity",comShaderVelocity,initVelocityTex);

        this.computeRenderer.setVariableDependencies( this.comTexs.position.texture, [ this.comTexs.position.texture, this.comTexs.velocity.texture] );
        this.comTexs.position.uniforms = this.comTexs.position.texture.material.uniforms;

        this.computeRenderer.setVariableDependencies( this.comTexs.velocity.texture, [ this.comTexs.position.texture, this.comTexs.velocity.texture] );  
        this.comTexs.velocity.uniforms = this.comTexs.velocity.texture.material.uniforms;
        this.comTexs.velocity.uniforms.time =  { type:"f", value : 0};
        this.comTexs.velocity.uniforms.seed =  { type:"f", value : Math.random() * 100};

        this.computeRenderer.init();
    }
    initPosition(tex){
        var texArray = tex.image.data;
        let range = new THREE.Vector3(10,10,10);
        for(var i = 0; i < texArray.length; i += this.length * 4){
            let x = Math.random() * range.x - range.x / 2;
            let y = Math.random() * range.y - range.y / 2 + 5;
            let z = Math.random() * range.z - range.z / 2;
            for(let j = 0; j < this.length * 4; j += 4){
                texArray[i + j + 0] = x;
                texArray[i + j + 1] = y;
                texArray[i + j + 2] = z;
                texArray[i + j + 3] = 0.0;
            }
        }
    }

麺を生成

今回の肝である、麺のメッシュを貼っていきたいと思います。
基本は前回のTrailの生成と同じですが、メッシュを貼るためにはインデックスを設定する必要があります。
インデックスとはどの頂点同士を繋いで面を構成するかを示す配列です。

なんとなく今までやりたくなかったインデックスの生成ですが、そろそろ観念して面張りの達人になりたいと思います。

コード

BoxTrails.js
createTrails(){
        let geo = new THREE.BufferGeometry();

        let posArray = [];
        let indexArray = [];
        let uvArray = []; 

        for(let i = 0; i < this.num; i++){
            for(let j = 0; j < this.length; j++){
                let cNum = i * this.length + j;

                for(let k = 0; k < this.shapes; k++){
                    let rad = Math.PI * 2 / this.shapes * k;
                    let x = Math.cos(rad) * this.r;
                    let y = Math.sin(rad) * this.r;
                    let z = 0;

                    posArray.push(x);
                    posArray.push(y);
                    posArray.push(z);

                    uvArray.push(j / this.length);
                    uvArray.push(i / this.num);

                    if(j > 0){
                        let currentBase = cNum * this.shapes;
                        let underBase = (cNum - 1) * this.shapes;
                        let next = (k + 1) % this.shapes;

                        indexArray.push(currentBase + k);
                        indexArray.push((underBase + next));
                        indexArray.push((currentBase + next));

                        indexArray.push(currentBase + k);
                        indexArray.push(underBase + k);                        
                        indexArray.push((underBase + next));
                    }
                }
            }

            let n = i * this.length;
            indexArray.push(n, n + 2, n + 1, n, n + 3, n + 2);

            n = (i + 1) * this.length * this.shapes - 1;            
            indexArray.push(n, n - 2, n - 1, n, n - 3, n - 2);
        }

        let pos = new Float32Array(posArray);
        let indices = new Uint32Array(indexArray);
        let uv = new Float32Array(uvArray);

        geo.addAttribute('position', new THREE.BufferAttribute( pos, 3 ) );
        geo.addAttribute('uv', new THREE.BufferAttribute( uv, 2 ) );
        geo.setIndex(new THREE.BufferAttribute(indices,1));

        let stFrag = THREE.ShaderLib.standard;

        let customUni = {
            texturePosition : {value: null},
            textureVelocity : {value: null},
            uvDiff: {value: 1 / this.length},
            lineWidth: {value: this.lineWidth}
        }

        this.uni = THREE.UniformsUtils.merge([stFrag.uniforms, customUni]);
        this.uni.diffuse.value = new THREE.Vector3(1.0,0.8,0.0);
        this.uni.roughness.value = 0.3;

        let mat = new THREE.ShaderMaterial({
            vertexShader: vert,
            fragmentShader: stFrag.fragmentShader,
            uniforms: this.uni,
            lights: true,
            flatShading: true
        });

        this.obj = new THREE.Mesh(geo,mat);
        this.obj.castShadow = true;

        this.obj.customDepthMaterial = new THREE.ShaderMaterial({
            vertexShader: depth,
            fragmentShader: shadowFrag,
            uniforms: mat.uniforms
        });
    }

解説

インデックス

WebGLで面を作る場合、3つの頂点で1つの面を作る必要があります。
例えば、以下のような4つの頂点があったとすると面を張るために頂点0,1,3頂点0,2,3の二枚の三角面を使って面を貼ります。また、頂点を指定する順番は面の表と裏の情報になります。
これを踏まえて頂点を作りながらindexを指定して行きます。

IMG_A5E472C87C40-1.jpeg

マテリアルの設定

美味しい麺を作るためにはやはり麺の光沢感というのが必要です。
今回はせっかくメッシュを貼ったので、Three.jsのライトを考慮したマテリアル、というかThree.jsのStandardマテリアルをShaderMaterialで設定しました。

fragment shaderの場所

Three.jsのデフォルトシェーダーたちはThree.ShaderLibから取得できるようになっています。

uniform変数の初期化

必要なUniform変数もセットになっているので

THREE.UniformsUtils.merge([uniform1,uniform2]);

で、がっちゃんこしちゃいます。
console.logとか使ってFragmentShaderの中見て、最低限必要なであるdiffuseroughnessを設定します。

影をつける

ShaderMaterialをShadowマップに対応させるために、customDepthMaterialを設定します。
設定項目はマテリアルとほぼ一緒です。
ただ、他のサンプルを見るとfragmentShaderにTHREE.ShaderLib.basic.fragmentShaderをつけているところが多いのですがなぜか動かなく、結局THREE.ShaderLib.basic.fragmentShaderをコピーしてgl_FragColorのalphaを0にしたら綺麗に出ました。

BoxTrails.js
this.obj.customDepthMaterial = new THREE.ShaderMaterial({
    vertexShader: vert,
    fragmentShader: shadowFrag,
    uniforms: mat.uniforms
});

麺のシェーダーを書く

vertex shaderでは隣の頂点座標と自身の頂点座標から向きを計算し、回転させています。x軸の回転しかしてないけど、それっぽく動いたからOK!!
varying変数はフラグメントシェーダーで必要だから忘れないように書きます。

vert.vs
varying vec3 vViewPosition;
varying vec3 vWorldPosition;
uniform sampler2D texturePosition;
uniform float uvDiff;
uniform float lineWidth;

float PI = 3.141592653589793;

highp float atan2(in float y, in float x)
{
    return x == 0.0 ? sign(y) * PI / 2.0 : atan(y, x);
}

highp mat2 rotate(float rad){
    return mat2(cos(rad),sin(rad),-sin(rad),cos(rad));
}

void main() {
    vec2 nUV = uv + vec2(uvDiff,0.0);
    if(nUV.x >= 1.0){
        nUV = uv - vec2(uvDiff,0.0);
    }
    vec3 p = position;
    vec3 pos = texture2D( texturePosition, uv ).xyz;
    vec3 nPos = texture2D( texturePosition, nUV).xyz;

    vec3 vec = normalize(nPos - pos);
    float rotX = atan2(vec.y,vec.z);

    p.xy *= lineWidth * (abs(sin(uv.y * 2.0)) + 0.1);
    p.yz *= rotate(rotX);

    vec4 mvPosition = modelViewMatrix * vec4(p + pos, 1.0 );
    gl_Position = projectionMatrix * mvPosition;

    vec4 worldPosition = modelMatrix * vec4(p + pos, 1.0);
    vWorldPosition = worldPosition.xyz;
    vViewPosition = -mvPosition.xyz; 
}

コンピュートシェーダー

速度

computeVelocity.js
 void main() {
    if(gl_FragCoord.x >= 1.0) return;    

    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec3 pos = texture2D( texturePosition, uv ).xyz;
    vec3 vel = texture2D( textureVelocity, uv ).xyz;
    float idParticle = uv.y * resolution.x + uv.x;

    vel.xyz += 40.0 * vec3(
      snoise( vec4( 0.15 * pos.xyz, 7.225 + seed + 0.4 * time ) ),
      snoise( vec4( 0.15 * pos.xyz, 3.553 + seed * 100.0 + 0.4 * time ) ),
      snoise( vec4( 0.15 * pos.xyz, 1.259 + seed * 200.0 + 0.4 * time ) )
    ) * 0.2 * vec3(1.0,1.0,1.0);

    vec3 bPos = pos - vec3(0.0,7.0,0.0);
    vel += -(bPos) * length(bPos) * 0.15;
    vel.xyz *= 0.9 + abs(sin(uv.y * 9.0)) * 0.03;

    if(pos.y < 2.0){
      vel.y += 15.0;
    }
    gl_FragColor = vec4( vel.xyz, 1.0 );
}

位置更新

computePosition.js
void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;

    if(gl_FragCoord.x <= 1.0){
        vec3 pos = texture2D( texturePosition, uv ).xyz;
        vec3 vel = texture2D( textureVelocity, uv ).xyz;

        pos += vel * 0.01;

        gl_FragColor = vec4(pos,1.0);
    }else{
        vec2 bUV = (gl_FragCoord.xy - vec2(1.0,0.0)) / resolution.xy;
        vec3 beforePos = texture2D( texturePosition, bUV ).xyz;
        vec3 pos = beforePos;

        gl_FragColor = vec4(pos,1.0);
    }
}

コンピュート

update(){
        this.time += this.clock.getDelta();
        this.computeRenderer.compute();
        this.comTexs.velocity.uniforms.time.value = this.time;
        this.uni.texturePosition.value = this.computeRenderer.getCurrentRenderTarget(this.comTexs.position.texture).texture;
        this.uni.textureVelocity.value = this.computeRenderer.getCurrentRenderTarget(this.comTexs.velocity.texture).texture;
    }

盛り付け

いよいよ今までつくった材料を組み合わせてラーメンを仕上げたいと思います。

丼ぶりを作る

blenderでぽちぽち作ってGLTFで書き出しました。
スクリーンショット 2019-03-14 2.51.08.png

丼ぶりを読み込む

gltfの読み込みにはthree-gltf2-loaderを使いました。npmでインストールできます。
webpackで読み込む。


import GLTF2Loader from 'three-gltf2-loader';
GLTF2Loader(THREE);

ロードします。object.traverseで再帰的に子要素のreceiveShadowをtrueにしてます。

loader.load('./models/ramen.glb', (gltf) => {
    var object = gltf.scene;
    object.traverse((child) => {
        if (child.isMesh) {
            child.receiveShadow = true;
        }
    });

    object.position.y = 0.5;
    object.scale.set(2,2,2);
    this.scene.add(object);     
});

ポストプロセシングで味付け

ポストプロセシングはthree-effectcomposer-es6を使いました。npmでインストールできます。

import EffectComposer,{RenderPass,ShaderPass,CopyShader} from 'three-effectcomposer-es6';
MainScene.js
this.composer = new EffectComposer(this.renderer);
        this.composer.addPass(new RenderPass(this.scene,this.camera));

        var effect = {
            uniforms:{
                tDiffuse:{
                    value: null,
                    type:'t',
                },
                time: {
                    value: 0,
                    type: "f",
                }
            },
            vertexShader: ppVert,
            fragmentShader: ppFrag,
        }
        this.customPass = new ShaderPass(effect);
        this.customPass.renderToScreen = true;
        this.composer.addPass(this.customPass);

ポストプロセスシェーダー

uniform float time;
uniform sampler2D tDiffuse;
varying vec2 vUv;
#define N 16

void main() {
    vec2 uv = vUv;
    vec2 u = uv * 2.0 - 1.0;
    vec2 uu = u + vec2(0.0,0.4);
    vec3 c;

    float w = -max(.0,length(uu)) * 0.01;

    vec2 vig = uu * w;

    for(int i = 0; i < N; i++){
        vig *= 1.0 + (0.2 / float(N) * float(i));
        c.x += texture2D(tDiffuse,uv + vig).x;
        c.y += texture2D(tDiffuse,uv + vig * 0.8).y;
        c.z += texture2D(tDiffuse,uv + vig).z;
    }
    c /= float(N) - 5.0;
    gl_FragColor = vec4(c,1.0);
}

さいごに

すごくざっくりした解説になってしまいましたが、とっても美味しそうなラーメンが作れました。
今回はラーメンでしたが、同じものをうまく使うと結構カッコ良い表現ができます。
FISHデモ
スクリーンショット 2019-03-14 3.08.21.png

indexにめげずにメッシュを貼って、厚みのあるコンテンツを作って行きたいと思います。

それと子供の頃の僕の夢はラーメン屋さんでした。

79
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ukonpower
シェーダー!ブレンダー!
junni
デジタル領域における広告デザインの企画・制作を行っています。アソビゴコロで、世界をハッピーに変えていく。それがわたしたちの理念です。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
79
Help us understand the problem. What is going on with this article?