diff --git a/package.json b/package.json index 06bf68d0..e9685d1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mdx-m3-viewer", - "version": "4.8.0", + "version": "4.8.1", "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/common/gl-matrix-addon.js b/src/common/gl-matrix-addon.js index c2fb5a1c..8632b47e 100644 --- a/src/common/gl-matrix-addon.js +++ b/src/common/gl-matrix-addon.js @@ -54,6 +54,20 @@ function distanceToPlane2(plane, px, py) { return plane[0] * px + plane[1] * py + plane[3]; } +/** + * Get the distance of a point from a plane. + * dot(plane, vec4(point, 1)) + * + * @param {vec4} plane + * @param {number} px + * @param {number} py + * @param {number} pz + * @return {number} + */ +function distanceToPlane3(plane, px, py, pz) { + return plane[0] * px + plane[1] * py + plane[2] * pz + plane[3]; +} + /** * Normalize a plane. Note that this is not the same as normalizing a vec4. * @@ -176,6 +190,7 @@ export { unproject, distanceToPlane, distanceToPlane2, + distanceToPlane3, normalizePlane, unpackPlanes, getRotationX, diff --git a/src/index.js b/src/index.js index 8d1e1448..c0c34e91 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.8.0', + version: '4.8.1', common, parsers, viewer, diff --git a/src/viewer/camera.js b/src/viewer/camera.js index 9448a408..2042ddbe 100644 --- a/src/viewer/camera.js +++ b/src/viewer/camera.js @@ -1,5 +1,5 @@ import {vec3, vec4, quat, mat4} from 'gl-matrix'; -import {unproject, distanceToPlane, distanceToPlane2, unpackPlanes, VEC3_UNIT_Y, VEC3_UNIT_X, VEC3_UNIT_Z} from '../common/gl-matrix-addon'; +import {unproject, distanceToPlane2, distanceToPlane3, unpackPlanes, VEC3_UNIT_Y, VEC3_UNIT_X, VEC3_UNIT_Z} from '../common/gl-matrix-addon'; let vectorHeap = vec3.create(); let vectorHeap2 = vec3.create(); @@ -317,13 +317,15 @@ export default class Camera { /** * Test it a sphere with the given center and radius intersects this frustum. * - * @param {vec3} center - * @param {number} radius + * @param {number} x + * @param {number} y + * @param {number} z + * @param {number} r * @return {boolean} */ - testSphere(center, radius) { + testSphere(x, y, z, r) { for (let plane of this.planes) { - if (distanceToPlane(plane, center) <= -radius) { + if (distanceToPlane3(plane, x, y, z) <= -r) { return false; } } @@ -331,6 +333,17 @@ export default class Camera { return true; } + /** + * @param {ModelInstance} instance + * @return {boolean} + */ + testInstance(instance) { + let location = instance.worldLocation; + let bounds = instance.model.bounds; + + return this.testSphere(location[0] + bounds.x, location[1] + bounds.y, location[2], bounds.r); + } + /** * @param {Cell} cell * @return {boolean} diff --git a/src/viewer/handlers/geo/model.js b/src/viewer/handlers/geo/model.js index 32d1d7eb..50a3feb9 100644 --- a/src/viewer/handlers/geo/model.js +++ b/src/viewer/handlers/geo/model.js @@ -1,11 +1,11 @@ -import TexturedModel from '../../texturedmodel'; +import Model from '../../model'; /** * A geometry model. * * Used to render simple geometric shapes. */ -export default class GeometryModel extends TexturedModel { +export default class GeometryModel extends Model { /** * Load the model. * diff --git a/src/viewer/handlers/m3/layer.js b/src/viewer/handlers/m3/layer.js index f3a9da3f..92c1bbbd 100644 --- a/src/viewer/handlers/m3/layer.js +++ b/src/viewer/handlers/m3/layer.js @@ -110,7 +110,7 @@ export default class M3Layer { if (active) { gl.uniform1i(uniforms[uniformMap.map], this.textureUnit); - this.model.bindTexture(this.texture, this.textureUnit, bucket.modelView); + bucket.modelView.bindTexture(this.texture, this.textureUnit); gl.uniform1f(uniforms[uniformMap.op], this.op); gl.uniform1f(uniforms[uniformMap.channels], this.colorChannels); diff --git a/src/viewer/handlers/m3/model.js b/src/viewer/handlers/m3/model.js index 01610b98..69828860 100644 --- a/src/viewer/handlers/m3/model.js +++ b/src/viewer/handlers/m3/model.js @@ -1,5 +1,5 @@ import Parser from '../../../parsers/m3/model'; -import TexturedModel from '../../texturedmodel'; +import Model from '../../model'; import M3StandardMaterial from './standardmaterial'; import M3Bone from './bone'; import M3Sequence from './sequence'; @@ -13,7 +13,7 @@ import M3Region from './region'; /** * An M3 model. */ -export default class M3Model extends TexturedModel { +export default class M3Model extends Model { /** * @param {Object} resourceData */ diff --git a/src/viewer/handlers/mdx/eventobjectemitterview.js b/src/viewer/handlers/mdx/eventobjectemitterview.js index 0a24f8f9..526e7a62 100644 --- a/src/viewer/handlers/mdx/eventobjectemitterview.js +++ b/src/viewer/handlers/mdx/eventobjectemitterview.js @@ -1,7 +1,5 @@ -import {vec2} from 'gl-matrix'; - // Heap allocations needed for this module. -let track = vec2.create(); +let valueHeap = new Uint32Array(1); /** * An event object emitter view. @@ -14,7 +12,7 @@ export default class EventObjectEmitterView { constructor(instance, emitter) { this.instance = instance; this.emitter = emitter; - this.lastTrack = new Uint16Array(2); // Support more than 256 keyframes per sequence, why not. + this.lastValue = 0; this.currentEmission = 0; } @@ -22,7 +20,7 @@ export default class EventObjectEmitterView { * */ reset() { - this.lastTrack.fill(0); + this.lastValue = 0; } /** @@ -30,17 +28,15 @@ export default class EventObjectEmitterView { */ update() { if (this.instance.allowParticleSpawn) { - let emitter = this.emitter; - let lastTrack = this.lastTrack; + this.emitter.getValue(valueHeap, this.instance); - emitter.getValue(track, this.instance); + let value = valueHeap[0]; - if (track[0] === 1 && (track[0] !== lastTrack[0] || track[1] !== lastTrack[1])) { + if (value === 1 && value !== this.lastValue) { this.currentEmission += 1; } - lastTrack[0] = track[0]; - lastTrack[1] = track[1]; + this.lastValue = value; } } } diff --git a/src/viewer/handlers/mdx/eventobjectsplemitter.js b/src/viewer/handlers/mdx/eventobjectsplemitter.js index 7973dea3..8c3a628e 100644 --- a/src/viewer/handlers/mdx/eventobjectsplemitter.js +++ b/src/viewer/handlers/mdx/eventobjectsplemitter.js @@ -36,7 +36,7 @@ export default class EventObjectSplEmitter extends GeometryEmitter { gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); - model.bindTexture(modelObject.internalResource, 0, modelView); + modelView.bindTexture(modelObject.internalResource, 0); gl.uniform1f(uniforms.u_emitter, EMITTER_SPLAT); diff --git a/src/viewer/handlers/mdx/eventobjectsplubr.js b/src/viewer/handlers/mdx/eventobjectsplubr.js index 9b865a19..dc38d09a 100644 --- a/src/viewer/handlers/mdx/eventobjectsplubr.js +++ b/src/viewer/handlers/mdx/eventobjectsplubr.js @@ -8,7 +8,7 @@ const vertexHeap = vec3.create(); */ export default class EventObjectSplUbr { /** - * @param {MdxEventObjectEmitter} emitter + * @param {EventObjectSplEmitter|EventObjectUbrEmitter} emitter */ constructor(emitter) { this.emitter = emitter; diff --git a/src/viewer/handlers/mdx/eventobjectubremitter.js b/src/viewer/handlers/mdx/eventobjectubremitter.js index da8eb13c..190a6ae0 100644 --- a/src/viewer/handlers/mdx/eventobjectubremitter.js +++ b/src/viewer/handlers/mdx/eventobjectubremitter.js @@ -35,7 +35,7 @@ export default class EventObjectUbrEmitter extends GeometryEmitter { gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); - model.bindTexture(modelObject.internalResource, 0, modelView); + modelView.bindTexture(modelObject.internalResource, 0); gl.uniform1f(uniforms.u_emitter, EMITTER_UBER); diff --git a/src/viewer/handlers/mdx/model.js b/src/viewer/handlers/mdx/model.js index 52135ad7..5548a810 100644 --- a/src/viewer/handlers/mdx/model.js +++ b/src/viewer/handlers/mdx/model.js @@ -1,5 +1,5 @@ import Parser from '../../../parsers/mdlx/model'; -import TexturedModel from '../../texturedmodel'; +import Model from '../../model'; import TextureAnimation from './textureanimation'; import Layer from './layer'; import GeosetAnimation from './geosetanimation'; @@ -21,7 +21,7 @@ import CollisionShape from './collisionshape'; /** * An MDX model. */ -export default class MdxModel extends TexturedModel { +export default class MdxModel extends Model { /** * @param {Object} resourceData */ @@ -504,7 +504,7 @@ export default class MdxModel extends TexturedModel { // Used for layers that use image animations, in order to scale the coordinates to match the generated texture atlas gl.uniform2f(uniforms.u_uvScale, 1 / layer.uvDivisor[0], 1 / layer.uvDivisor[1]); - this.bindTexture(texture, 0, bucket.modelView); + bucket.modelView.bindTexture(texture, 0); let geosetColor = attribs.a_geosetColor; let uvTransRot = attribs.a_uvTransRot; diff --git a/src/viewer/handlers/mdx/modeleventobject.js b/src/viewer/handlers/mdx/modeleventobject.js index f3cdcf79..8a3bc8de 100644 --- a/src/viewer/handlers/mdx/modeleventobject.js +++ b/src/viewer/handlers/mdx/modeleventobject.js @@ -1,11 +1,10 @@ -import {vec2} from 'gl-matrix'; -import MappedData from '../../../utils/mappeddata'; import {decodeAudioData} from '../../../common/audio'; +import MappedData from '../../../utils/mappeddata'; import GenericObject from './genericobject'; import {emitterFilterMode} from './filtermode'; -let mappedDataCallback = (text) => new MappedData(text); -let decodedDataCallback = (arrayBuffer) => decodeAudioData(arrayBuffer); +const mappedDataCallback = (text) => new MappedData(text); +const decodedDataCallback = (arrayBuffer) => decodeAudioData(arrayBuffer); /** * An event object. @@ -32,17 +31,36 @@ export default class EventObject extends GenericObject { this.ok = false; this.type = type; this.id = id; + this.tracks = eventObject.tracks; + this.globalSequence = null; + this.defval = new Uint32Array(1); + // SPN - Model + // SPL & UBR - Texture this.internalResource = null; - // For SND + // SPL & UBR + this.colors = null; + this.intervalTimes = null; + this.scale = 0; + this.columns = 0; + this.rows = 0; + this.lifeSpan = 0; + this.blendSrc = 0; + this.blendDst = 0; + + // SPL + this.intervals = null; + + // SND + this.distanceCutoff = 0; + this.maxDistance = 0; + this.minDistance = 0; + this.pitch = 0; + this.pitchVariance = 0; + this.volume = 0; this.decodedBuffers = []; - this.tracks = eventObject.tracks; - this.ok = false; - this.globalSequence = null; - this.defval = vec2.create(); - let globalSequenceId = eventObject.globalSequenceId; if (globalSequenceId !== -1) { this.globalSequence = model.globalSequences[globalSequenceId]; @@ -69,6 +87,14 @@ export default class EventObject extends GenericObject { viewer.whenLoaded(tables) .then((tables) => { + for (let table of tables) { + if (!table.ok) { + promise.resolve(); + + return; + } + } + this.load(tables); promise.resolve(); @@ -79,17 +105,14 @@ export default class EventObject extends GenericObject { * @param {Array} tables */ load(tables) { - if (!tables[0].ok) { - return; - } - - let type = this.type; - let model = this.model; - let viewer = model.viewer; - let pathSolver = model.pathSolver; let row = tables[0].data.getRow(this.id); + let type = this.type; if (row) { + let model = this.model; + let viewer = model.viewer; + let pathSolver = model.pathSolver; + if (type === 'SPN') { this.internalResource = viewer.load(row.Model.replace('.mdl', '.mdx'), pathSolver); } else if (type === 'SPL' || type === 'UBR') { @@ -98,28 +121,23 @@ export default class EventObject extends GenericObject { this.scale = row.Scale; if (type === 'SPL') { - this.dimensions = [row.Columns, row.Rows]; + this.columns = row.Columns; + this.rows = 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; } else { - this.dimensions = [1, 1]; + this.columns = 1; + this.rows = 1; this.intervalTimes = [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. // This is mostly to save on bandwidth and loading time, especially when loading full maps. if (viewer.enableAudio) { - if (!tables[1].ok) { - return; - } - row = tables[1].data.getRow(row.SoundLabel); if (row) { @@ -153,6 +171,11 @@ export default class EventObject extends GenericObject { } } + /** + * @param {Uint32Array} out + * @param {ModelInstance} instance + * @return {number} + */ getValue(out, instance) { if (this.globalSequence) { let globalSequence = this.globalSequence; @@ -163,38 +186,38 @@ export default class EventObject extends GenericObject { return this.getValueAtTime(out, instance.frame, interval[0], interval[1]); } else { - let defval = this.defval; + out[0] = this.defval[0]; - out[0] = defval[0]; - out[1] = defval[1]; - - return out; + return -1; } } + /** + * @param {Uint32Array} out + * @param {number} frame + * @param {number} start + * @param {number} end + * @return {number} + */ getValueAtTime(out, frame, start, end) { let tracks = this.tracks; if (frame < start || frame > end) { out[0] = 0; - out[1] = 0; - return out; + return -1; } for (let i = tracks.length - 1; i > -1; i--) { if (tracks[i] < start) { out[0] = 0; - out[1] = i; - return out; + return i; } else if (tracks[i] <= frame) { out[0] = 1; - out[1] = i; - return out; + return i; } } out[0] = 0; - out[1] = 0; - return out; + return -1; } } diff --git a/src/viewer/handlers/mdx/node.js b/src/viewer/handlers/mdx/node.js index e6cda32c..95263d1e 100644 --- a/src/viewer/handlers/mdx/node.js +++ b/src/viewer/handlers/mdx/node.js @@ -10,5 +10,6 @@ export default class MdxNode extends SkeletalNode { */ convertBasis(rotation) { quat.rotateY(rotation, rotation, -Math.PI / 2); + quat.rotateX(rotation, rotation, -Math.PI / 2); } } diff --git a/src/viewer/handlers/mdx/particle2.js b/src/viewer/handlers/mdx/particle2.js index 8c579196..95334440 100644 --- a/src/viewer/handlers/mdx/particle2.js +++ b/src/viewer/handlers/mdx/particle2.js @@ -20,7 +20,7 @@ const endHeap = vec3.create(); */ export default class Particle2 { /** - * @param {GeomeryEmitter} emitter + * @param {ParticleEmitter2} emitter */ constructor(emitter) { this.emitter = emitter; @@ -141,15 +141,12 @@ export default class Particle2 { } else { 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; + start[0] = end[0] - tailLength * velocity[0]; + start[1] = end[1] - tailLength * velocity[1]; + start[2] = end[2] - tailLength * velocity[2]; // If this is a model space emitter, the start and end are in local space, so convert them to world space. if (modelObject.modelSpace) { diff --git a/src/viewer/handlers/mdx/particleemitter2.js b/src/viewer/handlers/mdx/particleemitter2.js index db9d6fa4..80fc9be4 100644 --- a/src/viewer/handlers/mdx/particleemitter2.js +++ b/src/viewer/handlers/mdx/particleemitter2.js @@ -41,7 +41,7 @@ export default class ParticleEmitter2 extends GeometryEmitter { gl.blendFunc(modelObject.blendSrc, modelObject.blendDst); - model.bindTexture(modelObject.internalResource, 0, modelView); + modelView.bindTexture(modelObject.internalResource, 0); // Choose between a default rectangle or a billboarded one if (modelObject.xYQuad) { diff --git a/src/viewer/handlers/mdx/ribbon.js b/src/viewer/handlers/mdx/ribbon.js index e9f47c90..4116074e 100644 --- a/src/viewer/handlers/mdx/ribbon.js +++ b/src/viewer/handlers/mdx/ribbon.js @@ -13,7 +13,7 @@ let slotHeap = new Float32Array(1); */ export default class Ribbon { /** - * @param {MdxRibbonEmitter} emitter + * @param {RibbonEmitter} emitter */ constructor(emitter) { this.emitter = emitter; diff --git a/src/viewer/handlers/mdx/ribbonemitter.js b/src/viewer/handlers/mdx/ribbonemitter.js index f4fe48ab..7dd9efc6 100644 --- a/src/viewer/handlers/mdx/ribbonemitter.js +++ b/src/viewer/handlers/mdx/ribbonemitter.js @@ -45,7 +45,7 @@ export default class RibbonEmitter extends GeometryEmitter { layer.bind(shader); - model.bindTexture(modelObject.texture, 0, modelView); + modelView.bindTexture(modelObject.texture, 0); gl.uniform1f(uniforms.u_emitter, EMITTER_RIBBON); diff --git a/src/viewer/modelinstance.js b/src/viewer/modelinstance.js index 56e5ed4a..b41da93d 100644 --- a/src/viewer/modelinstance.js +++ b/src/viewer/modelinstance.js @@ -20,6 +20,8 @@ export default class ModelInstance extends EventNode { this.bottom = -1; /** @member {number} */ this.top = -1; + /** @member {boolean} */ + this.culled = false; /** @member {number} */ this.updateFrame = 0; /** @member {number} */ @@ -31,9 +33,17 @@ export default class ModelInstance extends EventNode { /** @member {?ModelViewData} */ this.modelViewData = null; - /** @member {boolean} */ + /** + * Decides whether the instance is updated or not. + * + * @member {boolean} + */ this.paused = false; - /** @member {boolean} */ + /** + * Decides whether the instance is rendered or not. + * + * @member {boolean} + */ this.rendered = true; } diff --git a/src/viewer/scene.js b/src/viewer/scene.js index 7f450800..e9ce7bca 100644 --- a/src/viewer/scene.js +++ b/src/viewer/scene.js @@ -204,9 +204,15 @@ export default class Scene { for (let instance of cell.instances) { if (!instance.parent) { - // The instance will check its update frame against the viewer, and act accordingly. - // It can't be done here like rendering, because instances with a parents won't actually be updated here. - instance.update(this); + /// TODO: This stops attached instances from being updated when their parent isn't, but it doesn't do the same for rendering. + /// Need to think how to handle this - even though updating is more important to cull, rendering is preferable as well. + let visible = this.camera.testInstance(instance); + + instance.culled = !visible; + + if (visible) { + instance.update(this); + } } } } else { @@ -230,7 +236,9 @@ export default class Scene { this.renderedCells += 1; for (let instance of cell.instances) { - instance.render(); + if (!instance.culled) { + instance.render(); + } } } } diff --git a/src/viewer/texturedmodel.js b/src/viewer/texturedmodel.js deleted file mode 100644 index a2f247b2..00000000 --- a/src/viewer/texturedmodel.js +++ /dev/null @@ -1,32 +0,0 @@ -import Model from './model'; - -/** - * A textured model. - * Gives a consistent API for texture overloading for handlers that use it. - */ -export default class TexturedModel extends Model { - /** - * Bind a texture to some texture unit. - * Checks the model view for an override. - * - * @param {Texture} texture - * @param {number} unit - * @param {ModelView} modelView - */ - bindTexture(texture, unit, modelView) { - let viewer = this.viewer; - let textures = modelView.textures; - - if (textures.has(texture)) { - texture = textures.get(texture); - } - - // If the texture exists, bind it. - // Otherwise, bind null, which will result in a black texture being bound to avoid console errors. - if (texture) { - texture.bind(unit); - } else { - viewer.webgl.bindTexture(null, unit); - } - } -} diff --git a/src/viewer/texturedmodelview.js b/src/viewer/texturedmodelview.js index 9b6a06e6..319216da 100644 --- a/src/viewer/texturedmodelview.js +++ b/src/viewer/texturedmodelview.js @@ -15,6 +15,27 @@ export default class TexturedModelView extends ModelView { this.textures = new Map(); } + /** + * Bind a texture to some texture unit. + * Checks the model view for an override. + * + * @param {Texture} texture + * @param {number} unit + */ + bindTexture(texture, unit) { + if (this.textures.has(texture)) { + texture = this.textures.get(texture); + } + + // If the texture exists, bind it. + // Otherwise, bind null, which will result in a black texture being bound to avoid console errors. + if (texture) { + texture.bind(unit); + } else { + this.model.viewer.webgl.bindTexture(null, unit); + } + } + /** * The shallow copy of a textured model view is a map of its textures. *