liteng 812a609b8b 1、火焰插件改为通过js引入。
2、不再为选中物体渲染边框。
2018-09-04 12:02:38 +08:00

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;
} ) );