mirror of
https://github.com/tengge1/ShadowEditor.git
synced 2026-01-25 15:08:11 +00:00
637 lines
16 KiB
JavaScript
637 lines
16 KiB
JavaScript
|
|
/**
|
|
* @author yomotsu / http://yomotsu.net
|
|
* ported from http://webgl-fire.appspot.com/html/fire.html
|
|
*
|
|
* https://www.youtube.com/watch?v=jKRHmQmduDI
|
|
* https://graphics.ethz.ch/teaching/former/imagesynthesis_06/miniprojects/p3/
|
|
* https://www.iusb.edu/math-compsci/_prior-thesis/YVanzine_thesis.pdf
|
|
*/
|
|
|
|
( function ( root, factory ) {
|
|
|
|
if ( typeof define === 'function' && define.amd ) {
|
|
|
|
// AMD. Register as an anonymous module.
|
|
define( [], factory );
|
|
|
|
} else if ( typeof module === 'object' && module.exports ) {
|
|
|
|
// Node. Does not work with strict CommonJS, but
|
|
// only CommonJS-like environments that support module.exports,
|
|
// like Node.
|
|
module.exports = factory();
|
|
|
|
} else {
|
|
|
|
// Browser globals (root is window)
|
|
root.VolumetricFire = factory( root );
|
|
|
|
}
|
|
|
|
}( window, function () {
|
|
|
|
'use strict';
|
|
|
|
var vs = [
|
|
|
|
'attribute vec3 position;',
|
|
'attribute vec3 tex;',
|
|
'uniform mat4 projectionMatrix;',
|
|
'uniform mat4 modelViewMatrix;',
|
|
|
|
'varying vec3 texOut;',
|
|
|
|
'void main ( void ) {',
|
|
|
|
'gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0 );',
|
|
'texOut = tex;',
|
|
|
|
'}',
|
|
|
|
].join( '\n' );
|
|
|
|
var fs = [
|
|
|
|
'precision highp float;',
|
|
|
|
// Modified Blum Blum Shub pseudo-random number generator.
|
|
'vec2 mBBS( vec2 val, float modulus ) {',
|
|
|
|
'val = mod( val, modulus ); // For numerical consistancy.',
|
|
'return mod(val * val, modulus);',
|
|
|
|
'}',
|
|
|
|
// Pregenerated noise texture.
|
|
'uniform sampler2D nzw;',
|
|
'const float modulus = 61.0; // Value used in pregenerated noise texture.',
|
|
|
|
/**
|
|
* Modified noise function.
|
|
* @see http://www.csee.umbc.edu/~olano/papers/index.html#mNoise
|
|
**/
|
|
'float mnoise ( vec3 pos ) {',
|
|
|
|
'float intArg = floor( pos.z );',
|
|
'float fracArg = fract( pos.z );',
|
|
'vec2 hash = mBBS( intArg * 3.0 + vec2( 0, 3 ), modulus );',
|
|
'vec4 g = vec4 (',
|
|
'texture2D( nzw, vec2( pos.x, pos.y + hash.x ) / modulus ).xy,',
|
|
'texture2D( nzw, vec2( pos.x, pos.y + hash.y ) / modulus ).xy',
|
|
') * 2.0 - 1.0;',
|
|
|
|
'return mix(',
|
|
'g.x + g.y * fracArg,',
|
|
'g.z + g.w * ( fracArg - 1.0 ),',
|
|
'smoothstep( 0.0, 1.0, fracArg )',
|
|
');',
|
|
|
|
'}',
|
|
|
|
'const int octives = 4;',
|
|
'const float lacunarity = 2.0;',
|
|
'const float gain = 0.5;',
|
|
|
|
/**
|
|
* Adds multiple octives of noise together.
|
|
**/
|
|
'float turbulence( vec3 pos ) {',
|
|
|
|
'float sum = 0.0;',
|
|
'float freq = 1.0;',
|
|
'float amp = 1.0;',
|
|
|
|
'for ( int i = 0; i < 4; i++ ) {',
|
|
|
|
'sum += abs( mnoise( pos * freq ) ) * amp;',
|
|
'freq *= lacunarity;',
|
|
'amp *= gain;',
|
|
|
|
'}',
|
|
|
|
'return sum;',
|
|
|
|
'}',
|
|
|
|
'const float magnatude = 1.3;',
|
|
'uniform float time;',
|
|
'uniform sampler2D fireProfile;',
|
|
|
|
/**
|
|
* Samples the fire.
|
|
*
|
|
* @param loc the normalized location (0.0-1.0) to sample the fire
|
|
* @param scale the 'size' of the fire in world space and time
|
|
**/
|
|
'vec4 sampleFire( vec3 loc, vec4 scale ) {',
|
|
|
|
// Convert xz to [-1.0, 1.0] range.
|
|
'loc.xz = loc.xz * 2.0 - 1.0;',
|
|
|
|
// Convert to (radius, height) to sample fire profile texture.
|
|
'vec2 st = vec2( sqrt( dot( loc.xz, loc.xz ) ), loc.y );',
|
|
|
|
// Convert loc to 'noise' space
|
|
'loc.y -= time * scale.w; // Scrolling noise upwards over time.',
|
|
'loc *= scale.xyz; // Scaling noise space.',
|
|
|
|
// Offsetting vertial texture lookup.
|
|
// We scale this by the sqrt of the height so that things are
|
|
// relatively stable at the base of the fire and volital at the
|
|
// top.
|
|
'float offset = sqrt( st.y ) * magnatude * turbulence( loc );',
|
|
'st.y += offset;',
|
|
|
|
// TODO: Update fireProfile texture to have a black row of pixels.
|
|
'if ( st.y > 1.0 ) {',
|
|
|
|
'return vec4( 0, 0, 0, 1 );',
|
|
|
|
'}',
|
|
|
|
'vec4 result = texture2D( fireProfile, st );',
|
|
|
|
// Fading out bottom so slice clipping isnt obvious
|
|
'if ( st.y < 0.1 ) {',
|
|
|
|
'result *= st.y / 0.1;',
|
|
|
|
'}',
|
|
|
|
'return result;',
|
|
|
|
'}',
|
|
|
|
'varying vec3 texOut;',
|
|
|
|
'void main( void ) {',
|
|
|
|
// Mapping texture coordinate to -1 => 1 for xy, 0=> 1 for y
|
|
'vec3 color = sampleFire( texOut, vec4( 1.0, 2.0, 1.0, 0.5 ) ).xyz;',
|
|
'gl_FragColor = vec4( color * 1.5, 1 );',
|
|
|
|
'if(color.x < 0.01 && color.y < 0.01 && color.z < 0.01) {',
|
|
|
|
' discard; ',
|
|
|
|
'}',
|
|
|
|
'}',
|
|
|
|
].join( '\n' );
|
|
|
|
var initMaterial = ( function () {
|
|
|
|
var material;
|
|
var textureLoader = new THREE.TextureLoader();
|
|
|
|
return function () {
|
|
|
|
if ( !!material ) { return material; }
|
|
|
|
// TODO
|
|
// Canvas2D で noise 画像を作る
|
|
var nzw = textureLoader.load( VolumetricFire.texturePath + 'nzw.png' );
|
|
nzw.wrapS = THREE.RepeatWrapping;
|
|
nzw.wrapT = THREE.RepeatWrapping;
|
|
nzw.magFilter = THREE.LinearFilter;
|
|
nzw.minFilter = THREE.LinearFilter;
|
|
|
|
var fireProfile = textureLoader.load( VolumetricFire.texturePath + 'firetex.png' );
|
|
fireProfile.wrapS = THREE.ClampToEdgeWrapping;
|
|
fireProfile.wrapT = THREE.ClampToEdgeWrapping;
|
|
fireProfile.magFilter = THREE.LinearFilter;
|
|
fireProfile.minFilter = THREE.LinearFilter;
|
|
|
|
var uniforms = {
|
|
nzw: {
|
|
type: 't',
|
|
value: nzw
|
|
},
|
|
fireProfile: {
|
|
type: 't',
|
|
value: fireProfile
|
|
},
|
|
time: {
|
|
type: 'f',
|
|
value: 1.0
|
|
}
|
|
};
|
|
|
|
material = new THREE.RawShaderMaterial( {
|
|
vertexShader : vs,
|
|
fragmentShader : fs,
|
|
uniforms : uniforms,
|
|
side : THREE.DoubleSide,
|
|
blending : THREE.AdditiveBlending,
|
|
transparent : true
|
|
} );
|
|
|
|
return material;
|
|
|
|
};
|
|
|
|
} )();
|
|
|
|
|
|
var cornerNeighbors = [
|
|
[ 1, 2, 4 ],
|
|
[ 0, 5, 3 ],
|
|
[ 0, 3, 6 ],
|
|
[ 1, 7, 2 ],
|
|
[ 0, 6, 5 ],
|
|
[ 1, 4, 7 ],
|
|
[ 2, 7, 4 ],
|
|
[ 3, 5, 6 ],
|
|
];
|
|
|
|
var incomingEdges = [
|
|
[ -1, 2, 4, -1, 1, -1, -1, -1 ],
|
|
[ 5, -1, -1, 0, -1, 3, -1, -1 ],
|
|
[ 3, -1, -1, 6, -1, -1, 0, -1 ],
|
|
[ -1, 7, 1, -1, -1, -1, -1, 2 ],
|
|
[ 6, -1, -1, -1, -1, 0, 5, -1 ],
|
|
[ -1, 4, -1, -1, 7, -1, -1, 1 ],
|
|
[ -1, -1, 7, -1, 2, -1, -1, 4 ],
|
|
[ -1, -1, -1, 5, -1, 6, 3, -1 ],
|
|
];
|
|
|
|
var VolumetricFire = function ( width, height, depth, sliceSpacing, camera ) {
|
|
|
|
this.camera = camera;
|
|
|
|
this._sliceSpacing = sliceSpacing;
|
|
|
|
var widthHalf = width * 0.5;
|
|
var heightHalf = height * 0.5;
|
|
var depthHalf = depth * 0.5;
|
|
|
|
this._posCorners = [
|
|
new THREE.Vector3( -widthHalf, -heightHalf, -depthHalf ),
|
|
new THREE.Vector3( widthHalf, -heightHalf, -depthHalf ),
|
|
new THREE.Vector3( -widthHalf, heightHalf, -depthHalf ),
|
|
new THREE.Vector3( widthHalf, heightHalf, -depthHalf ),
|
|
new THREE.Vector3( -widthHalf, -heightHalf, depthHalf ),
|
|
new THREE.Vector3( widthHalf, -heightHalf, depthHalf ),
|
|
new THREE.Vector3( -widthHalf, heightHalf, depthHalf ),
|
|
new THREE.Vector3( widthHalf, heightHalf, depthHalf )
|
|
];
|
|
this._texCorners = [
|
|
new THREE.Vector3( 0, 0, 0 ),
|
|
new THREE.Vector3( 1, 0, 0 ),
|
|
new THREE.Vector3( 0, 1, 0 ),
|
|
new THREE.Vector3( 1, 1, 0 ),
|
|
new THREE.Vector3( 0, 0, 1 ),
|
|
new THREE.Vector3( 1, 0, 1 ),
|
|
new THREE.Vector3( 0, 1, 1 ),
|
|
new THREE.Vector3( 1, 1, 1 )
|
|
];
|
|
|
|
this._viewVector = new THREE.Vector3();
|
|
|
|
|
|
// TODO
|
|
// still did not figure out, how many vertexes should be...
|
|
// three.jsでは可変にできない、ひとまず多めに用意
|
|
|
|
var index = new Uint16Array ( ( width + height + depth ) * 30 );
|
|
var position = new Float32Array( ( width + height + depth ) * 30 * 3 );
|
|
var tex = new Float32Array( ( width + height + depth ) * 30 * 3 );
|
|
|
|
var geometry = new THREE.BufferGeometry();
|
|
geometry.dynamic = true;
|
|
geometry.setIndex( new THREE.BufferAttribute( index, 1 ) );
|
|
geometry.addAttribute( 'position', new THREE.BufferAttribute( position, 3 ) );
|
|
geometry.addAttribute( 'tex', new THREE.BufferAttribute( tex, 3 ) );
|
|
|
|
var material = initMaterial();
|
|
|
|
this.mesh = new THREE.Mesh(
|
|
geometry,
|
|
material
|
|
);
|
|
|
|
this.mesh.updateMatrixWorld();
|
|
|
|
}
|
|
|
|
VolumetricFire.prototype.update = function ( elapsed ) {
|
|
|
|
this.updateViewVector();
|
|
this.slice();
|
|
this.updateGeometry();
|
|
this.mesh.material.uniforms.time.value = elapsed;
|
|
|
|
}
|
|
|
|
VolumetricFire.prototype.updateGeometry = function () {
|
|
|
|
this.mesh.geometry.index.array.set( this._indexes );
|
|
this.mesh.geometry.attributes.position.array.set( this._points );
|
|
this.mesh.geometry.attributes.tex.array.set( this._texCoords );
|
|
|
|
this.mesh.geometry.index.needsUpdate = true;
|
|
this.mesh.geometry.attributes.position.needsUpdate = true;
|
|
this.mesh.geometry.attributes.tex.needsUpdate = true;
|
|
|
|
}
|
|
|
|
VolumetricFire.prototype.updateViewVector = function () {
|
|
|
|
// TODO
|
|
// MVMatrixが前回と同じなら、アップデートしないようにする
|
|
//
|
|
// つまり、カメラの位置とオブジェクトの位置に変化なければ
|
|
// アップデートしないようにする
|
|
|
|
var modelViewMatrix = new THREE.Matrix4();
|
|
|
|
modelViewMatrix.multiplyMatrices(
|
|
this.camera.matrixWorldInverse,
|
|
this.mesh.matrixWorld
|
|
);
|
|
|
|
this._viewVector.set(
|
|
- modelViewMatrix.elements[ 2 ],
|
|
- modelViewMatrix.elements[ 6 ],
|
|
- modelViewMatrix.elements[ 10 ]
|
|
).normalize();
|
|
|
|
};
|
|
|
|
VolumetricFire.prototype.slice = function () {
|
|
|
|
this._points = [];
|
|
this._texCoords = [];
|
|
this._indexes = [];
|
|
|
|
var i;
|
|
var cornerDistance0 = this._posCorners[ 0 ].dot( this._viewVector );
|
|
|
|
var cornerDistance = [ cornerDistance0 ];
|
|
var maxCorner = 0;
|
|
var minDistance = cornerDistance0;
|
|
var maxDistance = cornerDistance0;
|
|
|
|
for ( i = 1; i < 8; i = ( i + 1 )|0 ) {
|
|
|
|
cornerDistance[ i ] = this._posCorners[ i ].dot( this._viewVector );
|
|
|
|
if ( cornerDistance[ i ] > maxDistance ) {
|
|
|
|
maxCorner = i;
|
|
maxDistance = cornerDistance[ i ];
|
|
|
|
}
|
|
|
|
if ( cornerDistance[ i ] < minDistance ) {
|
|
|
|
minDistance = cornerDistance[ i ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Aligning slices
|
|
var sliceDistance = Math.floor( maxDistance / this._sliceSpacing ) * this._sliceSpacing;
|
|
|
|
var activeEdges = [];
|
|
var firstEdge = 0;
|
|
var nextEdge = 0;
|
|
var expirations = new PriorityQueue();
|
|
|
|
var createEdge = function ( startIndex, endIndex ) {
|
|
|
|
if ( nextEdge >= 12 ) { return undefined; }
|
|
|
|
var activeEdge = {
|
|
expired : false,
|
|
startIndex : startIndex,
|
|
endIndex : endIndex,
|
|
deltaPos : new THREE.Vector3(),
|
|
deltaTex : new THREE.Vector3(),
|
|
pos : new THREE.Vector3(),
|
|
tex : new THREE.Vector3(),
|
|
cur : nextEdge
|
|
}
|
|
|
|
var range = cornerDistance[ startIndex ] - cornerDistance[ endIndex ];
|
|
|
|
if ( range !== 0.0 ) {
|
|
|
|
var irange = 1.0 / range;
|
|
|
|
activeEdge.deltaPos.subVectors(
|
|
this._posCorners[ endIndex ],
|
|
this._posCorners[ startIndex ]
|
|
).multiplyScalar( irange );
|
|
|
|
activeEdge.deltaTex.subVectors(
|
|
this._texCorners[ endIndex ],
|
|
this._texCorners[ startIndex ]
|
|
).multiplyScalar( irange );
|
|
|
|
var step = cornerDistance[ startIndex ] - sliceDistance;
|
|
|
|
activeEdge.pos.addVectors(
|
|
activeEdge.deltaPos.clone().multiplyScalar( step ),
|
|
this._posCorners[ startIndex ]
|
|
);
|
|
|
|
activeEdge.tex.addVectors(
|
|
activeEdge.deltaTex.clone().multiplyScalar( step ),
|
|
this._texCorners[ startIndex ]
|
|
);
|
|
|
|
activeEdge.deltaPos.multiplyScalar( this._sliceSpacing );
|
|
activeEdge.deltaTex.multiplyScalar( this._sliceSpacing );
|
|
|
|
}
|
|
|
|
expirations.push( activeEdge, cornerDistance[ endIndex ] );
|
|
activeEdges[ nextEdge++ ] = activeEdge;
|
|
return activeEdge;
|
|
|
|
};
|
|
|
|
for ( i = 0; i < 3; i = ( i + 1 )|0 ) {
|
|
|
|
var activeEdge = createEdge.call( this, maxCorner, cornerNeighbors[ maxCorner ][ i ] );
|
|
activeEdge.prev = ( i + 2 ) % 3;
|
|
activeEdge.next = ( i + 1 ) % 3;
|
|
|
|
}
|
|
|
|
var nextIndex = 0;
|
|
|
|
while ( sliceDistance > minDistance ) {
|
|
|
|
while ( expirations.top().priority >= sliceDistance ) {
|
|
|
|
var edge = expirations.pop().object;
|
|
|
|
if ( edge.expired ) { continue; }
|
|
|
|
if (
|
|
edge.endIndex !== activeEdges[ edge.prev ].endIndex &&
|
|
edge.endIndex !== activeEdges[ edge.next ].endIndex
|
|
) {
|
|
|
|
// split this edge.
|
|
edge.expired = true;
|
|
|
|
// create two new edges.
|
|
var activeEdge1 = createEdge.call(
|
|
this,
|
|
edge.endIndex,
|
|
incomingEdges[ edge.endIndex ][ edge.startIndex ]
|
|
);
|
|
activeEdge1.prev = edge.prev;
|
|
activeEdges[ edge.prev ].next = nextEdge - 1;
|
|
activeEdge1.next = nextEdge;
|
|
|
|
var activeEdge2 = createEdge.call(
|
|
this,
|
|
edge.endIndex,
|
|
incomingEdges[ edge.endIndex ][ activeEdge1.endIndex ]
|
|
);
|
|
activeEdge2.prev = nextEdge - 2;
|
|
activeEdge2.next = edge.next;
|
|
activeEdges[activeEdge2.next].prev = nextEdge - 1;
|
|
firstEdge = nextEdge - 1;
|
|
|
|
} else {
|
|
|
|
// merge edge.
|
|
var prev;
|
|
var next;
|
|
|
|
if ( edge.endIndex === activeEdges[ edge.prev ].endIndex ) {
|
|
|
|
prev = activeEdges[ edge.prev ];
|
|
next = edge;
|
|
|
|
} else {
|
|
|
|
prev = edge;
|
|
next = activeEdges[ edge.next ];
|
|
|
|
}
|
|
|
|
prev.expired = true;
|
|
next.expired = true;
|
|
|
|
// make new edge
|
|
var activeEdge = createEdge.call(
|
|
this,
|
|
edge.endIndex,
|
|
incomingEdges[ edge.endIndex ][ prev.startIndex ]
|
|
);
|
|
activeEdge.prev = prev.prev;
|
|
activeEdges[ activeEdge.prev ].next = nextEdge - 1;
|
|
activeEdge.next = next.next;
|
|
activeEdges[ activeEdge.next ].prev = nextEdge - 1;
|
|
firstEdge = nextEdge - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var cur = firstEdge;
|
|
var count = 0;
|
|
|
|
do {
|
|
|
|
++count;
|
|
var activeEdge = activeEdges[ cur ];
|
|
this._points.push(
|
|
activeEdge.pos.x,
|
|
activeEdge.pos.y,
|
|
activeEdge.pos.z
|
|
);
|
|
this._texCoords.push(
|
|
activeEdge.tex.x,
|
|
activeEdge.tex.y,
|
|
activeEdge.tex.z
|
|
);
|
|
activeEdge.pos.add( activeEdge.deltaPos );
|
|
activeEdge.tex.add( activeEdge.deltaTex );
|
|
cur = activeEdge.next;
|
|
|
|
} while ( cur !== firstEdge );
|
|
|
|
for ( i = 2; i < count; i = ( i + 1 )|0 ) {
|
|
|
|
this._indexes.push(
|
|
nextIndex,
|
|
nextIndex + i - 1,
|
|
nextIndex + i
|
|
);
|
|
|
|
}
|
|
|
|
nextIndex += count;
|
|
sliceDistance -= this._sliceSpacing;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
VolumetricFire.texturePath = './textures/';
|
|
|
|
///
|
|
|
|
var PriorityQueue = function () {
|
|
|
|
this.contents = [];
|
|
this.sorted = false;
|
|
|
|
};
|
|
|
|
PriorityQueue.prototype = {
|
|
|
|
sort: function () {
|
|
|
|
this.contents.sort();
|
|
this.sorted = true;
|
|
|
|
},
|
|
|
|
pop: function () {
|
|
|
|
if ( !this.sorted ) {
|
|
|
|
this.sort();
|
|
|
|
}
|
|
|
|
return this.contents.pop();
|
|
|
|
},
|
|
|
|
top : function() {
|
|
|
|
if ( !this.sorted ) {
|
|
|
|
this.sort();
|
|
|
|
}
|
|
|
|
return this.contents[ this.contents.length - 1 ];
|
|
|
|
},
|
|
|
|
push : function( object, priority ) {
|
|
|
|
this.contents.push( { object: object, priority: priority } );
|
|
this.sorted = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return VolumetricFire;
|
|
|
|
} ) );
|