シェーダで雲を制作
「How to Create Procedural Clouds Using Three.js Sprites」を参考に、シェーダで雲を制作しました。※Three.jsはr129を使用しています。
シェーダで雲を制作
まずは平面を生成し、フラグメントシェーダで雲のテクスチャと形状を制作します。
● 平面を生成
PlaneGeometryにRawShaderMaterialを設定して、平面を生成します。RawShaderMaterialについては、「RawShaderMaterial」を参考にしてください。
const geometry = new THREE.PlaneGeometry(7,7,2,2); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, side:THREE.DoubleSide, transparent:true }); const plane = new THREE.Mesh(geometry,material); scene.add(plane);
● 雲のテクスチャを設定
テクスチャはシェーダでも生成できますが、GPUへの負担を軽減するため、2枚のテクスチャ画像をスライドアニメーションさせて実装します。
テクスチャ画像は、「How to Create Procedural Clouds Using Three.js Sprites」の「Download Source」ボタンから取得できます。
let time = 0; //テクスチャ画像の読み込み const uTexture1 = new THREE.TextureLoader().load( './img/texture.jpg' ); const uniforms = { time:{type:'f',value:0.0}, uTexture1:{type:'t',value:null}, }; //平面の生成 const geometry = new THREE.PlaneGeometry(7,7,2,2); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms:uniforms side:THREE.BackSide, transparent:true }); //テクスチャの設定 uniforms.uTexture1.value = uTexture1; //テクスチャの繰り返し設定 uniforms.uTexture1.value.wrapS = uniforms.uTexture1.value.wrapT = THREE.RepeatWrapping; plane = new THREE.Mesh(geometry,material); scene.add(plane); //アニメーション function rendering(){ requestAnimationFrame(rendering); time++; plane.material.uniforms.time.value = time; renderer.render(scene,camera); }
● glsl.js
フラグメントシェーダで、2枚のテクスチャ画像をスライドアニメーションさせます。
//バーテックスシェーダ const vertexShader =` precision highp float; attribute vec3 position; attribute vec2 uv; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; varying vec2 vUv; void main(void){ //フラグメントシェーダにuvを転送 vUv = uv; vec4 mvPosition = modelViewMatrix * vec4(position,1.0); gl_Position = projectionMatrix * mvPosition; } `; //フラグメントシェーダ const fragmentShader =` precision highp float; //テクスチャの取得 uniform sampler2D uTexture1; uniform float time; varying vec2 vUv; //レベル補正でコントラストを調整 vec4 gammaCorrect(vec4 color, float gamma){ return pow(color, vec4(1.0 / gamma)); } vec4 levelRange(vec4 color, float minInput, float maxInput){ return min(max(color - vec4(minInput), vec4(0.0)) / (vec4(maxInput) - vec4(minInput)), vec4(1.0)); } vec4 levels(vec4 color, float minInput, float gamma, float maxInput){ return gammaCorrect(levelRange(color, minInput, maxInput), gamma); } void main(void){ //テクスチャをスライドアニメーション vec4 txtNoise1 = texture2D(uTexture1,vec2(vUv.x + time * 0.0001,vUv.y - time * 0.00014)); vec4 txtNoise2 = texture2D(uTexture1,vec2(vUv.x - time * 0.00002,vUv.y + time * 0.000017 + 0.2)); //レベル補正で透明度を計算 float alpha = levels((txtNoise1 + txtNoise2) * 0.6,0.2,0.4,0.7).r; l_FragColor = vec4(vec3(0.95,0.95,0.95),alpha); } `; export { vertexShader, fragmentShader };
● 雲の形状を設定
フラグメントシェーダで、テクスチャ画像を使用して雲の形状にマスクします。テクスチャ画像は、「How to Create Procedural Clouds Using Three.js Sprites」の「Download Source」ボタンから取得できます。
const fragmentShader =` precision highp float; uniform sampler2D uTexture1; //テクスチャの取得 uniform sampler2D uTexture2; uniform float time; varying vec2 vUv; vec4 gammaCorrect(vec4 color, float gamma){ return pow(color, vec4(1.0 / gamma)); } vec4 levelRange(vec4 color, float minInput, float maxInput){ return min(max(color - vec4(minInput), vec4(0.0)) / (vec4(maxInput) - vec4(minInput)), vec4(1.0)); } vec4 levels(vec4 color, float minInput, float gamma, float maxInput){ return gammaCorrect(levelRange(color, minInput, maxInput), gamma); } void main(void){ vec4 txtNoise1 = texture2D(uTexture1,vec2(vUv.x + time * 0.0001,vUv.y - time * 0.00014)); vec4 txtNoise2 = texture2D(uTexture1,vec2(vUv.x - time * 0.00002,vUv.y + time * 0.000017 + 0.2)); //雲の形状のテクスチャ vec4 txtShape = texture2D(uTexture2,vUv); float alpha = levels((txtNoise1 + txtNoise2) * 0.6,0.2,0.4,0.7).r; //雲の形状にマスク alpha *= txtShape.r; l_FragColor = vec4(vec3(0.95,0.95,0.95),alpha); } `;
● シンプレックスノイズと非整数ブラウン運動
シンプレックスノイズと非整数ブラウン運動(Fractional Brownian motion)で、雲の形状をモーフィングアニメーションさせ、蒸気境界効果を与えます。シンプレックスノイズに関しては、「GLSLでシンプレックスノイズ」を参考にしてください。
const fragmentShader =` precision highp float; uniform sampler2D uTexture1; uniform sampler2D uTexture2; uniform float time; uniform float uFac1; uniform float uFac2; uniform float uTimeFactor1; uniform float uTimeFactor2; uniform float uDisplStrength1; uniform float uDisplStrength2; varying vec2 vUv; // webgl-noise // Description : Array and textureless GLSL 2D/3D/4D simplex noise functions. // Author : Ian McEwan,Ashima Arts. // Maintainer : stegu // Lastmod : 20201014(stegu) // License : Copyright(C)2011 Ashima Arts.All rights reserved. // Distributed under the MIT License.See LICENSE file. // https://github.com/ashima/webgl-noise // https://github.com/stegu/webgl-noise vec3 mod289(vec3 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 mod289(vec4 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 permute(vec4 x){ return mod289(((x*34.0)+1.0)*x); } vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; } float snoise(vec3 v){ const vec2 C = vec2(1.0/6.0, 1.0/3.0); const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); vec3 i = floor(v + dot(v, C.yyy) ); vec3 x0 = v - i + dot(i, C.xxx) ; vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min( g.xyz, l.zxy ); vec3 i2 = max( g.xyz, l.zxy ); vec3 x1 = x0 - i1 + C.xxx; vec3 x2 = x0 - i2 + C.yyy; vec3 x3 = x0 - D.yyy; i = mod289(i); vec4 p = permute(permute(permute( i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); float n_ = 0.142857142857; vec3 ns = n_ * D.wyz - D.xzx; vec4 j = p - 49.0 * floor(p * ns.z * ns.z); vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_); vec4 x = x_ * ns.x + ns.yyyy; vec4 y = y_ * ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y); vec4 b0 = vec4(x.xy, y.xy); vec4 b1 = vec4(x.zw, y.zw); vec4 s0 = floor(b0)*2.0 + 1.0; vec4 s1 = floor(b1)*2.0 + 1.0; vec4 sh = -step(h, vec4(0.0)); vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy; vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww; vec3 p0 = vec3(a0.xy,h.x); vec3 p1 = vec3(a0.zw,h.y); vec3 p2 = vec3(a1.xy,h.z); vec3 p3 = vec3(a1.zw,h.w); vec4 norm = taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2, p2),dot(p3,p3))); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; vec4 m = max(0.5 - vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0); m = m * m; return 105.0 * dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); } //非整数ブラウン運動 float fbm3d(vec3 x, const in int it) { float v = 0.0; float a = 0.5; vec3 shift = vec3(100); for (int i = 0; i < 32; ++i) { if(i < it) { v += a * snoise(x); x = x * 2.0 + shift; a *= 0.5; } } return v; } vec4 gammaCorrect(vec4 color, float gamma){ return pow(color, vec4(1.0 / gamma)); } vec4 levelRange(vec4 color, float minInput, float maxInput){ return min(max(color - vec4(minInput), vec4(0.0)) / (vec4(maxInput) - vec4(minInput)), vec4(1.0)); } vec4 levels(vec4 color, float minInput, float gamma, float maxInput){ return gammaCorrect(levelRange(color, minInput, maxInput), gamma); } void main(void){ vec2 newUv = vUv; vec4 txtNoise1 = texture2D(uTexture1,vec2(vUv.x + time * 0.0001,vUv.y - time * 0.00014)); vec4 txtNoise2 = texture2D(uTexture1,vec2(vUv.x - time * 0.00002,vUv.y + time * 0.000017 + 0.2)); //蒸気境界効果を与える float noiseBig = fbm3d(vec3(vUv * uFac1,time * uTimeFactor1),4) + 1.0 * 0.5; newUv += noiseBig * uDisplStrength1; //雲の形状をモーフィングアニメーションさせる float noiseSmall = snoise(vec3(newUv * uFac2,time * uTimeFactor2)) + 1.0 * 0.5; newUv += noiseSmall * uDisplStrength2; vec4 txtShape = texture2D(uTexture2,newUv); float alpha = levels((txtNoise1 + txtNoise2) * 0.6,0.2,0.4,0.7).r; alpha *= txtShape.r; gl_FragColor = vec4(vec3(0.95,0.95,0.95),alpha); } `;
● ビルボード効果の設定
スプライトマテリアルを拡張してビルボード効果を与え、雲が常にカメラに向くようにします。
const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms: { //スプライトのuniforms ...THREE.UniformsUtils.clone(THREE.ShaderLib.sprite.uniforms), ...uniforms }, side:THREE.BackSide, transparent:true });
バーテックスシェーダでビルボード効果を設定します。
const vertexShader =` precision highp float; attribute vec3 position; attribute vec2 uv; uniform float rotation; uniform vec2 center; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; #include <common> #include <uv_pars_vertex> #include <fog_pars_vertex> #include <logdepthbuf_pars_vertex> #include <clipping_planes_pars_vertex> void main(void){ vUv = uv; vec4 mvPosition = modelViewMatrix * vec4(0.0,0.0,0.0,1.0); vec2 scale; scale.x = length(vec3(modelMatrix[0].x,modelMatrix[0].y,modelMatrix[0].z)); scale.y = length(vec3(modelMatrix[1].x,modelMatrix[1].y,modelMatrix[1].z)); vec2 alignedPosition = (position.xy - (center - vec2(0.5))) * scale; vec2 rotatedPosition; rotatedPosition.x = cos(rotation) * alignedPosition.x - sin(rotation) * alignedPosition.y; rotatedPosition.y = sin(rotation) * alignedPosition.x - cos(rotation) * alignedPosition.y; mvPosition.xy += rotatedPosition; gl_Position = projectionMatrix * mvPosition; #include <logdepthbuf_vertex> #include <clipping_planes_vertex> #include <fog_vertex> } `;
● script.js
必要なライブラリを読み込みます。
<script src="js/preloadjs.min.js"></script> <script src="js/TweenMax.min.js"></script> <script src="js/script.js" type="module"></script>
完成したscript.jsになります。
//=============================================================== // Import Library //=============================================================== import * as THREE from './lib/three_jsm/three.module.js'; import { OrbitControls } from './lib/three_jsm/OrbitControls.js'; import { scene, camera, container, renderer } from './lib/basescene.js'; import { vertexShader, fragmentShader } from './glsl.js'; //=============================================================== // Init //=============================================================== window.addEventListener('load',function(){ init(); }); let orbitControls; let textureArray; let plane; let time = 0; function init(){ setLoading(); } function setLoading(){ TweenMax.to('.loader',0.1,{opacity:1}); const nameArray = ['texture','shape']; let manifestArray =[]; for(let i = 0; i < nameArray.length; i++){ let name = nameArray[i]; let path = 'img/' + name + '.jpg'; manifestArray.push({id:name,src:path}); } const loadQueue = new createjs.LoadQueue(); loadQueue.on('progress',function(e){ const progress = e.progress; }); textureArray = []; loadQueue.on('complete',function(){ for(let i = 0; i < nameArray.length; i++){ let tempImage = loadQueue.getResult(nameArray[i]); let tempTexture = new THREE.Texture(tempImage); tempTexture.needsUpdate = true; textureArray.push(tempTexture); } TweenMax.to('#loader_wrapper',1,{ opacity:0, onComplete: function(){ document.getElementById('loader_wrapper').style.display = 'none'; TweenMax.to('.loader',0,{opacity:0}); } }); threeWorld(); setLight(); setControll(); rendering(); }); loadQueue.loadManifest(manifestArray); } //=============================================================== // Create World //=============================================================== function threeWorld(){ renderer.outputEncoding = THREE.sRGBEncoding; const gridHelper = new THREE.GridHelper(50,50); gridHelper.position.y = -2.5; scene.add(gridHelper); const uniforms = { time:{type:'f',value:0.0}, uFac1: {value: 17.8}, uFac2: {value: 2.7}, uTexture1:{type:'t',value:null}, uTexture2:{type:'t',value:null}, uTimeFactor1:{value:0.002}, uTimeFactor2:{value:0.0015}, uDisplStrength1:{value:0.04}, uDisplStrenght2:{value:0.08}, }; const geometry = new THREE.PlaneGeometry(7,7,2,2); const material = new THREE.RawShaderMaterial({ vertexShader:vertexShader, fragmentShader:fragmentShader, uniforms: { ...THREE.UniformsUtils.clone(THREE.ShaderLib.sprite.uniforms), ...uniforms }, side:THREE.BackSide, transparent:true }); uniforms.uTexture1.value = textureArray[0]; uniforms.uTexture2.value = textureArray[1]; uniforms.uTexture1.value.wrapS = uniforms.uTexture1.value.wrapT = THREE.RepeatWrapping; plane = new THREE.Mesh(geometry,material); scene.add(plane); } function setLight(){ const ambientlight = new THREE.AmbientLight(0xFFFFFF,1.0); scene.add(ambientlight); } function setControll(){ document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false}); orbitControls = new OrbitControls(camera,renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.5; } function rendering(){ requestAnimationFrame(rendering); if(orbitControls){ orbitControls.update(); } time++; plane.material.uniforms.time.value = time; renderer.render(scene,camera); }
● glsl.js
const vertexShader =` precision highp float; attribute vec3 position; attribute vec2 uv; uniform float rotation; uniform vec2 center; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; #include <common> #include <uv_pars_vertex> #include <fog_pars_vertex> #include <logdepthbuf_pars_vertex> #include <clipping_planes_pars_vertex> void main(void){ vUv = uv; vec4 mvPosition = modelViewMatrix * vec4(0.0,0.0,0.0,1.0); vec2 scale; scale.x = length(vec3(modelMatrix[0].x,modelMatrix[0].y,modelMatrix[0].z)); scale.y = length(vec3(modelMatrix[1].x,modelMatrix[1].y,modelMatrix[1].z)); vec2 alignedPosition = (position.xy - (center - vec2(0.5))) * scale; vec2 rotatedPosition; rotatedPosition.x = cos(rotation) * alignedPosition.x - sin(rotation) * alignedPosition.y; rotatedPosition.y = sin(rotation) * alignedPosition.x - cos(rotation) * alignedPosition.y; mvPosition.xy += rotatedPosition; gl_Position = projectionMatrix * mvPosition; #include <logdepthbuf_vertex> #include <clipping_planes_vertex> #include <fog_vertex> } `; const fragmentShader =` precision highp float; uniform sampler2D uTexture1; uniform sampler2D uTexture2; uniform float time; uniform float uFac1; uniform float uFac2; uniform float uTimeFactor1; uniform float uTimeFactor2; uniform float uDisplStrength1; uniform float uDisplStrength2; varying vec2 vUv; // Description : Array and textureless GLSL 2D/3D/4D simplex noise functions. // Author : Ian McEwan,Ashima Arts. // Maintainer : stegu // Lastmod : 20201014(stegu) // License : Copyright(C)2011 Ashima Arts.All rights reserved. // Distributed under the MIT License.See LICENSE file. // https://github.com/ashima/webgl-noise // https://github.com/stegu/webgl-noise vec3 mod289(vec3 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 mod289(vec4 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 permute(vec4 x){ return mod289(((x*34.0)+1.0)*x); } vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; } float snoise(vec3 v){ const vec2 C = vec2(1.0/6.0, 1.0/3.0); const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); vec3 i = floor(v + dot(v, C.yyy) ); vec3 x0 = v - i + dot(i, C.xxx) ; vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min( g.xyz, l.zxy ); vec3 i2 = max( g.xyz, l.zxy ); vec3 x1 = x0 - i1 + C.xxx; vec3 x2 = x0 - i2 + C.yyy; vec3 x3 = x0 - D.yyy; i = mod289(i); vec4 p = permute(permute(permute( i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); float n_ = 0.142857142857; vec3 ns = n_ * D.wyz - D.xzx; vec4 j = p - 49.0 * floor(p * ns.z * ns.z); vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_); vec4 x = x_ * ns.x + ns.yyyy; vec4 y = y_ * ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y); vec4 b0 = vec4(x.xy, y.xy); vec4 b1 = vec4(x.zw, y.zw); vec4 s0 = floor(b0)*2.0 + 1.0; vec4 s1 = floor(b1)*2.0 + 1.0; vec4 sh = -step(h, vec4(0.0)); vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy; vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww; vec3 p0 = vec3(a0.xy,h.x); vec3 p1 = vec3(a0.zw,h.y); vec3 p2 = vec3(a1.xy,h.z); vec3 p3 = vec3(a1.zw,h.w); vec4 norm = taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2, p2),dot(p3,p3))); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; vec4 m = max(0.5 - vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0); m = m * m; return 105.0 * dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); } float fbm3d(vec3 x, const in int it) { float v = 0.0; float a = 0.5; vec3 shift = vec3(100); for (int i = 0; i < 32; ++i) { if(i < it) { v += a * snoise(x); x = x * 2.0 + shift; a *= 0.5; } } return v; } vec4 gammaCorrect(vec4 color, float gamma){ return pow(color, vec4(1.0 / gamma)); } vec4 levelRange(vec4 color, float minInput, float maxInput){ return min(max(color - vec4(minInput), vec4(0.0)) / (vec4(maxInput) - vec4(minInput)), vec4(1.0)); } vec4 levels(vec4 color, float minInput, float gamma, float maxInput){ return gammaCorrect(levelRange(color, minInput, maxInput), gamma); } void main(void){ vec2 newUv = vUv; vec4 txtNoise1 = texture2D(uTexture1,vec2(vUv.x + time * 0.0001,vUv.y - time * 0.00014)); vec4 txtNoise2 = texture2D(uTexture1,vec2(vUv.x - time * 0.00002,vUv.y + time * 0.000017 + 0.2)); float noiseBig = fbm3d(vec3(vUv * uFac1,time * uTimeFactor1),4) + 1.0 * 0.5; newUv += noiseBig * uDisplStrength1; float noiseSmall = snoise(vec3(newUv * uFac2,time * uTimeFactor2)) + 1.0 * 0.5; newUv += noiseSmall * uDisplStrength2; vec4 txtShape = texture2D(uTexture2,newUv); float alpha = levels((txtNoise1 + txtNoise2) * 0.6,0.2,0.4,0.7).r; alpha *= txtShape.r; gl_FragColor = vec4(vec3(0.95,0.95,0.95),alpha); } `; export { vertexShader, fragmentShader };
● basescene.js
sceneやcameraなど基本的な設定を管理するbasescene.jsです。
//=============================================================== // Import Library //=============================================================== import * as THREE from './three_jsm/three.module.js'; //=============================================================== // Base scene //=============================================================== let scene,camera,container,renderer; init(); function init(){ scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,1,1000); camera.position.set(0,0,6); scene.add(camera); renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth,window.innerHeight); renderer.setClearColor(0x036da9); container = document.querySelector('#canvas_vr'); container.appendChild(renderer.domElement); window.addEventListener('resize',function(){ camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight); },false); } export { scene, camera, container, renderer }
完成したデモになります。背景を空色にして、シェーダで雲を制作しました。