diff --git a/clients/tests/compare/base-attachments.png b/clients/tests/compare/base-attachments.png index 9f54ba51..3474b931 100644 Binary files a/clients/tests/compare/base-attachments.png and b/clients/tests/compare/base-attachments.png differ diff --git a/clients/tests/compare/mdx-base.png b/clients/tests/compare/mdx-base.png index fb1cf685..f900fb2a 100644 Binary files a/clients/tests/compare/mdx-base.png and b/clients/tests/compare/mdx-base.png differ diff --git a/clients/tests/compare/mdx-event-object-spl.png b/clients/tests/compare/mdx-event-object-spl.png index 88625264..62da12b4 100644 Binary files a/clients/tests/compare/mdx-event-object-spl.png and b/clients/tests/compare/mdx-event-object-spl.png differ diff --git a/clients/tests/compare/mdx-event-object-spn.png b/clients/tests/compare/mdx-event-object-spn.png index 8e219e31..2aa21e14 100644 Binary files a/clients/tests/compare/mdx-event-object-spn.png and b/clients/tests/compare/mdx-event-object-spn.png differ diff --git a/clients/tests/compare/mdx-event-object-ubr.png b/clients/tests/compare/mdx-event-object-ubr.png index 795430c9..cc32295c 100644 Binary files a/clients/tests/compare/mdx-event-object-ubr.png and b/clients/tests/compare/mdx-event-object-ubr.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-base.png b/clients/tests/compare/mdx-particle-2-emitter-base.png index 0a81d028..dd3c4e62 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-base.png and b/clients/tests/compare/mdx-particle-2-emitter-base.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-line-emitter.png b/clients/tests/compare/mdx-particle-2-emitter-line-emitter.png index 96a4d334..d0a1aa5b 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-line-emitter.png and b/clients/tests/compare/mdx-particle-2-emitter-line-emitter.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-repeat.png b/clients/tests/compare/mdx-particle-2-emitter-repeat.png index 85a686f2..e61d4614 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-repeat.png and b/clients/tests/compare/mdx-particle-2-emitter-repeat.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-squirt.png b/clients/tests/compare/mdx-particle-2-emitter-squirt.png index 96ec1e5a..5c66254e 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-squirt.png and b/clients/tests/compare/mdx-particle-2-emitter-squirt.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-tail.png b/clients/tests/compare/mdx-particle-2-emitter-tail.png index a8c70436..ffbf40ed 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-tail.png and b/clients/tests/compare/mdx-particle-2-emitter-tail.png differ diff --git a/clients/tests/compare/mdx-particle-2-emitter-xy-quad.png b/clients/tests/compare/mdx-particle-2-emitter-xy-quad.png index b29dfe76..fa323fcb 100644 Binary files a/clients/tests/compare/mdx-particle-2-emitter-xy-quad.png and b/clients/tests/compare/mdx-particle-2-emitter-xy-quad.png differ diff --git a/clients/tests/compare/mdx-ribbon-emitter.png b/clients/tests/compare/mdx-ribbon-emitter.png index c2e2735d..6e411506 100644 Binary files a/clients/tests/compare/mdx-ribbon-emitter.png and b/clients/tests/compare/mdx-ribbon-emitter.png differ diff --git a/clients/tests/compare/mdx-sequence.png b/clients/tests/compare/mdx-sequence.png index a6ecaa78..6a7fd6c9 100644 Binary files a/clients/tests/compare/mdx-sequence.png and b/clients/tests/compare/mdx-sequence.png differ diff --git a/clients/tests/compare/mdx-team-color.png b/clients/tests/compare/mdx-team-color.png index c6884c70..8e63eb53 100644 Binary files a/clients/tests/compare/mdx-team-color.png and b/clients/tests/compare/mdx-team-color.png differ diff --git a/clients/tests/compare/mdx-texture-animation-translation-and-slot.png b/clients/tests/compare/mdx-texture-animation-translation-and-slot.png index dbd87d01..85a9ae71 100644 Binary files a/clients/tests/compare/mdx-texture-animation-translation-and-slot.png and b/clients/tests/compare/mdx-texture-animation-translation-and-slot.png differ diff --git a/clients/tests/compare/mdx-texture-overriding.png b/clients/tests/compare/mdx-texture-overriding.png index c46ed786..45ae0882 100644 Binary files a/clients/tests/compare/mdx-texture-overriding.png and b/clients/tests/compare/mdx-texture-overriding.png differ diff --git a/clients/tests/compare/mdx-vertex-and-team-colors.png b/clients/tests/compare/mdx-vertex-and-team-colors.png index 4f415d21..ae50650a 100644 Binary files a/clients/tests/compare/mdx-vertex-and-team-colors.png and b/clients/tests/compare/mdx-vertex-and-team-colors.png differ diff --git a/clients/tests/compare/mdx-vertex-color.png b/clients/tests/compare/mdx-vertex-color.png index d65775ef..94600dae 100644 Binary files a/clients/tests/compare/mdx-vertex-color.png and b/clients/tests/compare/mdx-vertex-color.png differ diff --git a/clients/tests/mdx.js b/clients/tests/mdx.js index 272fa21e..f1b89753 100644 --- a/clients/tests/mdx.js +++ b/clients/tests/mdx.js @@ -377,7 +377,7 @@ let mdxTests = { scene.addInstance(instance); - for (let i = 0; i < 205; i++) { + for (let i = 0; i < 170; i++) { viewer.update(); } }, diff --git a/package.json b/package.json index cd9b231b..06bf68d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mdx-m3-viewer", - "version": "4.7.7", + "version": "4.8.0", "description": "A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.", "main": "src/index.js", "directories": { diff --git a/src/index.js b/src/index.js index 334f2925..8d1e1448 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ import viewer from './viewer'; import utils from './utils'; export default { - version: '4.7.7', + version: '4.8.0', common, parsers, viewer, diff --git a/src/viewer/handlers/mdx/sharedemitter.js b/src/viewer/handlers/mdx/emitter.js similarity index 74% rename from src/viewer/handlers/mdx/sharedemitter.js rename to src/viewer/handlers/mdx/emitter.js index a34ac137..53523963 100644 --- a/src/viewer/handlers/mdx/sharedemitter.js +++ b/src/viewer/handlers/mdx/emitter.js @@ -1,12 +1,15 @@ /** - * A shared emitter. + * An emitter. * The base class of all MDX emitters. */ -export default class SharedEmitter { +export default class Emitter { /** + * @param {ModelViewData} modelViewData * @param {ParticleEmitter|ParticleEmitter2|RibbonEmitter|EventObject} modelObject */ - constructor(modelObject) { + constructor(modelViewData, modelObject) { + /** @member {ModelViewData} */ + this.modelViewData = modelViewData; /** @member {ParticleEmitter|ParticleEmitter2|RibbonEmitter|EventObject} */ this.modelObject = modelObject; /** @member {Array} */ @@ -16,13 +19,13 @@ export default class SharedEmitter { } /** - * Note: flag is used for ParticleEmitter2's head/tail selection. + * Note: tail is used for ParticleEmitter2's head/tail selection. * * @param {ParticleEmitterView|ParticleEmitter2View|RibbonEmitterView|EventObjectEmitterView} emitterView - * @param {boolean} flag + * @param {number} tail * @return {Particle|Particle2|Ribbon|EventObjectSpn|EventObjectSpl|EventObjectUbr} */ - emitObject(emitterView, flag) { + emitObject(emitterView, tail) { let objects = this.objects; // If there are no unused objects, create a new one. @@ -35,7 +38,7 @@ export default class SharedEmitter { this.alive += 1; - object.reset(emitterView, flag); + object.bind(emitterView, tail); return object; } @@ -44,18 +47,22 @@ export default class SharedEmitter { * */ update() { + let dt = this.modelObject.model.viewer.frameTime * 0.001; let objects = this.objects; + let offset = 0; for (let i = 0; i < this.alive; i++) { let object = objects[i]; - object.update(); + object.render(offset, dt); - if (object.health <= 0) { + if (object.health > 0) { + offset += 1; + } else { this.alive -= 1; - // Swap between this object and the first unused object. - // Decrement the iterator so the moved object is indexed. + // Swap between this object and the last living object. + // Decrement the iterator so the swapped object is updated this frame. if (i !== this.alive) { objects[i] = objects[this.alive]; objects[this.alive] = object; @@ -63,8 +70,6 @@ export default class SharedEmitter { } } } - - this.updateData(); } /** @@ -80,13 +85,6 @@ export default class SharedEmitter { } } - /** - * - */ - updateData() { - - } - /** * @param {ModelView} modelView * @param {ShaderProgram} shader diff --git a/src/viewer/handlers/mdx/emittergroup.js b/src/viewer/handlers/mdx/emittergroup.js index 09c37f99..594d2257 100644 --- a/src/viewer/handlers/mdx/emittergroup.js +++ b/src/viewer/handlers/mdx/emittergroup.js @@ -4,15 +4,12 @@ export default class EmitterGroup { /** * @param {ModelView} modelView - * @param {boolean} isRibbons */ - constructor(modelView, isRibbons) { + constructor(modelView) { /** @member {ModelView} */ this.modelView = modelView; /** @member {Array} */ this.objects = []; - /** @member {boolean} */ - this.isRibbons = isRibbons; } /** @@ -21,8 +18,11 @@ export default class EmitterGroup { render(modelViewData) { let viewer = this.modelView.model.viewer; let gl = viewer.gl; + let instancedArrays = gl.extensions.instancedArrays; let modelView = modelViewData.modelView; let shader = viewer.shaderMap.get('MdxParticleShader'); + let uniforms = shader.uniforms; + let attribs = shader.attribs; gl.depthMask(0); gl.enable(gl.BLEND); @@ -31,12 +31,34 @@ export default class EmitterGroup { viewer.webgl.useShaderProgram(shader); - gl.uniformMatrix4fv(shader.uniforms.u_mvp, false, modelViewData.scene.camera.worldProjectionMatrix); - gl.uniform1i(shader.uniforms.u_texture, 0); - gl.uniform1f(shader.uniforms.u_isRibbonEmitter, this.isRibbons); + gl.uniformMatrix4fv(uniforms.u_mvp, false, modelViewData.scene.camera.worldProjectionMatrix); + gl.uniform1i(uniforms.u_texture, 0); + + instancedArrays.vertexAttribDivisorANGLE(attribs.a_position, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, viewer.rectBuffer); + gl.vertexAttribPointer(attribs.a_position, 1, gl.UNSIGNED_BYTE, false, 0, 0); + + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p0, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p1, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p2, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p3, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_health, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_color, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_tail, 1); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_leftRightTop, 1); for (let emitter of this.objects) { emitter.render(modelView, shader); } + + instancedArrays.vertexAttribDivisorANGLE(attribs.a_leftRightTop, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_tail, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_color, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_health, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p3, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p2, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p1, 0); + instancedArrays.vertexAttribDivisorANGLE(attribs.a_p0, 0); } } diff --git a/src/viewer/handlers/mdx/eventobjectsndemitter.js b/src/viewer/handlers/mdx/eventobjectsndemitter.js index 04d24ef7..7204c414 100644 --- a/src/viewer/handlers/mdx/eventobjectsndemitter.js +++ b/src/viewer/handlers/mdx/eventobjectsndemitter.js @@ -7,7 +7,27 @@ export default class EventObjectSndEmitter { */ constructor(modelObject) { this.modelObject = modelObject; - this.type = 'SND'; + + /** + * Does nothing. + * Defined to stay compatible with Emitter. + * + * @member {number} + */ + this.alive = 0; + } + + /** + * @param {*} emitterView + */ + fill(emitterView) { + let emission = emitterView.currentEmission; + + if (emission >= 1) { + for (let i = 0; i < emission; i += 1, emitterView.currentEmission--) { + this.emit(emitterView); + } + } } /** @@ -43,26 +63,17 @@ export default class EventObjectSndEmitter { } /** - * + * Does nothing. + * Defined to stay compatible with Emitter. */ update() { } /** - * @param {*} emitterView - */ - fill(emitterView) { - let emission = emitterView.currentEmission; - - if (emission >= 1) { - for (let i = 0; i < emission; i += 1, emitterView.currentEmission--) { - this.emit(emitterView); - } - } - } - - /** + * Does nothing. + * Defined to stay compatible with Emitter. + * * @param {ModelView} modelView * @param {ShaderProgram} shader */ @@ -72,7 +83,7 @@ export default class EventObjectSndEmitter { /** * Does nothing. - * Defined to stay compatible with SharedEmitter. + * Defined to stay compatible with Emitter. * * @param {ModelInstance} owner */ diff --git a/src/viewer/handlers/mdx/eventobjectspl.js b/src/viewer/handlers/mdx/eventobjectspl.js deleted file mode 100644 index 297b43e8..00000000 --- a/src/viewer/handlers/mdx/eventobjectspl.js +++ /dev/null @@ -1,106 +0,0 @@ -import {vec3, vec4} from 'gl-matrix'; -import {lerp} from '../../../common/math'; -import {uint8ToUint24} from '../../../common/typecast'; - -/** - * An MDX splat object. - */ -export default class EventObjectSpl { - /** - * @param {MdxEventObjectEmitter} emitter - */ - constructor(emitter) { - this.emitter = emitter; - this.emitterView = null; - this.health = 0; - this.color = new Uint8Array(4); - this.vertices = new Float32Array(12); - this.lta = 0; - this.lba = 0; - this.rta = 0; - this.rba = 0; - this.rgb = 0; - } - - /** - * @param {EventObjectEmitterView} emitterView - */ - reset(emitterView) { - let modelObject = this.emitter.modelObject; - let vertices = this.vertices; - let emitterScale = modelObject.scale; - let node = emitterView.instance.nodes[modelObject.index]; - let worldMatrix = node.worldMatrix; - let vertex; - - this.emitterView = emitterView; - - vertex = vertices.subarray(0, 2); - vec3.transformMat4(vertex, [-emitterScale, -emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(3, 5); - vec3.transformMat4(vertex, [-emitterScale, emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(6, 8); - vec3.transformMat4(vertex, [emitterScale, emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(9, 11); - vec3.transformMat4(vertex, [emitterScale, -emitterScale, 0], worldMatrix); - - this.health = modelObject.lifespan; - } - - /** - * - */ - update() { - let modelObject = this.emitter.modelObject; - let columns = modelObject.dimensions[0]; - let intervalTimes = modelObject.intervalTimes; - let intervals = modelObject.intervals; - let first = intervalTimes[0]; - let second = intervalTimes[1]; - let colors = modelObject.colors; - let color = this.color; - let factor; - let interval; - let firstColor; - let index; - - this.health -= modelObject.model.viewer.frameTime * 0.001; - - // Inverse of health - let time = modelObject.lifespan - this.health; - - if (time < first) { - factor = time / first; - interval = intervals[0]; - firstColor = 0; - } else { - factor = (time - first) / second; - interval = intervals[1]; - firstColor = 1; - } - - // Interpolated color - vec4.lerp(color, colors[firstColor], colors[firstColor + 1], factor); - - // The texture portion to index - index = Math.floor(lerp(interval[0], interval[1], factor)); - - // Calculate the UV rectangle. - let left = index % columns; - let top = Math.floor(index / columns); - let right = left + 1; - let bottom = top + 1; - let a = color[3]; - - // Encode the UV rectangle and color in floats. - // This is a shader optimization. - this.lta = uint8ToUint24(right, bottom, a); - this.lba = uint8ToUint24(left, bottom, a); - this.rta = uint8ToUint24(right, top, a); - this.rba = uint8ToUint24(left, top, a); - this.rgb = uint8ToUint24(color[0], color[1], color[2]); - } -} diff --git a/src/viewer/handlers/mdx/eventobjectsplemitter.js b/src/viewer/handlers/mdx/eventobjectsplemitter.js index 4be5bf3e..7973dea3 100644 --- a/src/viewer/handlers/mdx/eventobjectsplemitter.js +++ b/src/viewer/handlers/mdx/eventobjectsplemitter.js @@ -1,20 +1,10 @@ -import SharedGeometryEmitter from './sharedgeometryemitter'; -import EventObjectSpl from './eventobjectspl'; +import {GeometryEmitter, EMITTER_SPLAT} from './geometryemitter'; +import EventObjectSplUbr from './eventobjectsplubr'; /** * An MDX splat emitter. */ -export default class EventObjectSplEmitter extends SharedGeometryEmitter { - /** - * @param {EventObject} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.type = 'SPL'; - this.elementsPerEmit = 30; - } - +export default class EventObjectSplEmitter extends GeometryEmitter { /** * @param {EventObjectEmitterView} emitterView */ @@ -25,9 +15,43 @@ export default class EventObjectSplEmitter extends SharedGeometryEmitter { } /** - * @return {EventObjectSpl} + * @return {EventObjectSplUbr} */ createObject() { - return new EventObjectSpl(this); + return new EventObjectSplUbr(this); + } + + /** + * @param {ModelView} modelView + * @param {ShaderProgram} shader + */ + bind(modelView, shader) { + let modelObject = this.modelObject; + let intervalTimes = modelObject.intervalTimes; + let intervals = modelObject.intervals; + let colors = modelObject.colors; + let model = modelObject.model; + let gl = model.viewer.gl; + let uniforms = shader.uniforms; + + gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); + + model.bindTexture(modelObject.internalResource, 0, modelView); + + gl.uniform1f(uniforms.u_emitter, EMITTER_SPLAT); + + gl.uniform1f(uniforms.u_lifeSpan, modelObject.lifeSpan); + gl.uniform1f(uniforms.u_columns, modelObject.columns); + gl.uniform1f(uniforms.u_rows, modelObject.rows); + + // 3 because the uniform is shared with UBR, which has 3 intervals. + gl.uniform3f(uniforms.u_intervalTimes, intervalTimes[0], intervalTimes[1], 0); + + gl.uniform3fv(uniforms['u_intervals[0]'], intervals[0]); + gl.uniform3fv(uniforms['u_intervals[1]'], intervals[1]); + + gl.uniform4fv(uniforms['u_colors[0]'], colors[0]); + gl.uniform4fv(uniforms['u_colors[1]'], colors[1]); + gl.uniform4fv(uniforms['u_colors[2]'], colors[2]); } } diff --git a/src/viewer/handlers/mdx/eventobjectsplubr.js b/src/viewer/handlers/mdx/eventobjectsplubr.js new file mode 100644 index 00000000..9b865a19 --- /dev/null +++ b/src/viewer/handlers/mdx/eventobjectsplubr.js @@ -0,0 +1,85 @@ +import {vec3} from 'gl-matrix'; +import {FLOATS_PER_OBJECT, FLOAT_OFFSET_P0, FLOAT_OFFSET_HEALTH} from './geometryemitter'; + +const vertexHeap = vec3.create(); + +/** + * An MDX splat or ubersplat object. + */ +export default class EventObjectSplUbr { + /** + * @param {MdxEventObjectEmitter} emitter + */ + constructor(emitter) { + this.emitter = emitter; + this.emitterView = null; + this.health = 0; + this.vertices = new Float32Array(12); + } + + /** + * @param {EventObjectEmitterView} emitterView + */ + bind(emitterView) { + let modelObject = this.emitter.modelObject; + let vertices = this.vertices; + let scale = modelObject.scale; + let node = emitterView.instance.nodes[modelObject.index]; + let worldMatrix = node.worldMatrix; + + this.emitterView = emitterView; + this.health = modelObject.lifeSpan; + + // Note that the order here isn't the same as particles/ribbons. + vertexHeap[0] = scale; + vertexHeap[1] = scale; + vertexHeap[2] = 0; + vec3.transformMat4(vertices.subarray(0, 2), vertexHeap, worldMatrix); + + vertexHeap[0] = -scale; + vertexHeap[1] = scale; + vertexHeap[2] = 0; + vec3.transformMat4(vertices.subarray(3, 5), vertexHeap, worldMatrix); + + vertexHeap[0] = -scale; + vertexHeap[1] = -scale; + vertexHeap[2] = 0; + vec3.transformMat4(vertices.subarray(6, 8), vertexHeap, worldMatrix); + + vertexHeap[0] = scale; + vertexHeap[1] = -scale; + vertexHeap[2] = 0; + vec3.transformMat4(vertices.subarray(9, 11), vertexHeap, worldMatrix); + } + + /** + * @param {number} offset + * @param {number} dt + */ + render(offset, dt) { + this.health -= dt; + + if (this.health > 0) { + let emitter = this.emitter; + let floatView = emitter.floatView; + let floatOffset = offset * FLOATS_PER_OBJECT; + let p0Offset = floatOffset + FLOAT_OFFSET_P0; + let vertices = this.vertices; + + floatView[p0Offset + 0] = vertices[0]; + floatView[p0Offset + 1] = vertices[1]; + floatView[p0Offset + 2] = vertices[2]; + floatView[p0Offset + 3] = vertices[3]; + floatView[p0Offset + 4] = vertices[4]; + floatView[p0Offset + 5] = vertices[5]; + floatView[p0Offset + 6] = vertices[6]; + floatView[p0Offset + 7] = vertices[7]; + floatView[p0Offset + 8] = vertices[8]; + floatView[p0Offset + 9] = vertices[9]; + floatView[p0Offset + 10] = vertices[10]; + floatView[p0Offset + 11] = vertices[11]; + + floatView[floatOffset + FLOAT_OFFSET_HEALTH] = this.health; + } + } +} diff --git a/src/viewer/handlers/mdx/eventobjectspn.js b/src/viewer/handlers/mdx/eventobjectspn.js index d67694b0..550023da 100644 --- a/src/viewer/handlers/mdx/eventobjectspn.js +++ b/src/viewer/handlers/mdx/eventobjectspn.js @@ -15,7 +15,7 @@ export default class EventObjectSpn { /** * @param {EventObjectEmitterView} emitterView */ - reset(emitterView) { + bind(emitterView) { let instance = this.internalResource; let node = emitterView.instance.nodes[this.emitter.modelObject.index]; @@ -31,9 +31,10 @@ export default class EventObjectSpn { } /** - * + * @param {number} offset + * @param {number} dt */ - update() { + render(offset, dt) { let instance = this.internalResource; // Once the sequence finishes, this event object dies diff --git a/src/viewer/handlers/mdx/eventobjectspnemitter.js b/src/viewer/handlers/mdx/eventobjectspnemitter.js index 3700cfd4..f1e2b953 100644 --- a/src/viewer/handlers/mdx/eventobjectspnemitter.js +++ b/src/viewer/handlers/mdx/eventobjectspnemitter.js @@ -1,19 +1,10 @@ -import SharedEmitter from './sharedemitter'; +import Emitter from './emitter'; import EventObjectSpn from './eventobjectspn'; /** * An MDX model emitter. */ -export default class EventObjectSpnEmitter extends SharedEmitter { - /** - * @param {EventObject} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.type = 'SPN'; - } - +export default class EventObjectSpnEmitter extends Emitter { /** * @param {EventObjectEmitterView} emitterView */ diff --git a/src/viewer/handlers/mdx/eventobjectubr.js b/src/viewer/handlers/mdx/eventobjectubr.js deleted file mode 100644 index 97720d68..00000000 --- a/src/viewer/handlers/mdx/eventobjectubr.js +++ /dev/null @@ -1,88 +0,0 @@ -import {vec3, vec4} from 'gl-matrix'; -import {uint8ToUint24} from '../../../common/typecast'; - -/** - * An MDX ubersplat object. - */ -export default class EventObjectUbr { - /** - * @param {MdxEventObjectEmitter} emitter - */ - constructor(emitter) { - this.emitter = emitter; - this.emitterView = null; - this.health = 0; - this.color = new Uint8Array(4); - this.vertices = new Float32Array(12); - this.lta = 0; - this.lba = 0; - this.rta = 0; - this.rba = 0; - this.rgb = 0; - } - - /** - * @param {EventObjectEmitterView} emitterView - */ - reset(emitterView) { - let modelObject = this.emitter.modelObject; - let vertices = this.vertices; - let emitterScale = modelObject.scale; - let node = emitterView.instance.nodes[modelObject.index]; - let worldMatrix = node.worldMatrix; - let vertex; - - this.emitterView = emitterView; - - vertex = vertices.subarray(0, 2); - vec3.transformMat4(vertex, [-emitterScale, -emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(3, 5); - vec3.transformMat4(vertex, [-emitterScale, emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(6, 8); - vec3.transformMat4(vertex, [emitterScale, emitterScale, 0], worldMatrix); - - vertex = vertices.subarray(9, 11); - vec3.transformMat4(vertex, [emitterScale, -emitterScale, 0], worldMatrix); - - this.health = modelObject.lifespan; - } - - /** - * - */ - update() { - let modelObject = this.emitter.modelObject; - let intervalTimes = modelObject.intervalTimes; - let first = intervalTimes[0]; - let second = intervalTimes[1]; - let third = intervalTimes[2]; - let colors = modelObject.colors; - let color = this.color; - - this.health -= modelObject.model.viewer.frameTime * 0.001; - - // Inverse of health - let time = modelObject.lifespan - this.health; - - if (time < first) { - vec4.lerp(color, colors[0], colors[1], time / first); - } else if (time < first + second) { - vec4.copy(color, modelObject.colors[1]); - } else { - vec4.lerp(color, colors[1], colors[2], (time - first - second) / third); - } - - // Calculate the UV rectangle. - let a = color[3]; - - // Encode the UV rectangle and color in floats. - // This is a shader optimization. - this.lta = uint8ToUint24(1, 0, a); - this.lba = uint8ToUint24(0, 0, a); - this.rta = uint8ToUint24(1, 1, a); - this.rba = uint8ToUint24(0, 1, a); - this.rgb = uint8ToUint24(color[0], color[1], color[2]); - } -} diff --git a/src/viewer/handlers/mdx/eventobjectubremitter.js b/src/viewer/handlers/mdx/eventobjectubremitter.js index 9d462351..da8eb13c 100644 --- a/src/viewer/handlers/mdx/eventobjectubremitter.js +++ b/src/viewer/handlers/mdx/eventobjectubremitter.js @@ -1,20 +1,10 @@ -import SharedGeometryEmitter from './sharedgeometryemitter'; -import EventObjectUbr from './eventobjectubr'; +import {GeometryEmitter, EMITTER_UBER} from './geometryemitter'; +import EventObjectSplUbr from './eventobjectsplubr'; /** * An MDX ubersplat emitter. */ -export default class EventObjectUbrEmitter extends SharedGeometryEmitter { - /** - * @param {EventObject} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.type = 'UBR'; - this.elementsPerEmit = 30; - } - +export default class EventObjectUbrEmitter extends GeometryEmitter { /** * @param {EventObjectEmitterView} emitterView */ @@ -28,6 +18,35 @@ export default class EventObjectUbrEmitter extends SharedGeometryEmitter { * @return {EventObjectUbr} */ createObject() { - return new EventObjectUbr(this); + return new EventObjectSplUbr(this); + } + + /** + * @param {ModelView} modelView + * @param {ShaderProgram} shader + */ + bind(modelView, shader) { + let modelObject = this.modelObject; + let intervalTimes = modelObject.intervalTimes; + let colors = modelObject.colors; + let model = modelObject.model; + let gl = model.viewer.gl; + let uniforms = shader.uniforms; + + gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); + + model.bindTexture(modelObject.internalResource, 0, modelView); + + gl.uniform1f(uniforms.u_emitter, EMITTER_UBER); + + gl.uniform1f(uniforms.u_lifeSpan, modelObject.lifeSpan); + gl.uniform1f(uniforms.u_columns, modelObject.columns); + gl.uniform1f(uniforms.u_rows, modelObject.rows); + + gl.uniform3fv(uniforms.u_intervalTimes, intervalTimes); + + gl.uniform4fv(uniforms['u_colors[0]'], colors[0]); + gl.uniform4fv(uniforms['u_colors[1]'], colors[1]); + gl.uniform4fv(uniforms['u_colors[2]'], colors[2]); } } diff --git a/src/viewer/handlers/mdx/geometryemitter.js b/src/viewer/handlers/mdx/geometryemitter.js new file mode 100644 index 00000000..feb22423 --- /dev/null +++ b/src/viewer/handlers/mdx/geometryemitter.js @@ -0,0 +1,112 @@ +import {powerOfTwo} from '../../../common/math'; +import Emitter from './emitter'; + +// The total storage that emitted objects can use. +// This is enough to support all of the MDX geometry emitters. +// The memory layout is the same as this C struct: +// +// struct { +// float p0[3] +// float p1[3] +// float p2[3] +// float p3[3] +// float health +// byte color[4] +// byte tail +// byte leftRightTop[3] +// } +export const BYTES_PER_OBJECT = 60; +export const FLOATS_PER_OBJECT = BYTES_PER_OBJECT >> 2; + +// Offsets into the emitted object structure. +export const BYTE_OFFSET_P0 = 0; +export const BYTE_OFFSET_P1 = 12; +export const BYTE_OFFSET_P2 = 24; +export const BYTE_OFFSET_P3 = 36; +export const BYTE_OFFSET_HEALTH = 48; +export const BYTE_OFFSET_COLOR = 52; +export const BYTE_OFFSET_TAIL = 56; +export const BYTE_OFFSET_LEFT_RIGHT_TOP = 57; + +// Offset aliases. +export const FLOAT_OFFSET_P0 = BYTE_OFFSET_P0 >> 2; +export const FLOAT_OFFSET_P1 = BYTE_OFFSET_P1 >> 2; +export const FLOAT_OFFSET_P2 = BYTE_OFFSET_P2 >> 2; +export const FLOAT_OFFSET_P3 = BYTE_OFFSET_P3 >> 2; +export const FLOAT_OFFSET_HEALTH = BYTE_OFFSET_HEALTH >> 2; +export const BYTE_OFFSET_TEAM_COLOR = BYTE_OFFSET_LEFT_RIGHT_TOP; + +// Head or tail. +export const HEAD = 0; +export const TAIL = 1; + +// Emitter types +export const EMITTER_PARTICLE2 = 0; +export const EMITTER_RIBBON = 1; +export const EMITTER_SPLAT = 2; +export const EMITTER_UBER = 3; + +/** + * A geometry emitter. + * The base class of all MDX geometry emitters. + */ +export class GeometryEmitter extends Emitter { + /** + * @param {ModelViewData} modelViewData + * @param {ParticleEmitter2|RibbonEmitter|EventObjectSplEmitter|EventObjectUbrEmitter} modelObject + */ + constructor(modelViewData, modelObject) { + super(modelViewData, modelObject); + + this.arrayBuffer = new ArrayBuffer(0); + this.byteView = null; + this.floatView = null; + this.buffer = modelObject.model.viewer.gl.createBuffer(); + } + + /** + * @override + */ + update() { + let bytesNeeded = this.alive * BYTES_PER_OBJECT; + + if (this.arrayBuffer.byteLength < bytesNeeded) { + let gl = this.modelObject.model.viewer.gl; + + this.arrayBuffer = new ArrayBuffer(powerOfTwo(bytesNeeded)); + this.byteView = new Uint8Array(this.arrayBuffer); + this.floatView = new Float32Array(this.arrayBuffer); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, this.arrayBuffer.byteLength, gl.DYNAMIC_DRAW); + } + + super.update(); + } + + /** + * @param {ModelView} modelView + * @param {ShaderProgram} shader + */ + render(modelView, shader) { + if (this.alive) { + let gl = this.modelObject.model.viewer.gl; + let attribs = shader.attribs; + + this.bind(modelView, shader); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.byteView.subarray(0, this.alive * BYTES_PER_OBJECT)); + gl.vertexAttribPointer(attribs.a_p0, 3, gl.FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P0); + gl.vertexAttribPointer(attribs.a_p1, 3, gl.FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P1); + gl.vertexAttribPointer(attribs.a_p2, 3, gl.FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P2); + gl.vertexAttribPointer(attribs.a_p3, 3, gl.FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_P3); + gl.vertexAttribPointer(attribs.a_health, 1, gl.FLOAT, false, BYTES_PER_OBJECT, BYTE_OFFSET_HEALTH); + gl.vertexAttribPointer(attribs.a_color, 4, gl.UNSIGNED_BYTE, true, BYTES_PER_OBJECT, BYTE_OFFSET_COLOR); + gl.vertexAttribPointer(attribs.a_tail, 1, gl.UNSIGNED_BYTE, false, BYTES_PER_OBJECT, BYTE_OFFSET_TAIL); + gl.vertexAttribPointer(attribs.a_leftRightTop, 3, gl.UNSIGNED_BYTE, false, BYTES_PER_OBJECT, BYTE_OFFSET_LEFT_RIGHT_TOP); + + gl.extensions.instancedArrays.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, this.alive); + } + } +} diff --git a/src/viewer/handlers/mdx/model.js b/src/viewer/handlers/mdx/model.js index 44b9d7bf..52135ad7 100644 --- a/src/viewer/handlers/mdx/model.js +++ b/src/viewer/handlers/mdx/model.js @@ -21,7 +21,7 @@ import CollisionShape from './collisionshape'; /** * An MDX model. */ -export default class Model extends TexturedModel { +export default class MdxModel extends TexturedModel { /** * @param {Object} resourceData */ diff --git a/src/viewer/handlers/mdx/modeleventobject.js b/src/viewer/handlers/mdx/modeleventobject.js index b9d2c26b..f3cdcf79 100644 --- a/src/viewer/handlers/mdx/modeleventobject.js +++ b/src/viewer/handlers/mdx/modeleventobject.js @@ -101,13 +101,16 @@ export default class EventObject extends GenericObject { this.dimensions = [row.Columns, row.Rows]; this.intervals = [[row.UVLifespanStart, row.UVLifespanEnd, row.LifespanRepeat], [row.UVDecayStart, row.UVDecayEnd, row.DecayRepeat]]; this.intervalTimes = [row.Lifespan, row.Decay]; - this.lifespan = row.Lifespan + row.Decay; + this.lifeSpan = row.Lifespan + row.Decay; } else { this.dimensions = [1, 1]; this.intervalTimes = [row.BirthTime, row.PauseTime, row.Decay]; - this.lifespan = row.BirthTime + row.PauseTime + row.Decay; + this.lifeSpan = row.BirthTime + row.PauseTime + row.Decay; } + this.columns = this.dimensions[0]; + this.rows = this.dimensions[1]; + [this.blendSrc, this.blendDst] = emitterFilterMode(row.BlendMode, viewer.gl); } else if (type === 'SND') { // Only load sounds if audio is enabled. diff --git a/src/viewer/handlers/mdx/modelparticleemitter2.js b/src/viewer/handlers/mdx/modelparticleemitter2.js index 19497320..d7f74186 100644 --- a/src/viewer/handlers/mdx/modelparticleemitter2.js +++ b/src/viewer/handlers/mdx/modelparticleemitter2.js @@ -29,6 +29,8 @@ export default class ParticleEmitter2 extends GenericObject { let replaceableId = emitter.replaceableId; this.dimensions = [emitter.columns, emitter.rows]; + this.columns = emitter.columns; + this.rows = emitter.rows; this.teamColored = false; @@ -44,6 +46,8 @@ export default class ParticleEmitter2 extends GenericObject { this.dimensions[0] = 14; this.dimensions[1] = 1; this.teamColored = true; + this.columns = 14; + this.rows = 1; } else { this.internalResource = model.viewer.load('ReplaceableTextures\\' + replaceableIds[replaceableId] + '.blp', model.pathSolver); } @@ -58,12 +62,14 @@ export default class ParticleEmitter2 extends GenericObject { this.cellWidth = 1 / emitter.columns; this.cellHeight = 1 / emitter.rows; this.colors = []; + this.floatColors = []; let colors = emitter.segmentColors; let alpha = emitter.segmentAlphas; for (let i = 0; i < 3; i++) { this.colors[i] = new Uint8Array([Math.min(colors[i][0], 1) * 255, Math.min(colors[i][1], 1) * 255, Math.min(colors[i][2], 1) * 255, alpha[i]]); + this.floatColors[i] = new Float32Array([colors[i][0], colors[i][1], colors[i][2], alpha[i] / 255]); } this.scaling = emitter.segmentScaling; diff --git a/src/viewer/handlers/mdx/modelviewdata.js b/src/viewer/handlers/mdx/modelviewdata.js index 04788f17..a8d214be 100644 --- a/src/viewer/handlers/mdx/modelviewdata.js +++ b/src/viewer/handlers/mdx/modelviewdata.js @@ -14,34 +14,23 @@ function getPrio(object) { if (object.layer) { return object.layer.priorityPlane; } else if (object.modelObject) { - return object.modelObject.priorityPlane; - } else { - console.log(object); - throw 'asdasdsadsadsadsadsaad'; + // Not all emitters have priority planes. + return object.modelObject.priorityPlane || 0; } } -function isNormalEmitter(object) { - return object instanceof ParticleEmitter2 || object instanceof EventObjectSpnEmitter || object instanceof EventObjectSplEmitter || object instanceof EventObjectUbrEmitter; -} - -function isRibbonEmitter(object) { - return object instanceof RibbonEmitter; -} - function matchingGroup(group, object) { - return (group instanceof BatchGroup && object instanceof Batch) || - (group instanceof EmitterGroup && group.isRibbons === false && isNormalEmitter(object)) || - (group instanceof EmitterGroup && group.isRibbons === true && isRibbonEmitter(object)); + let a = group instanceof BatchGroup; + let b = object instanceof Batch; + + return (a && b) || (!a && !b); } function createMatchingGroup(object, modelView) { if (object instanceof Batch) { return new BatchGroup(modelView); - } else if (isNormalEmitter(object)) { - return new EmitterGroup(modelView, false); - } else if (isRibbonEmitter(object)) { - return new EmitterGroup(modelView, true); + } else { + return new EmitterGroup(modelView); } } @@ -63,27 +52,29 @@ export default class MdxModelViewData extends ModelViewData { let eventObjectEmitters = []; for (let emitter of model.particleEmitters) { - particleEmitters.push(new ParticleEmitter(emitter)); + particleEmitters.push(new ParticleEmitter(this, emitter)); } for (let emitter of model.particleEmitters2) { - particleEmitters2.push(new ParticleEmitter2(emitter)); + particleEmitters2.push(new ParticleEmitter2(this, emitter)); } for (let emitter of model.ribbonEmitters) { - ribbonEmitters.push(new RibbonEmitter(emitter)); + ribbonEmitters.push(new RibbonEmitter(this, emitter)); } for (let emitter of model.eventObjects) { let type = emitter.type; if (type === 'SPN') { - eventObjectEmitters.push(new EventObjectSpnEmitter(emitter)); + eventObjectEmitters.push(new EventObjectSpnEmitter(this, emitter)); } else if (type === 'SPL') { - eventObjectEmitters.push(new EventObjectSplEmitter(emitter)); + eventObjectEmitters.push(new EventObjectSplEmitter(this, emitter)); } else if (type === 'UBR') { - eventObjectEmitters.push(new EventObjectUbrEmitter(emitter)); + eventObjectEmitters.push(new EventObjectUbrEmitter(this, emitter)); } else if (type === 'SND') { + // Sound objects aren't tracked in any way, they are fire-and-forget emitters. + // Therefore, they have no reason to store a reference back here. eventObjectEmitters.push(new EventObjectSndEmitter(emitter)); } } @@ -173,10 +164,7 @@ export default class MdxModelViewData extends ModelViewData { for (let emitter of this.eventObjectEmitters) { emitter.update(); - // Sounds are not particles. - if (emitter.type !== 'SND') { - this.particles += emitter.alive; - } + this.particles += emitter.alive; } } } diff --git a/src/viewer/handlers/mdx/particle.js b/src/viewer/handlers/mdx/particle.js index 419216a5..fff9d087 100644 --- a/src/viewer/handlers/mdx/particle.js +++ b/src/viewer/handlers/mdx/particle.js @@ -21,7 +21,6 @@ export default class Particle { constructor(emitter) { this.emitter = emitter; this.emitterView = null; - this.internalInstance = emitter.modelObject.internalResource.addInstance(); this.velocity = vec3.create(); this.gravity = 0; @@ -30,7 +29,7 @@ export default class Particle { /** * @param {ParticleEmitterView} emitterView */ - reset(emitterView) { + bind(emitterView) { let instance = emitterView.instance; let node = instance.nodes[this.emitter.modelObject.index]; let internalInstance = this.internalInstance; @@ -71,22 +70,23 @@ export default class Particle { } /** - * + * @param {number} offset + * @param {number} dt */ - update() { + render(offset, dt) { let internalInstance = this.internalInstance; - let velocity = this.velocity; - let frameTimeS = internalInstance.model.viewer.frameTime * 0.001; - internalInstance.paused = false; + internalInstance.paused = false; /// Why is this here? - this.health -= frameTimeS; + this.health -= dt; - velocity[2] -= this.gravity * frameTimeS; + if (this.health > 0) { + let velocity = this.velocity; - internalInstance.move(vec3.scale(velocityHeap, velocity, frameTimeS)); + velocity[2] -= this.gravity * dt; - if (this.health <= 0) { + internalInstance.move(vec3.scale(velocityHeap, velocity, dt)); + } else { this.internalInstance.hide(); } } diff --git a/src/viewer/handlers/mdx/particle2.js b/src/viewer/handlers/mdx/particle2.js index 5b0c8113..8c579196 100644 --- a/src/viewer/handlers/mdx/particle2.js +++ b/src/viewer/handlers/mdx/particle2.js @@ -1,49 +1,44 @@ -import {vec3, vec4, quat} from 'gl-matrix'; +import {vec3, quat} from 'gl-matrix'; import {VEC3_UNIT_Z} from '../../../common/gl-matrix-addon'; -import {degToRad, randomInRange, lerp} from '../../../common/math'; -import {uint8ToUint24} from '../../../common/typecast'; +import {degToRad, randomInRange} from '../../../common/math'; +import {BYTES_PER_OBJECT, FLOATS_PER_OBJECT, FLOAT_OFFSET_P0, FLOAT_OFFSET_HEALTH, BYTE_OFFSET_TAIL, BYTE_OFFSET_TEAM_COLOR, HEAD} from './geometryemitter'; // Heap allocations needed for this module. -let rotationHeap = quat.create(); -let locationHeap = vec3.create(); -let colorHeap = new Uint8Array(4); -let widthHeap = new Float32Array(1); -let lengthHeap = new Float32Array(1); -let latitudeHeap = new Float32Array(1); -let variationHeap = new Float32Array(1); -let speedHeap = new Float32Array(1); -let gravityHeap = new Float32Array(1); +const rotationHeap = quat.create(); +const widthHeap = new Float32Array(1); +const lengthHeap = new Float32Array(1); +const latitudeHeap = new Float32Array(1); +const variationHeap = new Float32Array(1); +const speedHeap = new Float32Array(1); +const gravityHeap = new Float32Array(1); +const locationHeap = vec3.create(); +const startHeap = vec3.create(); +const endHeap = vec3.create(); /** * A type 2 particle. */ export default class Particle2 { /** - * @param {MdxParticle2Emitter} emitter + * @param {GeomeryEmitter} emitter */ constructor(emitter) { this.emitter = emitter; this.emitterView = null; + this.node = null; + this.tail = false; this.health = 0; - this.head = true; + this.gravity = 0; this.location = vec3.create(); this.velocity = vec3.create(); - this.gravity = 0; - this.nodeScale = vec3.create(); - - this.vertices = new Float32Array(12); - this.lta = 0; - this.lba = 0; - this.rta = 0; - this.rba = 0; - this.rgb = 0; + this.scale = vec3.create(); } /** * @param {ParticleEmitter2View} emitterView - * @param {boolean} isHead + * @param {number} tail */ - reset(emitterView, isHead) { + bind(emitterView, tail) { emitterView.getWidth(widthHeap); emitterView.getLength(lengthHeap); emitterView.getLatitude(latitudeHeap); @@ -66,10 +61,10 @@ export default class Particle2 { this.emitterView = emitterView; this.node = node; this.health = modelObject.lifeSpan; - this.head = isHead; + this.tail = tail; this.gravity = gravityHeap[0] * scale[2]; - vec3.copy(this.nodeScale, scale); + vec3.copy(this.scale, scale); // Local location location[0] = pivot[0] + randomInRange(-width, width); @@ -103,203 +98,81 @@ export default class Particle2 { vec3.scale(velocity, velocity, speed + randomInRange(-variation, variation)); // Apply the parent's scale - vec3.mul(velocity, velocity, scale); + if (!modelObject.modelSpace) { + vec3.mul(velocity, velocity, scale); + } } /** - * + * @param {number} offset + * @param {number} dt */ - update() { - let modelObject = this.emitter.modelObject; - let dt = modelObject.model.viewer.frameTime * 0.001; - let location = this.location; - let worldLocation = locationHeap; - let velocity = this.velocity; - + render(offset, dt) { this.health -= dt; - velocity[2] -= this.gravity * dt; - - vec3.scaleAndAdd(location, location, velocity, dt); - - vec3.copy(worldLocation, location); - - let lifeFactor = (modelObject.lifeSpan - this.health) / modelObject.lifeSpan; - let timeMiddle = modelObject.timeMiddle; - let intervals = modelObject.intervals; - let factor; - let firstColor; - let head = this.head; - let interval; - - if (lifeFactor < timeMiddle) { - factor = lifeFactor / timeMiddle; - - firstColor = 0; - - if (head) { - interval = intervals[0]; - } else { - interval = intervals[2]; - } - } else { - factor = (lifeFactor - timeMiddle) / (1 - timeMiddle); - - firstColor = 1; - - if (head) { - interval = intervals[1]; + if (this.health > 0) { + let location = this.location; + let velocity = this.velocity; + let scale = this.scale; + let tail = this.tail; + let emitter = this.emitter; + let byteView = emitter.byteView; + let floatView = emitter.floatView; + let byteOffset = offset * BYTES_PER_OBJECT; + let floatOffset = offset * FLOATS_PER_OBJECT; + let p0Offset = floatOffset + FLOAT_OFFSET_P0; + let modelObject = emitter.modelObject; + + velocity[2] -= this.gravity * dt; + + location[0] += velocity[0] * dt; + location[1] += velocity[1] * dt; + location[2] += velocity[2] * dt; + + if (tail === HEAD) { + // If this is a model space emitter, the location is in local space, so convert it to world space. + if (modelObject.modelSpace) { + location = vec3.transformMat4(locationHeap, location, this.node.worldMatrix); + } + + floatView[p0Offset + 0] = location[0]; + floatView[p0Offset + 1] = location[1]; + floatView[p0Offset + 2] = location[2]; } else { - interval = intervals[3]; + let velocity = this.velocity; + let tailLength = modelObject.tailLength; + let offsetx = tailLength * velocity[0]; + let offsety = tailLength * velocity[1]; + let offsetz = tailLength * velocity[2]; + let start = startHeap; + let end = location; + + start[0] = end[0] - offsetx; + start[1] = end[1] - offsety; + start[2] = end[2] - offsetz; + + // If this is a model space emitter, the start and end are in local space, so convert them to world space. + if (modelObject.modelSpace) { + start = vec3.transformMat4(start, start, this.node.worldMatrix); + end = vec3.transformMat4(endHeap, end, this.node.worldMatrix); + } + + floatView[p0Offset + 0] = start[0]; + floatView[p0Offset + 1] = start[1]; + floatView[p0Offset + 2] = start[2]; + floatView[p0Offset + 3] = end[0]; + floatView[p0Offset + 4] = end[1]; + floatView[p0Offset + 5] = end[2]; } - } - - factor = Math.min(factor, 1); - - let start = interval[0]; - let end = interval[1]; - let repeat = interval[2]; - let scaling = modelObject.scaling; - let colors = modelObject.colors; - let scale = lerp(scaling[firstColor], scaling[firstColor + 1], factor); - let left; - let top; - let right; - let bottom; - let instance = this.emitterView.instance; - - // If this is a team colored emitter, get the team color tile from the atlas. - // Otherwise do normal texture atlas handling. - if (modelObject.teamColored) { - let teamColor = instance.teamColor; - - left = teamColor % 4; - top = (teamColor / 4) | 0; - right = left + 1; - bottom = top + 1; - } else { - let columns = modelObject.dimensions[0]; - let index = 0; - let spriteCount = end - start; - - if (spriteCount) { - let rows = modelObject.dimensions[1]; - - // Repeating speeds up the sprite animation, which makes it effectively run N times in its interval. - // E.g. if repeat is 4, the sprite animation will be seen 4 times, and thus also run 4 times as fast. - // The sprite index is limited to the number of actual sprites. - index = Math.min(start + Math.floor(spriteCount * repeat * factor) % spriteCount, columns * rows - 1); - } - - left = index % columns; - top = (index / columns) | 0; - right = left + 1; - bottom = top + 1; - } - - vec4.lerp(colorHeap, colors[firstColor], colors[firstColor + 1], factor); - - let a = colorHeap[3]; - - this.lta = uint8ToUint24(right, bottom, a); - this.lba = uint8ToUint24(left, bottom, a); - this.rta = uint8ToUint24(right, top, a); - this.rba = uint8ToUint24(left, top, a); - this.rgb = uint8ToUint24(colorHeap[0], colorHeap[1], colorHeap[2]); - - let camera = instance.scene.camera; - let vectors; - - // Choose between a default rectangle or billboarded one - if (modelObject.xYQuad) { - vectors = camera.vectors; - } else { - vectors = camera.billboardedVectors; - } - - let vertices = this.vertices; - let nodeScale = this.nodeScale; - - let scalex = scale * nodeScale[0]; - let scaley = scale * nodeScale[1]; - let scalez = scale * nodeScale[2]; - - if (head) { - // If this is a model space emitter, the particle location is in local space, so convert it now to world space. - if (modelObject.modelSpace) { - vec3.transformMat4(worldLocation, worldLocation, this.node.worldMatrix); - } - - let px = worldLocation[0]; - let py = worldLocation[1]; - let pz = worldLocation[2]; - - let pv1 = vectors[0]; - let pv2 = vectors[1]; - let pv3 = vectors[2]; - let pv4 = vectors[3]; - - vertices[0] = px + pv1[0] * scalex; - vertices[1] = py + pv1[1] * scaley; - vertices[2] = pz + pv1[2] * scalez; - vertices[3] = px + pv2[0] * scalex; - vertices[4] = py + pv2[1] * scaley; - vertices[5] = pz + pv2[2] * scalez; - vertices[6] = px + pv3[0] * scalex; - vertices[7] = py + pv3[1] * scaley; - vertices[8] = pz + pv3[2] * scalez; - vertices[9] = px + pv4[0] * scalex; - vertices[10] = py + pv4[1] * scaley; - vertices[11] = pz + pv4[2] * scalez; - } else { - let tailLength = modelObject.tailLength; - let offsetx = tailLength * velocity[0] * 1; - let offsety = tailLength * velocity[1] * 1; - let offsetz = tailLength * velocity[2] * 1; - - // The start and end of the tail. - let start = [worldLocation[0] - offsetx, worldLocation[1] - offsety, worldLocation[2] - offsetz]; - let end = [worldLocation[0], worldLocation[1], worldLocation[2]]; - - // If this is a model space emitter, the start and end are is in local space, so convert them to world space. - if (modelObject.modelSpace) { - vec3.transformMat4(start, start, this.node.worldMatrix); - vec3.transformMat4(end, end, this.node.worldMatrix); - } - - let startx = start[0]; - let starty = start[1]; - let startz = start[2]; - let endx = end[0]; - let endy = end[1]; - let endz = end[2]; - - // Get the normal to the tail in camera space. - // This allows to build a 2D rectangle around the 3D tail. - let tail = [endx - startx, endy - starty, endz - startz]; - vec3.normalize(tail, tail); - let normal = vec3.cross([], camera.billboardedVectors[6], tail); - vec3.normalize(normal, normal); - - let normalX = normal[0] * scalex; - let normalY = normal[1] * scaley; - let normalZ = normal[2] * scalez; - - vertices[0] = startx - normalX; - vertices[1] = starty - normalY; - vertices[2] = startz - normalZ; - vertices[6] = endx + normalX; - vertices[7] = endy + normalY; - vertices[8] = endz + normalZ; + floatView[p0Offset + 6] = scale[0]; + floatView[p0Offset + 7] = scale[0]; + floatView[p0Offset + 8] = scale[0]; - vertices[3] = endx - normalX; - vertices[4] = endy - normalY; - vertices[5] = endz - normalZ; + floatView[floatOffset + FLOAT_OFFSET_HEALTH] = this.health; - vertices[9] = startx + normalX; - vertices[10] = starty + normalY; - vertices[11] = startz + normalZ; + byteView[byteOffset + BYTE_OFFSET_TAIL] = tail; + byteView[byteOffset + BYTE_OFFSET_TEAM_COLOR] = this.emitterView.instance.teamColor; } } } diff --git a/src/viewer/handlers/mdx/particleemitter.js b/src/viewer/handlers/mdx/particleemitter.js index a88a7283..82fbb456 100644 --- a/src/viewer/handlers/mdx/particleemitter.js +++ b/src/viewer/handlers/mdx/particleemitter.js @@ -1,10 +1,10 @@ -import SharedEmitter from './sharedemitter'; +import Emitter from './emitter'; import Particle from './particle'; /** * An MDX particle emitter. */ -export default class ParticleEmitter extends SharedEmitter { +export default class ParticleEmitter extends Emitter { /** * @param {ParticleEmitterView} emitterView */ diff --git a/src/viewer/handlers/mdx/particleemitter2.js b/src/viewer/handlers/mdx/particleemitter2.js index d27d8128..db9d6fa4 100644 --- a/src/viewer/handlers/mdx/particleemitter2.js +++ b/src/viewer/handlers/mdx/particleemitter2.js @@ -1,29 +1,20 @@ -import SharedGeometryEmitter from './sharedgeometryemitter'; +import {GeometryEmitter, EMITTER_PARTICLE2} from './geometryemitter'; import Particle2 from './particle2'; /** * An MDX particle emitter type 2. */ -export default class ParticleEmitter2 extends SharedGeometryEmitter { - /** - * @param {ParticleEmitter2} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.elementsPerEmit = ((modelObject.headOrTail === 2) ? 2 : 1) * 30; - } - +export default class ParticleEmitter2 extends GeometryEmitter { /** * @param {ParticleEmitter2View} emitterView */ emit(emitterView) { if (this.modelObject.head) { - this.emitObject(emitterView, true); + this.emitObject(emitterView, 0); } if (this.modelObject.tail) { - this.emitObject(emitterView, false); + this.emitObject(emitterView, 1); } } @@ -33,4 +24,60 @@ export default class ParticleEmitter2 extends SharedGeometryEmitter { createObject() { return new Particle2(this); } + + /** + * @param {ModelView} modelView + * @param {ShaderProgram} shader + */ + bind(modelView, shader) { + let camera = this.modelViewData.scene.camera; + let modelObject = this.modelObject; + let model = modelObject.model; + let gl = model.viewer.gl; + let uniforms = shader.uniforms; + let colors = modelObject.floatColors; + let intervals = modelObject.intervals; + let vectors; + + gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); + + model.bindTexture(modelObject.internalResource, 0, modelView); + + // Choose between a default rectangle or a billboarded one + if (modelObject.xYQuad) { + vectors = camera.vectors; + } else { + vectors = camera.billboardedVectors; + } + + gl.uniform1f(uniforms.u_emitter, EMITTER_PARTICLE2); + + gl.uniform1f(uniforms.u_lifeSpan, modelObject.lifeSpan); + gl.uniform1f(uniforms.u_timeMiddle, modelObject.timeMiddle); + gl.uniform1f(uniforms.u_columns, modelObject.columns); + gl.uniform1f(uniforms.u_rows, modelObject.rows); + gl.uniform1f(uniforms.u_teamColored, modelObject.teamColored); + + gl.uniform3fv(uniforms['u_intervals[0]'], intervals[0]); + gl.uniform3fv(uniforms['u_intervals[1]'], intervals[1]); + gl.uniform3fv(uniforms['u_intervals[2]'], intervals[2]); + gl.uniform3fv(uniforms['u_intervals[3]'], intervals[3]); + + gl.uniform4fv(uniforms['u_colors[0]'], colors[0]); + gl.uniform4fv(uniforms['u_colors[1]'], colors[1]); + gl.uniform4fv(uniforms['u_colors[2]'], colors[2]); + + gl.uniform3fv(uniforms.u_scaling, modelObject.scaling); + + if (modelObject.head) { + gl.uniform3fv(uniforms['u_vertices[0]'], vectors[0]); + gl.uniform3fv(uniforms['u_vertices[1]'], vectors[1]); + gl.uniform3fv(uniforms['u_vertices[2]'], vectors[2]); + gl.uniform3fv(uniforms['u_vertices[3]'], vectors[3]); + } + + if (modelObject.tail) { + gl.uniform3fv(uniforms.u_cameraZ, camera.billboardedVectors[6]); + } + } } diff --git a/src/viewer/handlers/mdx/ribbon.js b/src/viewer/handlers/mdx/ribbon.js index e422d489..e9f47c90 100644 --- a/src/viewer/handlers/mdx/ribbon.js +++ b/src/viewer/handlers/mdx/ribbon.js @@ -1,5 +1,5 @@ import {vec3} from 'gl-matrix'; -import {uint8ToUint24} from '../../../common/typecast'; +import {BYTES_PER_OBJECT, FLOATS_PER_OBJECT, FLOAT_OFFSET_P0, BYTE_OFFSET_COLOR, BYTE_OFFSET_LEFT_RIGHT_TOP} from './geometryemitter'; // Heap allocations needed for this module. let belowHeap = vec3.create(); @@ -17,30 +17,22 @@ export default class Ribbon { */ constructor(emitter) { this.emitter = emitter; - this.health = 0; this.emitterView = null; - + this.index = 0; + this.health = 0; this.vertices = new Float32Array(12); - this.lta = 0; - this.lba = 0; - this.rta = 0; - this.rba = 0; - this.rgb = 0; } /** * @param {RibbonEmitterView} emitterView */ - reset(emitterView) { + bind(emitterView) { let emitter = this.emitter; - let vertices = this.vertices; - - this.index = emitterView.currentRibbon++; emitterView.ribbonCount++; this.emitterView = emitterView; - + this.index = emitterView.currentRibbon++; this.health = emitter.modelObject.lifeSpan; let lastEmit = emitterView.lastEmit; @@ -50,17 +42,27 @@ export default class Ribbon { // This allows the emitter to always work with quads, and therefore it can work with many views, because the ribbon chains are implicit. if (lastEmit && lastEmit.health > 0) { let node = emitterView.instance.nodes[emitter.modelObject.index]; - let pivot = node.pivot; + let [x, y, z] = node.pivot; + let worldMatrix = node.worldMatrix; emitterView.getHeightBelow(belowHeap); emitterView.getHeightAbove(aboveHeap); - vec3.set(belowHeap, pivot[0], pivot[1] - belowHeap[0], pivot[2]); - vec3.transformMat4(belowHeap, belowHeap, node.worldMatrix); + let heightBelow = belowHeap[0]; + let heightAbove = aboveHeap[0]; + + belowHeap[0] = x; + belowHeap[1] = y - heightBelow; + belowHeap[2] = z; + + aboveHeap[0] = x; + aboveHeap[1] = y + heightAbove; + aboveHeap[2] = z; - vec3.set(aboveHeap, pivot[0], pivot[1] + aboveHeap[0], pivot[2]); - vec3.transformMat4(aboveHeap, aboveHeap, node.worldMatrix); + vec3.transformMat4(belowHeap, belowHeap, worldMatrix); + vec3.transformMat4(aboveHeap, aboveHeap, worldMatrix); + let vertices = this.vertices; let lastVertices = lastEmit.vertices; // Left top @@ -82,68 +84,71 @@ export default class Ribbon { vertices[9] = lastVertices[0]; vertices[10] = lastVertices[1]; vertices[11] = lastVertices[2]; - } else { - vertices[0] = 0; - vertices[1] = 0; - vertices[2] = 0; - vertices[3] = 0; - vertices[4] = 0; - vertices[5] = 0; - vertices[6] = 0; - vertices[7] = 0; - vertices[8] = 0; - vertices[9] = 0; - vertices[10] = 0; - vertices[11] = 0; } } /** - * + * @param {number} offset + * @param {number} dt */ - update() { + render(offset, dt) { let emitterView = this.emitterView; - emitterView.getColor(colorHeap); - emitterView.getAlpha(alphaHeap); - emitterView.getTextureSlot(slotHeap); - - let modelObject = this.emitter.modelObject; - let dt = modelObject.model.viewer.frameTime * 0.001; - let gravity = modelObject.gravity * dt * dt; - let vertices = this.vertices; - let animatedAlpha = alphaHeap[0]; - let animatedSlot = slotHeap[0]; - let chainLengthFactor = 1 / emitterView.ribbonCount; - let locationInChain = (emitterView.currentRibbon - this.index - 1); - this.health -= dt; - vertices[1] -= gravity; - vertices[4] -= gravity; - vertices[7] -= gravity; - vertices[10] -= gravity; - - if (this.health <= 0) { - emitterView.ribbonCount--; - } else { + if (this.health > 0) { + let emitter = this.emitter; + let modelObject = emitter.modelObject; + let byteView = emitter.byteView; + let floatView = emitter.floatView; + let byteOffset = offset * BYTES_PER_OBJECT; + let floatOffset = offset * FLOATS_PER_OBJECT; + let p0Offset = floatOffset + FLOAT_OFFSET_P0; + let colorOffset = byteOffset + BYTE_OFFSET_COLOR; + let leftRightTopOffset = byteOffset + BYTE_OFFSET_LEFT_RIGHT_TOP; + + emitterView.getColor(colorHeap); + emitterView.getAlpha(alphaHeap); + emitterView.getTextureSlot(slotHeap); + + let animatedSlot = slotHeap[0]; + let chainLengthFactor = 1 / emitterView.ribbonCount; + let locationInChain = (emitterView.currentRibbon - this.index - 1); let columns = modelObject.dimensions[0]; let left = (animatedSlot % columns) + (locationInChain * chainLengthFactor); let top = (animatedSlot / columns) | 0; let right = left + chainLengthFactor; - let bottom = top + 1; - - left = (left * 255) | 0; - top = (top * 255) | 0; - right = (right * 255) | 0; - bottom = (bottom * 255); - animatedAlpha = (animatedAlpha * 255) | 0; - - this.lta = uint8ToUint24(left, top, animatedAlpha); - this.lba = uint8ToUint24(left, bottom, animatedAlpha); - this.rta = uint8ToUint24(right, top, animatedAlpha); - this.rba = uint8ToUint24(right, bottom, animatedAlpha); - this.rgb = uint8ToUint24((colorHeap[0] * 255) | 0, (colorHeap[1] * 255) | 0, (colorHeap[2] * 255) | 0); // Color even used??? + let vertices = this.vertices; + let gravity = modelObject.gravity * dt * dt; + + vertices[1] -= gravity; + vertices[4] -= gravity; + vertices[7] -= gravity; + vertices[10] -= gravity; + + floatView[p0Offset + 0] = vertices[0]; + floatView[p0Offset + 1] = vertices[1]; + floatView[p0Offset + 2] = vertices[2]; + floatView[p0Offset + 3] = vertices[3]; + floatView[p0Offset + 4] = vertices[4]; + floatView[p0Offset + 5] = vertices[5]; + floatView[p0Offset + 6] = vertices[6]; + floatView[p0Offset + 7] = vertices[7]; + floatView[p0Offset + 8] = vertices[8]; + floatView[p0Offset + 9] = vertices[9]; + floatView[p0Offset + 10] = vertices[10]; + floatView[p0Offset + 11] = vertices[11]; + + byteView[colorOffset + 0] = colorHeap[0] * 255; + byteView[colorOffset + 1] = colorHeap[1] * 255; + byteView[colorOffset + 2] = colorHeap[2] * 255; + byteView[colorOffset + 3] = alphaHeap[0] * 255; + + byteView[leftRightTopOffset + 0] = left * 255; + byteView[leftRightTopOffset + 1] = right * 255; + byteView[leftRightTopOffset + 2] = top * 255; + } else { + emitterView.ribbonCount--; } } } diff --git a/src/viewer/handlers/mdx/ribbonemitter.js b/src/viewer/handlers/mdx/ribbonemitter.js index 9abb92a9..f4fe48ab 100644 --- a/src/viewer/handlers/mdx/ribbonemitter.js +++ b/src/viewer/handlers/mdx/ribbonemitter.js @@ -1,19 +1,10 @@ -import SharedGeometryEmitter from './sharedgeometryemitter'; +import {GeometryEmitter, EMITTER_RIBBON} from './geometryemitter'; import Ribbon from './ribbon'; /** * A ribbon emitter. */ -export default class RibbonEmitter extends SharedGeometryEmitter { - /** - * @param {RibbonEmitter} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.elementsPerEmit = 30; - } - +export default class RibbonEmitter extends GeometryEmitter { /** * @param {RibbonEmitterView} emitterView */ @@ -44,27 +35,21 @@ export default class RibbonEmitter extends SharedGeometryEmitter { * @param {ModelView} modelView * @param {ShaderProgram} shader */ - render(modelView, shader) { - let alive = this.alive; + bind(modelView, shader) { + let modelObject = this.modelObject; + let layer = modelObject.layer; + let uvDivisor = layer.uvDivisor; + let model = modelObject.model; + let gl = model.viewer.gl; + let uniforms = shader.uniforms; - if (alive > 0) { - let modelObject = this.modelObject; - let model = modelObject.model; - let gl = model.viewer.gl; + layer.bind(shader); - modelObject.layer.bind(shader); + model.bindTexture(modelObject.texture, 0, modelView); - gl.uniform2fv(shader.uniforms.u_dimensions, modelObject.dimensions); + gl.uniform1f(uniforms.u_emitter, EMITTER_RIBBON); - model.bindTexture(modelObject.texture, 0, modelView); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.data.subarray(0, alive * 30)); - - gl.vertexAttribPointer(shader.attribs.a_position, 3, gl.FLOAT, false, 20, 0); - gl.vertexAttribPointer(shader.attribs.a_uva_rgb, 2, gl.FLOAT, false, 20, 12); - - gl.drawArrays(gl.TRIANGLES, 0, alive * 6); - } + gl.uniform1f(uniforms.u_columns, uvDivisor[0]); + gl.uniform1f(uniforms.u_rows, uvDivisor[1]); } } diff --git a/src/viewer/handlers/mdx/shaders.js b/src/viewer/handlers/mdx/shaders.js index 0c33966b..0bf8fee6 100644 --- a/src/viewer/handlers/mdx/shaders.js +++ b/src/viewer/handlers/mdx/shaders.js @@ -4,6 +4,7 @@ export default { vs: ` ${shaders.instanceId} ${shaders.boneTexture} + uniform mat4 u_mvp; attribute vec3 a_position; @@ -32,8 +33,8 @@ export default { mat4 b1 = fetchMatrix(bones[1], a_InstanceID); mat4 b2 = fetchMatrix(bones[2], a_InstanceID); mat4 b3 = fetchMatrix(bones[3], a_InstanceID); - vec4 p = vec4(position, 1); - vec4 n = vec4(normal, 0); + vec4 p = vec4(position, 1.0); + vec4 n = vec4(normal, 0.0); position = vec3(b0 * p + b1 * p + b2 * p + b3 * p) / boneNumber; normal = normalize(vec3(b0 * n + b1 * n + b2 * n + b3 * n)); @@ -56,13 +57,15 @@ export default { v_color.a *= a_layerAlpha; if (a_geosetColor.a > 0.0 && a_layerAlpha > 0.0) { - gl_Position = u_mvp * vec4(position, 1); + gl_Position = u_mvp * vec4(position, 1.0); } else { gl_Position = vec4(0.0); } } `, fs: ` + ${shaders.quat_transform} + uniform sampler2D u_texture; uniform float u_filterMode; // uniform bool u_unshaded; @@ -82,8 +85,6 @@ export default { // const vec3 lightDirection = normalize(vec3(-0.3, -0.3, 0.25)); - ${shaders.quat_transform} - void main() { vec2 uv; @@ -134,49 +135,280 @@ export default { } `, vsParticles: ` - ${shaders.decodeFloat} - uniform mat4 u_mvp; - uniform vec2 u_dimensions; - uniform bool u_isRibbonEmitter; + #define EMITTER_PARTICLE2 0.0 + #define EMITTER_RIBBON 1.0 + #define EMITTER_SPLAT 2.0 + #define EMITTER_UBER 3.0 + #define HEAD 0.0 - attribute vec3 a_position; - attribute vec2 a_uva_rgb; + uniform mat4 u_mvp; - varying vec2 v_uv; + uniform mediump float u_emitter; + + // Shared + uniform vec4 u_colors[3]; + uniform vec3 u_vertices[4]; + uniform vec3 u_intervals[4]; + uniform float u_lifeSpan; + uniform float u_columns; + uniform float u_rows; + + // Particle2 + uniform vec3 u_scaling; + uniform vec3 u_cameraZ; + uniform float u_timeMiddle; + uniform bool u_teamColored; + + // Splat and Uber. + uniform vec3 u_intervalTimes; + + // Vertices + attribute float a_position; + + // Instances + attribute vec3 a_p0; + attribute vec3 a_p1; + attribute vec3 a_p2; + attribute vec3 a_p3; + attribute float a_health; + attribute vec4 a_color; + attribute float a_tail; + attribute vec3 a_leftRightTop; + + varying vec2 v_texcoord; varying vec4 v_color; - void main() { - vec3 uva = decodeFloat3(a_uva_rgb[0]); - vec3 rgb = decodeFloat3(a_uva_rgb[1]); - vec2 uv = uva.xy; + float getCell(vec3 interval, float factor) { + float start = interval[0]; + float end = interval[1]; + float repeat = interval[2]; + float spriteCount = end - start; + + if (spriteCount > 0.0) { + // Repeating speeds up the sprite animation, which makes it effectively run N times in its interval. + // E.g. if repeat is 4, the sprite animation will be seen 4 times, and thus also run 4 times as fast. + // The sprite index is limited to the number of actual sprites. + return min(start + mod(floor(spriteCount * repeat * factor), spriteCount), u_columns * u_rows - 1.0); + } + + return 0.0; + } - if (u_isRibbonEmitter) { - uv /= 255.0; + void particle2() { + float factor = (u_lifeSpan - a_health) / u_lifeSpan; + int index = 0; + + if (factor < u_timeMiddle) { + factor = factor / u_timeMiddle; + index = 0; + } else { + factor = (factor - u_timeMiddle) / (1.0 - u_timeMiddle); + index = 1; } + + factor = min(factor, 1.0); + + float scale = mix(u_scaling[index], u_scaling[index + 1], factor); + vec4 color = mix(u_colors[index], u_colors[index + 1], factor); + + float cell = 0.0; + + if (u_teamColored) { + cell = a_leftRightTop[0]; + } else { + vec3 interval; + + if (a_tail == HEAD) { + interval = u_intervals[index]; + } else { + interval = u_intervals[index + 2]; + } + + cell = getCell(interval, factor); + } + + float left = floor(mod(cell, u_columns)); + float top = floor(cell / u_columns); + float right = left + 1.0; + float bottom = top + 1.0; + + left /= u_columns; + right /= u_columns; + top /= u_rows; + bottom /= u_rows; + + if (a_position == 0.0) { + v_texcoord = vec2(right, top); + } else if (a_position == 1.0) { + v_texcoord = vec2(left, top); + } else if (a_position == 2.0) { + v_texcoord = vec2(left, bottom); + } else if (a_position == 3.0) { + v_texcoord = vec2(right, bottom); + } + + v_color = color; - v_uv = uv / u_dimensions; - v_color = vec4(rgb, uva.z) / 255.0; + if (a_tail == HEAD) { + gl_Position = u_mvp * vec4(a_p0 + (u_vertices[int(a_position)] * scale), 1.0); + } else { + // Get the normal to the tail in camera space. + // This allows to build a 2D rectangle around the 3D tail. + vec3 normal = cross(u_cameraZ, normalize(a_p1 - a_p0)); + vec3 boundary = normal * scale * a_p2[0]; + vec3 position; + + if (a_position == 0.0) { + position = a_p0 - boundary; + } else if (a_position == 1.0) { + position = a_p1 - boundary; + } else if (a_position == 2.0) { + position = a_p1 + boundary; + } else if (a_position == 3.0) { + position = a_p0 + boundary; + } + + gl_Position = u_mvp * vec4(position, 1.0); + } + } + + void ribbon() { + vec3 position; + float left = a_leftRightTop[0] / 255.0; + float right = a_leftRightTop[1] / 255.0; + float top = a_leftRightTop[2] / 255.0; + float bottom = top + 1.0; + + if (a_position == 0.0) { + v_texcoord = vec2(left, top); + position = a_p0; + } else if (a_position == 1.0) { + v_texcoord = vec2(left, bottom); + position = a_p1; + } else if (a_position == 2.0) { + v_texcoord = vec2(right, bottom); + position = a_p2; + } else if (a_position == 3.0) { + v_texcoord = vec2(right, top); + position = a_p3; + } + + v_texcoord[0] /= u_columns; + v_texcoord[1] /= u_rows; + + v_color = a_color; - gl_Position = u_mvp * vec4(a_position, 1); + gl_Position = u_mvp * vec4(position, 1.0); + } + + void splat() { + float factor = u_lifeSpan - a_health; + int index; + + if (factor < u_intervalTimes[0]) { + factor = factor / u_intervalTimes[0]; + index = 0; + } else { + factor = (factor - u_intervalTimes[0]) / u_intervalTimes[1]; + index = 1; + } + + float cell = getCell(u_intervals[index], factor); + float left = floor(mod(cell, u_columns)); + float top = floor(cell / u_columns); + float right = left + 1.0; + float bottom = top + 1.0; + vec3 position; + + if (a_position == 0.0) { + v_texcoord = vec2(left, top); + position = a_p0; + } else if (a_position == 1.0) { + v_texcoord = vec2(left, bottom); + position = a_p1; + } else if (a_position == 2.0) { + v_texcoord = vec2(right, bottom); + position = a_p2; + } else if (a_position == 3.0) { + v_texcoord = vec2(right, top); + position = a_p3; + } + + v_texcoord[0] /= u_columns; + v_texcoord[1] /= u_rows; + + v_color = mix(u_colors[index], u_colors[index + 1], factor) / 255.0; + + gl_Position = u_mvp * vec4(position, 1.0); + } + + void uber() { + float factor = u_lifeSpan - a_health; + vec4 color; + + if (factor < u_intervalTimes[0]) { + color = mix(u_colors[0], u_colors[1], factor / u_intervalTimes[0]); + } else if (factor < u_intervalTimes[0] + u_intervalTimes[1]) { + color = u_colors[1]; + } else { + color = mix(u_colors[1], u_colors[2], (factor - u_intervalTimes[0] - u_intervalTimes[1]) / u_intervalTimes[2]); + } + + vec3 position; + + if (a_position == 0.0) { + v_texcoord = vec2(0.0, 0.0); + position = a_p0; + } else if (a_position == 1.0) { + v_texcoord = vec2(0.0, 1.0); + position = a_p1; + } else if (a_position == 2.0) { + v_texcoord = vec2(1.0, 1.0); + position = a_p2; + } else if (a_position == 3.0) { + v_texcoord = vec2(1.0, 0.0); + position = a_p3; + } + + v_color = color / 255.0; + + gl_Position = u_mvp * vec4(position, 1.0); + } + + void main() { + if (u_emitter == EMITTER_PARTICLE2) { + particle2(); + } else if (u_emitter == EMITTER_RIBBON) { + ribbon(); + } else if (u_emitter == EMITTER_SPLAT) { + splat(); + } else if (u_emitter == EMITTER_UBER) { + uber(); + } } `, fsParticles: ` + #define EMITTER_RIBBON 1.0 + uniform sampler2D u_texture; - uniform bool u_alphaTest; - uniform bool u_isRibbonEmitter; - varying vec2 v_uv; + uniform mediump float u_emitter; + + uniform float u_filterMode; + + varying vec2 v_texcoord; varying vec4 v_color; void main() { - vec4 texel = texture2D(u_texture, v_uv); + vec4 texel = texture2D(u_texture, v_texcoord); + vec4 color = texel * v_color; - // 1bit Alpha, used by ribbon emitters - if (u_isRibbonEmitter && u_alphaTest && texel.a < 0.75) { + // 1bit Alpha, used by ribbon emitters. + if (u_emitter == EMITTER_RIBBON && u_filterMode == 1.0 && color.a < 0.75) { discard; } - gl_FragColor = texel * v_color; + gl_FragColor = color; } `, }; diff --git a/src/viewer/handlers/mdx/sharedgeometryemitter.js b/src/viewer/handlers/mdx/sharedgeometryemitter.js deleted file mode 100644 index 9236f94f..00000000 --- a/src/viewer/handlers/mdx/sharedgeometryemitter.js +++ /dev/null @@ -1,112 +0,0 @@ -import {powerOfTwo} from '../../../common/math'; -import SharedEmitter from './sharedemitter'; - -/** - * Shared structure used by all geometry emitters. - */ -export default class SharedGeometryEmitter extends SharedEmitter { - /** - * @param {ParticleEmitter2|RibbonEmitter|EventObjectSplEmitter|EventObjectUbrEmitter} modelObject - */ - constructor(modelObject) { - super(modelObject); - - this.data = new Float32Array(0); - this.buffer = modelObject.model.viewer.gl.createBuffer(); - this.bufferSize = 0; - } - - /** - * - */ - updateData() { - let objects = this.objects; - let alive = this.alive; - let sizeNeeded = alive * this.elementsPerEmit; - - if (this.data.length < sizeNeeded) { - this.data = new Float32Array(powerOfTwo(sizeNeeded)); - - let gl = this.modelObject.model.viewer.gl; - - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); - gl.bufferData(gl.ARRAY_BUFFER, this.data.byteLength, gl.DYNAMIC_DRAW); - } - - let data = this.data; - - for (let i = 0, offset = 0; i < alive; i += 1, offset += 30) { - let object = objects[i]; - let vertices = object.vertices; - let lta = object.lta; - let lba = object.lba; - let rta = object.rta; - let rba = object.rba; - let rgb = object.rgb; - - data[offset + 0] = vertices[0]; - data[offset + 1] = vertices[1]; - data[offset + 2] = vertices[2]; - data[offset + 3] = lta; - data[offset + 4] = rgb; - - data[offset + 5] = vertices[3]; - data[offset + 6] = vertices[4]; - data[offset + 7] = vertices[5]; - data[offset + 8] = lba; - data[offset + 9] = rgb; - - data[offset + 10] = vertices[6]; - data[offset + 11] = vertices[7]; - data[offset + 12] = vertices[8]; - data[offset + 13] = rba; - data[offset + 14] = rgb; - - data[offset + 15] = vertices[0]; - data[offset + 16] = vertices[1]; - data[offset + 17] = vertices[2]; - data[offset + 18] = lta; - data[offset + 19] = rgb; - - data[offset + 20] = vertices[6]; - data[offset + 21] = vertices[7]; - data[offset + 22] = vertices[8]; - data[offset + 23] = rba; - data[offset + 24] = rgb; - - data[offset + 25] = vertices[9]; - data[offset + 26] = vertices[10]; - data[offset + 27] = vertices[11]; - data[offset + 28] = rta; - data[offset + 29] = rgb; - } - } - - /** - * @param {ModelView} modelView - * @param {ShaderProgram} shader - */ - render(modelView, shader) { - let modelObject = this.modelObject; - let alive = this.alive; - - if (modelObject.internalResource && alive > 0) { - let model = modelObject.model; - let gl = model.viewer.gl; - - gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); - - gl.uniform2fv(shader.uniforms.u_dimensions, modelObject.dimensions); - - model.bindTexture(modelObject.internalResource, 0, modelView); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); - gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.data.subarray(0, alive * 30)); - - gl.vertexAttribPointer(shader.attribs.a_position, 3, gl.FLOAT, false, 20, 0); - gl.vertexAttribPointer(shader.attribs.a_uva_rgb, 2, gl.FLOAT, false, 20, 12); - - gl.drawArrays(gl.TRIANGLES, 0, alive * 6); - } - } -} diff --git a/src/viewer/viewer.js b/src/viewer/viewer.js index 9fbea4d5..73fe10f6 100644 --- a/src/viewer/viewer.js +++ b/src/viewer/viewer.js @@ -68,6 +68,8 @@ export default class ModelViewer extends EventEmitter { /** @member {number} */ this.frame = 0; + let gl = this.gl; + /** * The instances buffer is used instead of gl_InstanceID, which isn't defined in WebGL shaders. * It's a simple buffer of indices, [0, 1, ..., instancesCount - 1]. @@ -75,10 +77,19 @@ export default class ModelViewer extends EventEmitter { * * @member {WebGLBuffer} */ - this.instancesBuffer = this.gl.createBuffer(); + this.instancesBuffer = gl.createBuffer(); /** @member {number} */ this.instancesCount = 0; + /** + * A simple buffer containing the bytes [0, 1, 2, 0, 2, 3]. + * These are used as vertices in all geometry shaders. + */ + this.rectBuffer = gl.createBuffer(); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.rectBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); + /** * A viewer-wide flag. * If it is false, not only will audio not run, but in fact audio files won't even be fetched in the first place.