threex
Version:
Game Extensions for three.js http://www.threejsgames.com/extensions/
369 lines (338 loc) • 11.9 kB
HTML
<!DOCTYPE html>
<script src='../../../vendor/three.js/build/three.min.js'></script>
<script src='js/threex.textureutils.js'></script>
<body style='margin: 0px; background-color: #bbbbbb; overflow: hidden;'><script>
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor('black')
document.body.appendChild( renderer.domElement );
var updateFcts = [];
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000 );
camera.position.z = 3;
//////////////////////////////////////////////////////////////////////////////////
// comment //
//////////////////////////////////////////////////////////////////////////////////
var urls = [
// 'images/flame/flame00.png',
// 'images/flame/flame01.png',
// 'images/flame/flame02.png',
// 'images/flame/flame03.png',
// 'images/flame/flame04.png',
// 'images/flame/flame05.png',
// 'images/flame/flame06.png',
// 'images/flame/flame07.png',
// 'images/flame/flame08.png',
// 'images/flame/flame09.png',
// 'images/flame/flame10.png',
// 'images/flame/flame11.png',
'images/flame/flame12.png',
'images/flame/flame13.png',
'images/flame/flame14.png',
'images/flame/flame15.png',
'images/flame/flame16.png',
'images/flame/flame17.png',
'images/flame/flame18.png',
'images/flame/flame19.png',
'images/flame/flame20.png',
'images/flame/flame21.png',
'images/flame/flame22.png',
'images/flame/flame23.png',
'images/flame/flame24.png'
];
//////////////////////////////////////////////////////////////////////////////////
// comment //
//////////////////////////////////////////////////////////////////////////////////
// TODO put that in the particle itself ?
loadTremulousFlameParticules(urls, function(texture, urls){
// create sound
var sound = new FlameThrowerWebAudio()
// update sound
updateFcts.push(function(delta, now){ sound.update(delta, now) })
// create emitter
var nTiles = urls.length
var emitter = new FlameThrowerEmitter(texture, nTiles, scene)
// update emitter
updateFcts.push(function(delta, now){ emitter.update(delta, now) })
var position = new THREE.Vector3(-3,0,0)
//var position = new THREE.Vector3(0,-1,-2)
var position = new THREE.Vector3(-4,0,-3)
var velocity = new THREE.Vector3(7, 0, 0)
emitter.start(position, velocity)
// start the trigger and
emitter.trigger.start();
sound.trigger.start();
document.addEventListener('mousedown' , function(){
emitter.trigger.start()
sound.trigger.start();
})
document.addEventListener('mouseup' , function(){
emitter.trigger.stop()
sound.trigger.stop()
})
})
function loadTremulousFlameParticules(urls, onReady){
// load all the images from urls
THREEx.TextureUtils.loadImages(urls, function(images, urls){
// build a tiled spreadsheet canvas with images
var canvas = THREEx.TextureUtils.buildTiledSpriteSheet({
images : images,
spriteW : images[0].width,
spriteH : images[0].height,
nSpriteX: 1
});
// create the texture
var texture = new THREE.Texture( canvas );
texture.needsUpdate = true;
// generate Alpha as it got no alpha
THREEx.TextureUtils.generateAlphaFromLuminance(texture, 16, 1);
// notify caller
onReady(texture, urls)
})
}
//////////////////////////////////////////////////////////////////////////////////
// render the scene //
//////////////////////////////////////////////////////////////////////////////////
updateFcts.push(function(){
renderer.render( scene, camera );
})
//////////////////////////////////////////////////////////////////////////////////
// loop runner //
//////////////////////////////////////////////////////////////////////////////////
var lastTimeMsec= null
requestAnimationFrame(function animate(nowMsec){
// keep looping
requestAnimationFrame( animate );
// measure time
lastTimeMsec = lastTimeMsec || nowMsec-1000/60
var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
lastTimeMsec = nowMsec
// call each update function
updateFcts.forEach(function(updateFn){
updateFn(deltaMsec/1000, nowMsec/1000)
})
})
//////////////////////////////////////////////////////////////////////////////////
// comment //
//////////////////////////////////////////////////////////////////////////////////
function FlameThrowerEmitter(texture, nTiles, container){
// handle local loop
var updateFcts = [];
// init trigger
var trigger = new MidiTrigger(0.2, 0.2)
this.trigger = trigger;
// handle tweening
var age2Friction= (function(){
var gradient = createLinearGradient([
{x : 0.00, y: 1.00}, {x : 0.50, y: 1.00},
{x : 0.70, y: 0.95}, {x : 1.00, y: 0.95}
]);
return function(age, maxAge){ return gradient(age/maxAge) }
})();
var age2Opacity = (function(){
var tween = createTweenMidi(1, 0.1, 0.5)
return function(age, maxAge){ return tween(age/maxAge) }
})();
var age2uvOffset= function(age, maxAge){
var imageIdx = Math.floor(age/maxAge * nTiles);
var uvOffsetY = 1 - imageIdx * 1/nTiles;
return uvOffsetY
}
// put the emmiter
this.emit = function(startPosition, initialVelocity){
// randomize the initial position
var position = startPosition.clone()
// init sprite material
var material = new THREE.SpriteMaterial({
map : texture,
useScreenCoordinates : false,
transparent : true,
blending : THREE.AdditiveBlending
})
material.uvScale.set(1, 1/nTiles)
// init sprite
var sprite = new THREE.Sprite(material)
sprite.position.copy(position)
sprite.rotation = Math.random()*Math.PI*2
sprite.scale.set(1,1,1).multiplyScalar(2)
container.add( sprite )
// init age2OffsetY
var uvOffset = age2uvOffset(0, maxAge)
material.uvOffset.set(0, uvOffset)
var maxAge = 1.2 + (Math.random()-0.5)*0
// set velocity
var speed = initialVelocity.length()
var velocity = initialVelocity.clone().normalize()
velocity.x += (Math.random()-0.5)*0.0
velocity.y += (Math.random()-0.5)*0.2
velocity.z += (Math.random()-0.5)*0.0
velocity.setLength(speed)
// set acceleration
var acceleration= new THREE.Vector3(0, 2, 0)
// init opacity
material.opacity= age2Opacity(0, maxAge)
var birthDate = Date.now()/1000
updateFcts.push(function callback(delta, now){
var age = Date.now()/1000 - birthDate
if( age >= maxAge ){
sprite.parent.remove(sprite)
updateFcts.splice(updateFcts.indexOf(callback),1)
return;
}
// handle acceleration
velocity.add(acceleration.clone().multiplyScalar(delta))
// handle friction
velocity.multiplyScalar( age2Friction(age, maxAge) )
// move by velocity
sprite.position.add( velocity.clone().multiplyScalar(delta) )
// make it grow
sprite.scale.multiplyScalar( 1.015 )
// handle opacity
material.opacity= age2Opacity(age, maxAge)
// init uvOffset
material.uvOffset.set(0, age2uvOffset(age, maxAge))
})
}
var _loopCb = null;
this.start = function(startPosition, initialVelocity, maxRate){
maxRate = maxRate !== undefined ? maxRate : 15
var lastEmit = 0;
updateFcts.push(_loopCb = function(delta, now){
// rate limiter emition
var rate = maxRate * trigger.intensity();
if( rate === 0 || now - lastEmit < 1/rate ) return;
lastEmit = now;
// emit one
this.emit(startPosition, initialVelocity)
}.bind(this))
return this;
}
this.stop = function(){
updateFcts.splice(updateFcts.indexOf(_loopCb),1)
return this;
}
this.update = function(delta, now){
updateFcts.forEach(function(updateFct){
updateFct(delta, now)
})
}
/**
* trigger with a attack, a sustain and a release. like midi notes
*
* @param {Number} attackDelay nb second for the attack phase
* @param {Number} releaseDelay nb second for the release phase
* @param {Number} maxValue the maximum value of the intensity
*/
function MidiTrigger(attackDelay, releaseDelay){
var lastStart = 0, lastStop = 0;
this.start = function(){ lastStart = Date.now()/1000 }
this.stop = function(){ lastStop = Date.now()/1000 }
this.intensity = function(){
var present = Date.now()/1000
if( lastStop >= lastStart ){ // release in-progress or overs
if(present - lastStop >= releaseDelay) return 0 // release over
return 1 - (present - lastStop) / releaseDelay // release inprogress
}else if( present - lastStart <= attackDelay ){ // attack in-progress
return (present - lastStart) / attackDelay
}else return 1; // sustain in-progress
}
}
function createTweenMidi(maxAge, attackTime, releaseTime){
return function(age){
if( age < attackTime ){
return age / attackTime
}else if( age < maxAge - releaseTime ){
return 1;
}else{
return (maxAge - age) / releaseTime
}
}
}
function createLinearGradient(keyPoints){
return function(x){
// find the keyPoints
for( var i = 0; i < keyPoints.length; i++ ){
if( x <= keyPoints[i].x ) break;
}
if( i === 0 ) return keyPoints[0].y;
// sanity check
console.assert(i < keyPoints.length );
// compute the y
var previous = keyPoints[i-1];
var next = keyPoints[i];
var ratio = (x - previous.x) / (next.x - previous.x)
var y = previous.y + ratio * (next.y - previous.y)
// return y
return y;
}
}
}
//////////////////////////////////////////////////////////////////////////////////
// comment //
//////////////////////////////////////////////////////////////////////////////////
/**
* granular sound for FlameThrowerEmitter with WebAudio API
*/
function FlameThrowerWebAudio(){
// webaudio API shim
window.AudioContext = window.AudioContext || window.webkitAudioContext
// init trigger
var trigger = new MidiTrigger(0.2, 0.2)
this.trigger = trigger;
this.isAvailable= AudioContext ? true : false;
var audioCtx = new AudioContext();
var url = 'sounds/flamethrower-freesoundloop.wav';
var updateFcts = []
loadSoundWebAudio(url, function(buffer){
var sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = buffer;
sourceNode.loop = true;
var gainNode = audioCtx.createGain()
sourceNode.connect(gainNode);
gainNode.connect(audioCtx.destination);
sourceNode.start(0);
updateFcts.push(function(delta, now){
var intensity = trigger.intensity()
sourceNode.playbackRate.value = 0.2 + intensity*2.5;
gainNode.gain.value = intensity*15;
});
})
this.update = function(delta, now){
updateFcts.forEach(function(updateFct){
updateFct(delta, now)
})
}
function loadSoundWebAudio(url, onLoad, onError){
onLoad = onLoad || function(){}
onError = onError || function(){}
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
audioCtx.decodeAudioData(request.response, onLoad, onError);
}
request.send();
}
/**
* trigger with a attack, a sustain and a release. like midi notes
*
* @param {Number} attackDelay nb second for the attack phase
* @param {Number} releaseDelay nb second for the release phase
* @param {Number} maxValue the maximum value of the intensity
*/
function MidiTrigger(attackDelay, releaseDelay){
var lastStart = 0, lastStop = 0;
this.start = function(){ lastStart = Date.now()/1000 }
this.stop = function(){ lastStop = Date.now()/1000 }
this.intensity = function(){
var present = Date.now()/1000
if( lastStop >= lastStart ){ // release in-progress or overs
if(present - lastStop >= releaseDelay) return 0 // release over
return 1 - (present - lastStop) / releaseDelay // release inprogress
}else if( present - lastStart <= attackDelay ){ // attack in-progress
return (present - lastStart) / attackDelay
}else return 1; // sustain in-progress
}
}
}
</script></body>