シェーダで雲を制作

「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 }
完成したデモになります。背景を空色にして、シェーダで雲を制作しました。

