WebGL
GLSL
three.js

[デザイナー向け]大草原の頂点シェーダ

More than 1 year has passed since last update.

大草原に住まう

今回の趣旨

・頂点シェーダで草を揺らす。
・入門向け、CGデザイナー向け。
・デザイナー寄りの自分がわかりやすいと思う方法で書いています。
・数学がよくわかってなくても大丈夫。
・maxScript,js,mel等で簡単なスクリプトを書いたことがあれば十分理解できると思います。
・実際に試す場合はhtml,js等の知識があったほうが良いです。

前知識

・大まかに頂点シェーダとフラグメントシェーダ(ピクセルシェーダ)の二つがあります。
・大雑把に言って頂点シェーダで頂点毎にあれこれを計算し、フラグメントシェーダでピクセル毎にあれこれ計算して色として書き出す、という感じです。
・数字は必ず10.0というように.0をつけてください。1とか書くとエラーになります。
・今回はhtmlのなかにシェーダを書いていいます。
・後ろの方にhtml,js等のソースを載せてあります。コピペして使ってください。

glslを書く場所
<script type="x-shader/x-vertex" id="grassVs">
  // ここに頂点シェーダを書きます。
void main()
{
  // 今回はこの中を変えていきます。
}
</script>
<script type="x-shader/x-fragment" id="grassFs">
  // ここにフラグメントシェーダを書きます。
void main()
{
  // 今回はこの中を変えていきますが、コメントアウト(//)をとったりつけたるするだけで良いです。
}
</script>

大草原も一輪から。

m01.jpg
今回使ったポリゴンメッシュ。
ただの板ポリにUV設定しただけの単純なもの。
メッシュの高さとUVのY軸を合わせています。
それをテクスチャ貼って増殖したのがこちら。
01.jpg

草よ、動け。

では単純に動かしてみましょう。時間が進むとx軸に動きます。
void main(){}の中を下記のように変えてみます。

とりあえずx軸に動け!
<script type="x-shader/x-vertex" id="grassVs">

uniform float time;         // JSから時間を貰っている。呪文 sin で使う。単位:1000分の一秒。
varying vec2 vertexUv;      // uv情報をfragmentへ送る。

const float power = 10.0; // 揺れの大きさ
const float speed = 3.0; // 揺れる速度

void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + time;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}

</script>

positionというのが頂点毎の位置情報です。その中の一つx位置にtimeを足しています。
草単位で動いているように見えますが、実は頂点ごとに計算して動いています。
草がそれぞれ思い思いの方向へ進んでいきますが、これは増殖時にランダムでy軸方向に回転させているためです。
これはこれで面白いですが、根無し草ですね。
01.gif

根無し草よ、根を張ろう。

では根を張っているようにするにはどうしたらいいか。
繰り返しになりますが、頂点ごとに動きの計算がされています。
地面に接する頂点は動かさず(つまり + 0.0)、
上に行くほど大きく動くようにすればいいわけです。

ここでUVのY軸を見てみましょう。ここではフラグメントシェーダ側にも細工して白黒で表現します。
頂点シェーダ、フラグメントシェーダそれぞれ変えるのはvoid main(){}内だけです。

頂点シェーダ
<script type="x-shader/x-vertex" id="grassVs">

uniform float time;         // JSから時間を貰っている。呪文 sin で使う。
varying vec2 vertexUv;      // uv情報をfragmentへ送る。

varying float testValue;    // 確認用。fragmentで使う。

const float power = 10.0; // 揺れの大きさ
const float speed = 3.0;  // 揺れる速度

void main()
{
    vertexUv = uv;

    testValue = uv.y;           // uvのy軸。

    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

</script>
フラグメントシェーダ
<script type="x-shader/x-fragment" id="grassFs">

uniform sampler2D txDef; // 拡散反射光map
uniform sampler2D txOpa; // 透明度map

varying vec2 vertexUv; // uv情報。vertexから。

varying float testValue;    // 確認用。

void main( void ) {
        vec4 samplerColor = texture2D(txDef, vertexUv);
        vec4 samplerAlpha = texture2D(txOpa, vertexUv);
    // gl_FragColor = vec4( vec3(samplerColor.xyz) , samplerAlpha.x ); // 本番用
    gl_FragColor = vec4( vec3(testValue) , 1.0 ); // testValueが白黒で確認できる。
}

</script>

025.jpg
見事に地面が黒、高くなるほど白く見えます。
黒に近いということは0.0に近く、真っ白は1.0になります。
ちなみにマイナスになると0.0と同じで真っ黒、1.0を超えると真っ白です。
このようにどうなってるかわからないものは一度白黒にしてから視覚的に確認するといいでしょう。

確認できたところでフラグメント側を元に戻し、頂点シェーダを変えます。
具体的にはx軸にtimeを足す前にuv.yをかけています。地面側は0.0なので実質移動しませんが、上に行くほど動いてくれます。
言い換えると白いところほどよく動き、黒くなると動かなくなります。

頂点シェーダ
void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + ( time * uv.y );

    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}

02.jpg
でもなんか硬い。

しなやかさを与えよう。

しなやかさが足りない。言い換えると曲線的に動かしたい。
CGデザイナーの方はキャラクターセットアップにおけるスキニングを思い出してください。ボーンの影響範囲を編集するのに似ています。
さっきの例だと白いほど動くわけですが、問題は黒でも白でもなく、グレーの部分です。
このグレーをあまり動かなくしたい、つまり濃いグレーにすれば良いわけです。
026.jpg

手っ取り早い方法は何かないかなぁと思いながらスクショを撮ってPhotoshopを立ち上げます。
おもむろにスクショをレイヤー2枚にしてレイヤーモードを乗算にする。
お、良い感じだ!
028.jpg

同じことをやれば良いわけですが、乗算ってなんだろう?

算術における乗法 (じょうほう、英: multiplication) は、算術の四則と呼ばれるものの一つで、 < 中略 > 掛け算(かけざん)、乗算(じょうざん)とも呼ばれる。
https://ja.wikipedia.org/wiki/%E4%B9%97%E6%B3%95

なんだ、掛け算か。

頂点シェーダ
void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + ( time * ( uv.y * uv.y ) ) ;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}

photoshopで同じレイヤーを乗算モードで重ねたように、uv.yとuv.yを乗算つまり掛け合わせてあげました。
03.jpg

正弦魔法、" sin "。相手は揺れる。

しなやかに曲げることはできましたがこのままでは曲がりっぱなしです。
せっかくならゆらゆら動かしたいところです。

glslにはこれを叶える便利な魔法があります。名前は サイン sin 。
細かいことは良いので早速見てみましょう。

頂点シェーダ
void main()
{
    vertexUv = uv;

    // testValue = sin ( time ); // 魔法『sin』!
    testValue = ( sin ( time ) + 1.0 ) / 2.0; // sin魔法が見やすくなった!

    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
フラグメントシェーダ
void main( void ) {
        vec4 samplerColor = texture2D(txDef, vertexUv);
        vec4 samplerAlpha = texture2D(txOpa, vertexUv);
    // gl_FragColor = vec4( vec3(samplerColor.xyz) , samplerAlpha.x ); // 本番用
    gl_FragColor = vec4( vec3(testValue) , 1.0 ); // testValueが白黒で確認できる。
}

035.gif
黒くなったり白くなったりを周期的に繰り返しています。すばらしい!
これを動きにしてあげれば良いわけです。

頂点シェーダ
void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + ( sin ( time ) * ( uv.y * uv.y ) ) ;

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

}

地味ですが良い感じになってきました。
最後にもう一工夫。

頂点シェーダ
uniform float time;         // JSから時間を貰っている。単位:1000分の一秒。
varying vec2 vertexUv;      // uv情報をfragmentへ送る。

const float power = 10.0; // 揺れの大きさ
const float speed = 3.0; // 揺れる速度

void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + ( sin ( time * speed ) * power * ( uv.y * uv.y ) ) ;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}

05.gif
timeにspeedを掛けて周期を早くしています。powerをかけることで揺れる大きさを増やしています。
おまけ:魔法サインの正式名称?は 三角関数 正弦。sin ( time ) だと-1.0から1.0の間を行き来する。

終わりに。

数字が色にも動きにもなることが分かったと思います。
逆に言えば色で理解できれば数字が作れて動きも作れるわけです。
CGデザイナーさんであればノードベースで質感やパーティクル等作ったことがあると思いますが、似たような考え方で応用できると思います。
使えそうなものをまず白黒で目で見て確認。うまくいきそうだったら動きに変えていく、という順番でやれば色々作れると思います。

お詫び:
一部gifアニメがループしませんが、"[Payload Too Large]()"のため差し替えできませんでした。
お手数ですがリロードなどしてご覧くださいませ、ごめんなさいorz

わかりやすかった、わかりにくかった、もっと良い方法があるなどなど、ご意見等いただけると嬉しいです。

次のステップ。

  • 全て同じタイミングで動いていますが、ずらすにはどうしたらいいでしょうか。
  • 縦方向(Y軸)はどうしたら説得力が出るでしょうか。

今回のデータ等。

three.js は v83を使っています。

three.min.js と OrbitControls.js の場所:
ダウンロードしたthree.js-masterフォルダ内の下記にあります。
three.js-master/build/three.min.js
three.js-master/examples/js/controls/OrbitControls.js

ファイル構造

Screen Shot 2016-12-16 at 8.35.17 PM.png

テクスチャ

grassDef.jpg
grassOpa.jpg

html練習用

シェーダはここに書いていきます。

index.html
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="utf-8" />
    <title>大草原</title>
    <style type="text/css">
        html { overflow: hidden; }
        body { margin: 0; padding: 0; overflow: hidden; font-family: Monospace; font-size: 13px; line-height: 20px; color: #000; background-color:#fff; }
    </style>

    <script src="jslib/three.min.js"></script>
    <script src="jslib/OrbitControls.js"></script>
</head>
<body>
<script type="x-shader/x-vertex" id="grassVs">

uniform float time;         // JSから時間を貰っている。単位:1000分の一秒。
varying vec2 vertexUv;      // uv情報をfragmentへ送る。

varying float testValue;         // 確認用。fragmentで使う。

const float power = 10.0; // 揺れの大きさ
const float speed = 3.0; // 揺れる速度
const vec3 center = vec3(0.0 , 0.0 , 20.0); // 風の中心。

const float PI = 3.1415926; // 円周率。呪文 sin で使う。
const float PI2 = PI * 2.0;

void main()
{
    vertexUv = uv;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

</script>
<script type="x-shader/x-fragment" id="grassFs">

uniform sampler2D txDef; // 拡散反射光map
uniform sampler2D txOpa; // 透明度map

varying vec2 vertexUv; // uv情報。vertexから。

varying float testValue;    // 確認用。

void main( void ) {
        vec4 samplerColor = texture2D(txDef, vertexUv);
        vec4 samplerAlpha = texture2D(txOpa, vertexUv);
    gl_FragColor = vec4( vec3(samplerColor.xyz) , samplerAlpha.x ); // 本番用
    // gl_FragColor = vec4( vec3(testValue) , 1.0 ); // testValueが白黒で確認できる。
}

</script>   
<script src="js/main.js"></script>

</body>
</html>

html完成

fin.html
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="utf-8" />
    <title>大草原</title>
    <style type="text/css">
        html { overflow: hidden; }
        body { margin: 0; padding: 0; overflow: hidden; font-family: Monospace; font-size: 13px; line-height: 20px; color: #000; background-color:#fff; }
    </style>

    <script src="jslib/three.min.js"></script>
    <script src="jslib/OrbitControls.js"></script>
</head>
<body>
<script type="x-shader/x-vertex" id="grassVs">

uniform float time;         // JSから時間を貰っている。単位:1000分の一秒。
varying vec2 vertexUv;      // uv情報をfragmentへ送る。

const float power = 10.0; // 揺れの大きさ
const float speed = 3.0; // 揺れる速度

void main()
{
    vertexUv = uv;

    vec3 pos = position;
    pos.x = position.x + ( sin ( time * speed ) * power * ( uv.y * uv.y ) ) ;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}

</script>
<script type="x-shader/x-fragment" id="grassFs">

uniform sampler2D txDef; // 拡散反射光map
uniform sampler2D txOpa; // 透明度map

varying vec2 vertexUv; // uv情報。vertexから。

void main( void ) {
        vec4 samplerColor = texture2D(txDef, vertexUv);
        vec4 samplerAlpha = texture2D(txOpa, vertexUv);
    gl_FragColor = vec4( vec3(samplerColor.xyz) , samplerAlpha.x );
    // gl_FragColor = vec4( vec3(testValue) , 1.0 ); // testValueが白黒で確認できる。
}

</script>   
<script src="js/main.js"></script>

</body>
</html>

javascript

three.js関連の記述があります。
今回は触らなくて良いです。
GPUがまだまだぬるい!これじゃ大草原じゃなくて鉢植えか何かだ!って方。
"自己責任で"
44行目の 256 を増やしまくってください。
50-51行目の 30 を増やしても良いです。44行目をすごいことしてから300位にすると大草原です。
"自己責任で"

main.js
var scene = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener( 'resize', onWindowResize, false );

// orbit
var camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.y = 5;
camera.position.z = 30;

var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.enableZoom = false;

// world
// set materials

var textureLoader = new THREE.TextureLoader();
var grassVs = document.getElementById('grassVs').textContent;
var grassFs = document.getElementById('grassFs').textContent;

var grassMaterial = new THREE.ShaderMaterial({
    transparent: true,
    opacity: 1.0,
    depthWrite: false,
    uniforms:{
        time:   { type: 'f' , value: 0.0 },
        txDef:  { type: "t",  value: textureLoader.load( "images/grassDef.jpg" ) },
        txOpa:  { type: "t",  value: textureLoader.load( "images/grassOpa.jpg" ) }
    },
    vertexShader: grassVs,
    fragmentShader: grassFs
});

// world
// geometry

var grass = new THREE.Group();
var modelsLoader = new THREE.JSONLoader();
modelsLoader.load( "js/grass.js" , function( geometry ){
    for ( var i = 0; i < 256; i ++ ){
        objMesh = new THREE.Mesh( geometry, grassMaterial );
        var scaleRand = 0.1 + ( ( Math.random() - 0.5 ) * 0.05 ) ;
        objMesh.scale.set( scaleRand , scaleRand , scaleRand );
        objMesh.position.y = -2.0;
        objMesh.name = "grass000";
        objMesh.position.x = ( Math.random() - 0.5 ) * 30;
        objMesh.position.z = ( Math.random() - 0.5 ) * 30;
        objMesh.rotation.y = ( Math.random() - 0.5 ) * 360;
        scene.add( objMesh );
    }
});

// renderer

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}
var render = function (timestamp) {
    requestAnimationFrame(render);
    grassMaterial.uniforms.time.value = timestamp * 0.001;
    controls.update();
    renderer.render(scene, camera);
}
render();

モデルデータ

json形式。3dsmaxから出してます。
コピペってください。

grass.js
{

"metadata":
{
"sourceFile": "grass.max",
"generatedBy": "3ds max ThreeJSExporter",
"formatVersion": 3.1,
"vertices": 40,
"normals": 40,
"colors": 0,
"uvs": 40,
"triangles": 32,
"materials": 1
},

"materials": [
{
"DbgIndex" : 0,
"DbgName"  : "dummy",
"colorDiffuse"  : [1.0000, 0.0000, 0.0000],
"vertexColors" : false
}

],

"vertices": [-10.0,0.0,8.74228e-07,10.0,0.0,8.74228e-07,-10.0,10.0,8.74228e-07,10.0,10.0,8.74228e-07,-10.0,20.0,4.37114e-07,10.0,20.0,1.31134e-06,-10.0,30.0,8.74228e-07,10.0,30.0,8.74228e-07,-10.0,40.0,8.74228e-07,10.0,40.0,8.74228e-07,10.0,10.0,2.38419e-06,10.0,0.0,2.38419e-06,-10.0,0.0,-6.3573e-07,-10.0,10.0,-6.3573e-07,10.0,20.0,2.8213e-06,-10.0,20.0,-1.07284e-06,10.0,30.0,2.38419e-06,-10.0,30.0,-6.3573e-07,10.0,40.0,2.38419e-06,-10.0,40.0,-6.3573e-07,0.0,10.0,-10.0,0.0,0.0,-10.0,0.0,0.0,10.0,0.0,10.0,10.0,0.0,20.0,-10.0,-9.53674e-07,20.0,10.0,0.0,30.0,-10.0,0.0,30.0,10.0,0.0,40.0,-10.0,0.0,40.0,10.0,-1.90735e-06,10.0,10.0,-1.90735e-06,0.0,10.0,1.90735e-06,0.0,-10.0,1.90735e-06,10.0,-10.0,-1.90735e-06,20.0,10.0,1.90735e-06,20.0,-10.0,-1.90735e-06,30.0,10.0,1.90735e-06,30.0,-10.0,-1.90735e-06,40.0,10.0,1.90735e-06,40.0,-10.0],

"normals": [0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,1.664e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,1.79303e-07,0.0,-1.0,1.79303e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,1.664e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,1.50996e-07,0.0,-1.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07,1.0,0.0,1.90735e-07],

"colors": [],

"uvs": [[3.27826e-07,0.0,0.999999,0.0,3.8743e-07,0.25,0.999999,0.25,4.76837e-07,0.5,1.0,0.5,5.96046e-07,0.75,1.0,0.75,6.55651e-07,1.0,1.0,1.0,0.999999,0.25,0.999999,0.0,1.78814e-07,0.0,3.27826e-07,0.25,1.0,0.5,3.8743e-07,0.5,1.0,0.75,5.36442e-07,0.75,1.0,1.0,6.85453e-07,1.0,0.0,0.25,0.0,0.0,1.0,0.0,1.0,0.25,0.0,0.5,1.0,0.5,2.08616e-07,0.75,1.0,0.75,2.68221e-07,1.0,1.0,1.0,1.0,0.25,1.0,0.0,0.0,0.0,0.0,0.25,1.0,0.5,1.78814e-07,0.5,1.0,0.75,2.98023e-07,0.75,1.0,1.0,4.47035e-07,1.0]],

"faces": [42,0,1,3,0,0,1,3,0,1,3,42,3,2,0,0,3,2,0,3,2,0,42,2,3,5,0,2,3,5,2,3,5,42,5,4,2,0,5,4,2,5,4,2,42,4,5,7,0,4,5,7,4,5,7,42,7,6,4,0,7,6,4,7,6,4,42,6,7,9,0,6,7,9,6,7,9,42,9,8,6,0,9,8,6,9,8,6,42,11,12,13,0,11,12,13,11,12,13,42,13,10,11,0,13,10,11,13,10,11,42,10,13,15,0,10,13,15,10,13,15,42,15,14,10,0,15,14,10,15,14,10,42,14,15,17,0,14,15,17,14,15,17,42,17,16,14,0,17,16,14,17,16,14,42,16,17,19,0,16,17,19,16,17,19,42,19,18,16,0,19,18,16,19,18,16,42,21,22,23,0,21,22,23,21,22,23,42,23,20,21,0,23,20,21,23,20,21,42,20,23,25,0,20,23,25,20,23,25,42,25,24,20,0,25,24,20,25,24,20,42,24,25,27,0,24,25,27,24,25,27,42,27,26,24,0,27,26,24,27,26,24,42,26,27,29,0,26,27,29,26,27,29,42,29,28,26,0,29,28,26,29,28,26,42,31,32,33,0,31,32,33,31,32,33,42,33,30,31,0,33,30,31,33,30,31,42,30,33,35,0,30,33,35,30,33,35,42,35,34,30,0,35,34,30,35,34,30,42,34,35,37,0,34,35,37,34,35,37,42,37,36,34,0,37,36,34,37,36,34,42,36,37,39,0,36,37,39,36,37,39,42,39,38,36,0,39,38,36,39,38,36]

}

参考

:GLSL魔法
GLSLの数学的な関数で感じたこと
http://qiita.com/muripo_life/items/b5f934df3ceda3dc8bf6

:three.js
http://threejs.org/
https://threejs.org/examples/misc_controls_orbit.html

:3dsmax用JSONエクスポーター
https://github.com/mrdoob/three.js/tree/master/utils/exporters
もう一つ別の方のものもありますが、そちらはv82でエラーになることを確認しています。