UNPKG

elation-engine

Version:
1,283 lines (1,222 loc) 70.3 kB
elation.require([ //"engine.things.trigger" "utils.proxy", "engine.math" ], function() { elation.component.add("engine.things.generic", function() { this.init = function() { this._proxies = {}; this._thingdef = { properties: {}, events: {}, actions: {} }; this.parentname = this.args.parentname || ''; this.name = this.args.name || ''; this.type = this.args.type || 'generic'; this.engine = this.args.engine; this.client = this.args.client; this.properties = {}; this.objects = {}; this.parts = {}; this.triggers = {}; this.parttypes = {}; this.children = {}; this.tags = []; this.sounds = {}; this.animations = false; this.skeleton = false; this.tmpvec = new THREE.Vector3(); this.interp = { rate: 20, lastTime: 0, time: 0, endpoint: new THREE.Vector3(), spline: [], active: false, fn: this.applyInterp }; //elation.events.add(this, 'thing_create', this); elation.events.add(this, 'thing_use_activate', this); this.defineActions({ 'spawn': this.spawn, 'move': this.move }); this.defineProperties({ 'position': { type: 'vector3', default: [0, 0, 0], comment: 'Object position, relative to parent' }, 'orientation': { type: 'quaternion', default: [0, 0, 0, 1], comment: 'Object orientation, relative to parent' }, 'scale': { type: 'vector3', default: [1, 1, 1], comment: 'Object scale, relative to parent' }, 'velocity': { type: 'vector3', default: [0, 0, 0], comment: 'Object velocity (m/s)' }, 'acceleration': { type: 'vector3', default: [0, 0, 0], comment: 'Object acceleration (m/s^2)' }, 'angular': { type: 'vector3', default: [0, 0, 0], comment: 'Object angular velocity (radians/sec)' }, 'angularacceleration': { type: 'vector3', default: [0, 0, 0], comment: 'Object angular acceleration (radians/sec^2)' }, 'mass': { type: 'float', default: 0.0, comment: 'Object mass (kg)' }, 'exists': { type: 'bool', default: true, comment: 'Exists' }, 'visible': { type: 'bool', default: true, comment: 'Is visible' }, 'physical': { type: 'bool', default: true, comment: 'Simulate physically' }, 'collidable': { type: 'bool', default: true, comment: 'Can crash into other things' }, 'restitution': { type: 'float', default: 0.5, comment: 'Amount of energy preserved after each bounce', set: this.updatePhysics }, 'dynamicfriction':{ type: 'float', default: 0.0, comment: 'Dynamic friction inherent to this object', set: this.updatePhysics }, 'staticfriction': { type: 'float', default: 0.0, comment: 'Static friction inherent to this object', set: this.updatePhysics }, //'fog': { type: 'bool', default: true, comment: 'Affected by fog' }, 'shadow': { type: 'bool', default: true, refreshMaterial: true, comment: 'Casts and receives shadows' }, 'wireframe': { type: 'bool', default: false, refreshMaterial: true, comment: 'Render this object as a wireframe' }, 'forcereload': { type: 'bool', default: false, refreshGeometry: true, refreshMaterial: true, comment: 'Force a full reload of all files' }, 'mouseevents': { type: 'bool', default: true, comment: 'Respond to mouse/touch events' }, 'persist': { type: 'bool', default: false, comment: 'Continues existing across world saves' }, 'pickable': { type: 'bool', default: true, comment: 'Selectable via mouse/touch events' }, 'render.mesh': { type: 'string', refreshGeometry: true, comment: 'URL for JSON model file' }, 'render.meshname':{ type: 'string', refreshGeometry: true }, 'render.scene': { type: 'string', refreshGeometry: true, comment: 'URL for JSON scene file' }, 'render.collada': { type: 'string', refreshGeometry: true, comment: 'URL for Collada scene file' }, 'render.model': { type: 'string', refreshGeometry: true, comment: 'Name of model asset' }, 'render.gltf': { type: 'string', refreshGeometry: true, comment: 'URL for glTF file' }, 'render.materialname': { type: 'string', refreshMaterial: true, comment: 'Material library name' }, 'render.texturepath': { type: 'string', refreshMaterial: true, comment: 'Texture location' }, 'player_id': { type: 'float', default: null, comment: 'Network id of the creator' }, 'tags': { type: 'string', comment: 'Default tags to add to this object' } }); this.defineEvents({ 'thing_create': [], 'thing_add': ['child'], 'thing_load': ['mesh'], 'thing_remove': [], 'thing_destroy': [], 'thing_tick': ['delta'], 'thing_think': ['delta'], 'thing_move': [], 'mouseover': ['clientX', 'clientY', 'position'], 'mouseout': [], 'mousedown': [], 'mouseup': [], 'click': [] }); if (typeof this.preinit == 'function') { this.preinit(); } if (typeof this.postinit == 'function') { this.postinit(); } this.init3D(); this.initDOM(); this.initPhysics(); setTimeout(elation.bind(this, function() { // Fire create event next frame this.createChildren(); this.refresh(); elation.events.fire({type: 'thing_create', element: this}); }), 0); } this.preinit = function() { } this.postinit = function() { } this.initProperties = function() { if (!this.properties) { this.properties = {}; } for (var propname in this._thingdef.properties) { var prop = this._thingdef.properties[propname]; if (!this.hasOwnProperty(propname)) { this.defineProperty(propname, prop); } } } this.getPropertyValue = function(type, value) { if (value === null) { return; } switch (type) { case 'vector2': if (elation.utils.isArray(value)) { value = new THREE.Vector2(+value[0], +value[1]); } else if (elation.utils.isString(value)) { var split = value.split((value.indexOf(' ') != -1 ? ' ' : ',')); value = new THREE.Vector2(+split[0], +split[1]); } break; case 'vector3': if (elation.utils.isArray(value)) { value = new elation.physics.vector3(+value[0], +value[1], +value[2]); } else if (elation.utils.isString(value)) { var split = value.split((value.indexOf(' ') != -1 ? ' ' : ',')); value = new elation.physics.vector3(+split[0], +split[1], +split[2]); } break; case 'euler': if (elation.utils.isArray(value)) { value = new EulerDegrees().set(+value[0], +value[1], +value[2]); } else if (elation.utils.isString(value)) { var split = value.split((value.indexOf(' ') != -1 ? ' ' : ',')); value = new EulerDegrees().set(+split[0], +split[1], +split[2]); } else if (value instanceof THREE.Vector3) { value = new EulerDegrees().set(value.x, value.y, value.z); } break; case 'quaternion': if (elation.utils.isArray(value)) { value = new elation.physics.quaternion(+value[0], +value[1], +value[2], +value[3]); } else if (elation.utils.isString(value)) { var split = value.split((value.indexOf(' ') != -1 ? ' ' : ',')); value = new elation.physics.quaternion(+split[0], +split[1], +split[2], +split[3]); } else if (value instanceof THREE.Quaternion && !(value instanceof elation.physics.quaternion)) { value = new elation.physics.quaternion().copy(value); } break; case 'color': var clamp = elation.utils.math.clamp; if (value instanceof THREE.Vector3) { value = new THREE.Color(clamp(value.x, 0, 1), clamp(value.y, 0, 1), clamp(value.z, 0, 1)); } else if (value instanceof THREE.Vector4) { this.opacity = clamp(value.w, 0, 1); value = new THREE.Color(clamp(value.x, 0, 1), clamp(value.y, 0, 1), clamp(value.z, 0, 1)); } else if (elation.utils.isString(value)) { var splitpos = value.indexOf(' '); if (splitpos == -1) splitpos = value.indexOf(','); if (splitpos == -1) { if (elation.utils.isnumeric(value) && value <= 1.0) { value = new THREE.Color(value, value, value); } else { value = new THREE.Color(value); } } else { var split = value.split((value.indexOf(' ') != -1 ? ' ' : ',')); value = new THREE.Color(clamp(split[0], 0, 1), clamp(split[1], 0, 1), clamp(split[2], 0, 1)); if (split.length > 3) { this.opacity = clamp(split[3], 0, 1); } } } else if (elation.utils.isArray(value)) { value = new THREE.Color(+value[0], +value[1], +value[2]); } else if (!(value instanceof THREE.Color)) { value = new THREE.Color(value); } break; case 'bool': case 'boolean': value = !(value === false || (elation.utils.isString(value) && value.toLowerCase() === 'false') || value === 0 || value === '0' || value === '' || value === null || typeof value == 'undefined'); break; case 'float': value = +value; break; case 'int': case 'integer': value = value | 0; break; case 'texture': if (value !== false) { value = (value instanceof THREE.Texture ? value : elation.engine.materials.getTexture(value)); } break; case 'json': if (value !== false) { value = (elation.utils.isString(value) ? JSON.parse(value) : value); } break; case 'component': if (value) { var component = elation.component.fetch(value[0], value[1]); if (component) { value = component; } } break; } return value; } this.defineProperties = function(properties) { elation.utils.merge(properties, this._thingdef.properties); this.initProperties(); } this.defineProperty = function(propname, prop) { var propval = elation.utils.arrayget(this.properties, propname, null); Object.defineProperty(this, propname, { configurable: true, enumerable: true, get: function() { if ('get' in prop) { return prop.get.call(this); } var proxy = this._proxies[propname]; if (proxy) { return proxy; } return elation.utils.arrayget(this.properties, propname); }, set: function(v) { this.set(propname, v, prop.refreshGeometry); //this.refresh(); //console.log('set ' + propname + ' to ', v); } }); if (propval === null) { if (!elation.utils.isNull(this.args.properties[propname])) { propval = this.args.properties[propname] } else if (!elation.utils.isNull(prop.default)) { propval = prop.default; } this.set(propname, propval); } if (prop.type == 'vector2' || prop.type == 'vector3' || prop.type == 'quaternion' || prop.type == 'color') { if (propval && !this._proxies[propname]) { // Create proxy objects for these special types var proxydef = { x: ['property', 'x'], y: ['property', 'y'], z: ['property', 'z'], changed: ['property', 'changed'], add: ['function', 'add'], addScalar: ['function', 'addScalar'], addScaledVector: ['function', 'addScaledVector'], addVectors: ['function', 'addVectors'], applyAxisAngle: ['function', 'applyAxisAngle'], angleTo: ['function', 'angleTo'], ceil: ['function', 'ceil'], clamp: ['function', 'clamp'], clampLength: ['function', 'clampLength'], clampScalar: ['function', 'clampScalar'], clone: ['function', 'clone'], constructor: ['function', 'constructor'], copy: ['function', 'copy'], cross: ['function', 'cross'], crossVectors: ['function', 'crossVectors'], distanceTo: ['function', 'distanceTo'], manhattanDistanceTo: ['function', 'manhattanDistanceTo'], distanceToSquared: ['function', 'distanceToSquared'], divide: ['function', 'divide'], divideScalar: ['function', 'divideScalar'], dot: ['function', 'dot'], equals: ['function', 'equals'], floor: ['function', 'floor'], length: ['function', 'length'], manhattanLength: ['function', 'manhattanLength'], lengthSq: ['function', 'lengthSq'], lerp: ['function', 'lerp'], lerpVectors: ['function', 'lerpVectors'], max: ['function', 'max'], min: ['function', 'min'], multiply: ['function', 'multiply'], multiplyScalar: ['function', 'multiplyScalar'], multiplyVectors: ['function', 'multiplyVectors'], negate: ['function', 'negate'], normalize: ['function', 'normalize'], projectOnPlane: ['function', 'projectOnPlane'], projectOnVector: ['function', 'projectOnVector'], reflect: ['function', 'reflect'], round: ['function', 'round'], roundToZero: ['function', 'roundToZero'], set: ['function', 'set'], setLength: ['function', 'setLength'], setScalar: ['function', 'setScalar'], sub: ['function', 'sub'], subScalar: ['function', 'subScalar'], subVectors: ['function', 'subVectors'], toArray: ['function', 'toArray'], reset: ['function', 'reset'], }; if (prop.type == 'quaternion') { proxydef.w = ['property', 'w']; } else if (prop.type == 'color') { // We want to support color.xyz as well as color.rgb proxydef.r = ['property', 'r']; proxydef.g = ['property', 'g']; proxydef.b = ['property', 'b']; proxydef.x = ['property', 'r']; proxydef.y = ['property', 'g']; proxydef.z = ['property', 'b']; } var propval = elation.utils.arrayget(this.properties, propname, null); if (propval) { this._proxies[propname] = new elation.proxy( propval, proxydef, true ); // FIXME - listening for proxy_change events would let us respond to changes for individual vector elements, but it gets expensive, and can lead to weird infinite loops /* elation.events.add(propval, 'proxy_change', elation.bind(this, function(ev) { //this.refresh(); //this.set('exists', this.properties.exists, prop.refreshGeometry); //this[propname] = this[propname]; var propdef = this._thingdef.properties[propname]; if (propdef && propdef.set) { propdef.set.apply(this, [propname, propval]); } })); */ } } } else if (prop.type == 'euler') { if (propval && !this._proxies[propname]) { // Create proxy objects for these special types var propval = elation.utils.arrayget(this.properties, propname, null); let degrees = new EulerDegrees(propval); var proxydef = { x: ['property', 'x'], y: ['property', 'y'], z: ['property', 'z'], order: ['property', 'order'], set: ['function', 'set'], copy: ['function', 'copy'], clone: ['function', 'clone'], constructor: ['function', 'constructor'], toArray: ['function', 'toArray'], }; if (propval) { /* this._proxies[propname] = new elation.proxy( degrees, proxydef, true ); */ } } } } this.defineActions = function(actions) { elation.utils.merge(actions, this._thingdef.actions); } this.defineEvents = function(events) { elation.utils.merge(events, this._thingdef.events); } this.set = function(property, value, forcerefresh) { var propdef = this._thingdef.properties[property]; if (!propdef) { console.warn('Tried to set unknown property', property, value, this); return; } var changed = false var propval = this.getPropertyValue(propdef.type, value); var currval = this.get(property); //if (currval !== null) { switch (propdef.type) { case 'vector2': case 'vector3': case 'vector4': case 'quaternion': case 'color': if (currval === null) { elation.utils.arrayset(this.properties, property, propval); changed = true; } else { if (!currval.equals(propval)) { currval.copy(propval); changed = true } } break; case 'euler': if (currval === null) { elation.utils.arrayset(this.properties, property, propval); changed = true; } else { if (!currval.equals(propval)) { currval.copy(propval); changed = true } } break; case 'texture': //console.log('TRY TO SET NEW TEX', property, value, forcerefresh); default: if (currval !== propval) { elation.utils.arrayset(this.properties, property, propval); changed = true; } } //} else { // elation.utils.arrayset(this.properties, property, propval); //} if (changed) { if (propdef.set) { propdef.set.apply(this, [property, propval]); } if (forcerefresh && this.objects['3d']) { var oldobj = this.objects['3d'], parent = oldobj.parent, newobj = this.createObject3D(); this.objects['3d'] = newobj; this.bindObjectProperties(this.objects['3d']); this.objects['3d'].userData.thing = this; elation.events.fire({type: 'thing_recreate', element: this}); if (parent) { parent.remove(oldobj); parent.add(newobj); } } if (this.objects.dynamics) { if (false && forcerefresh) { this.removeDynamics(); this.initPhysics(); } else { this.objects.dynamics.mass = this.properties.mass; this.objects.dynamics.updateState(); if (this.objects.dynamics.collider) { this.objects.dynamics.collider.getInertialMoment(); } } this.objects.dynamics.position = this.properties.position; this.objects.dynamics.orientation = this.properties.orientation; } this.refresh(); } } this.setProperties = function(properties, interpolate) { for (var prop in properties) { if (prop == 'position' && interpolate == true ) { if ( this.tmpvec.fromArray(properties[prop]).distanceToSquared(this.get('position')) > 1 ) { // call interpolate function // TODO: fix magic number 0.001 this.interpolateTo(properties[prop]); } } else { this.set(prop, properties[prop], false); } } this.refresh(); } this.interpolateTo = function(newPos) { this.interp.time = 0; this.interp.endpoint.fromArray(newPos); this.interp.spline = new THREE.SplineCurve3([this.get('position'), this.interp.endpoint]).getPoints(10); // console.log(this.interp.spline); elation.events.add(this.engine, 'engine_frame', elation.bind(this, this.applyInterp)); } this.applyInterp = function(ev) { this.interp.time += ev.data.delta * this.engine.systems.physics.timescale; if (this.interp.time >= this.interp.rate) { elation.events.remove(this, 'engine_frame', elation.bind(this, this.applyInterp)); return; } console.log("DEBUG: interpolating, time:", this.interp.time); if (this.interp.time - this.interp.lastTime >= 2) { this.set('position', this.interp.spline[Math.floor((this.interp.time * 10) / this.interp.rate)], false); this.refresh(); } }; this.get = function(property, defval) { if (typeof defval == 'undefined') defval = null; return elation.utils.arrayget(this.properties, property, defval); } this.init3D = function() { if (this.objects['3d']) { if (this.objects['3d'].parent) { this.objects['3d'].parent.remove(this.objects['3d']); } } if (this.properties.tags) { var tags = this.properties.tags.split(','); for (var i = 0; i < tags.length; i++) { this.addTag(tags[i].trim()); } } this.objects['3d'] = this.createObject3D(); if (this.objects['3d']) { this.bindObjectProperties(this.objects['3d']); //this.objects['3d'].useQuaternion = true; this.objects['3d'].userData.thing = this; } if (!this.colliders) { this.colliders = new THREE.Object3D(); this.bindObjectProperties(this.colliders); //this.colliders.scale.set(1/this.properties.scale.x, 1/this.properties.scale.y, 1/this.properties.scale.z); this.colliders.userData.thing = this; } var childkeys = Object.keys(this.children); if (childkeys.length > 0) { // Things were added during initialization, so make sure they're added to the scene for (var i = 0; i < childkeys.length; i++) { var k = childkeys[i]; if (this.children[k].objects['3d']) { this.objects['3d'].add(this.children[k].objects['3d']); } } } if (!this.properties.visible) { this.hide(); } elation.events.fire({type: 'thing_init3d', element: this}); this.refresh(); } this.initDOM = function() { if (ENV_IS_BROWSER) { this.objects['dom'] = this.createObjectDOM(); elation.html.addclass(this.container, "space.thing"); elation.html.addclass(this.container, "space.things." + this.type); //this.updateDOM(); } } this.initPhysics = function() { if (this.properties.physical) { this.createDynamics(); this.createForces(); } } this.createObject3D = function() { // if (this.properties.exists === false || !ENV_IS_BROWSER) return; if (this.properties.exists === false) return; var object = null, geometry = null, material = null; var cachebust = ''; if (this.properties.forcereload) cachebust = '?n=' + (Math.floor(Math.random() * 10000)); if (this.properties.render) { if (this.properties.render.scene) { this.loadJSONScene(this.properties.render.scene, this.properties.render.texturepath + cachebust); } else if (this.properties.render.mesh) { this.loadJSON(this.properties.render.mesh, this.properties.render.texturepath + cachebust); } else if (this.properties.render.collada) { this.loadCollada(this.properties.render.collada + cachebust); } else if (this.properties.render.model) { object = elation.engine.assets.find('model', this.properties.render.model); } else if (this.properties.render.gltf) { this.loadglTF(this.properties.render.gltf + cachebust); } else if (this.properties.render.meshname) { object = new THREE.Object3D(); setTimeout(elation.bind(this, this.loadMeshName, this.properties.render.meshname), 0); } } var geomparams = elation.utils.arrayget(this.properties, "generic.geometry") || {}; switch (geomparams.type) { case 'sphere': geometry = new THREE.SphereGeometry(geomparams.radius || geomparams.size, geomparams.segmentsWidth, geomparams.segmentsHeight); break; case 'cube': geometry = new THREE.BoxGeometry( geomparams.width || geomparams.size, geomparams.height || geomparams.size, geomparams.depth || geomparams.size, geomparams.segmentsWidth || 1, geomparams.segmentsHeight || 1, geomparams.segmentsDepth || 1); break; case 'cylinder': geometry = new THREE.CylinderGeometry( geomparams.radiusTop || geomparams.radius, geomparams.radiusBottom || geomparams.radius, geomparams.height, geomparams.segmentsRadius, geomparams.segmentsHeight, geomparams.openEnded); break; case 'torus': geometry = new THREE.TorusGeometry(geomparams.radius, geomparams.tube, geomparams.segmentsR, geomparams.segmentsT, geomparams.arc); break; } if (geometry) { var materialparams = elation.utils.arrayget(this.properties, "generic.material") || {}; if (materialparams instanceof THREE.Material) { material = materialparams; } else { switch (materialparams.type) { case 'phong': material = new THREE.MeshPhongMaterial(materialparams); break; case 'lambert': material = new THREE.MeshLambertMaterial(materialparams); break; case 'depth': material = new THREE.MeshDepthMaterial(); break; case 'normal': material = new THREE.MeshNormalMaterial(); break; case 'basic': default: material = new THREE.MeshBasicMaterial(materialparams); } } } if (!geometry && !material) { //geometry = new THREE.BoxGeometry(1, 1, 1); //material = new THREE.MeshPhongMaterial({color: 0xcccccc, opacity: .2, emissive: 0x333333, transparent: true}); //console.log('made placeholder thing', geometry, material); } if (!object) { object = (geometry && material ? new THREE.Mesh(geometry, material) : new THREE.Object3D()); } if (geometry && material) { if (geomparams.flipSided) material.side = THREE.BackSide; if (geomparams.doubleSided) material.side = THREE.DoubleSide; } this.objects['3d'] = object; //this.spawn('gridhelper', {persist: false}); return object; } this.createObjectDOM = function() { return; } this.createChildren = function() { return; } this.add = function(thing) { if (!this.children[thing.id]) { this.children[thing.id] = thing; thing.parent = this; if (this.objects && thing.objects && this.objects['3d'] && thing.objects['3d']) { this.objects['3d'].add(thing.objects['3d']); } else if (thing instanceof THREE.Object3D) { this.objects['3d'].add(thing); } if (this.objects && thing.objects && this.objects['dynamics'] && thing.objects['dynamics']) { this.objects['dynamics'].add(thing.objects['dynamics']); } if (this.container && thing.container) { this.container.appendChild(thing.container); } if (this.colliders && thing.colliders) { this.colliders.add(thing.colliders); } elation.events.fire({type: 'thing_add', element: this, data: {thing: thing}}); return true; } else { console.log("Couldn't add ", thing.name, " already exists in ", this.name); } return false; } this.remove = function(thing) { if (thing && this.children[thing.id]) { if (this.objects['3d'] && thing.objects['3d']) { this.objects['3d'].remove(thing.objects['3d']); } if (thing.container && thing.container.parentNode) { thing.container.parentNode.removeChild(thing.container); } if (thing.objects['dynamics'] && thing.objects['dynamics'].parent) { thing.objects['dynamics'].parent.remove(thing.objects['dynamics']); } if (this.colliders && thing.colliders) { this.colliders.remove(thing.colliders); } if (thing.colliderhelper) { //this.engine.systems.world.scene['colliders'].remove(thing.colliderhelper); } elation.events.fire({type: 'thing_remove', element: this, data: {thing: thing}}); delete this.children[thing.id]; thing.parent = false; } else { console.log("Couldn't remove ", thing.name, " doesn't exist in ", this.name); } } this.reparent = function(newparent) { if (newparent) { if (this.parent) { newparent.worldToLocal(this.parent.localToWorld(this.properties.position)); this.properties.orientation.copy(newparent.worldToLocalOrientation(this.parent.localToWorldOrientation())); this.parent.remove(this); //newparent.worldToLocalDir(this.parent.localToWorldDir(this.properties.orientation)); } var success = newparent.add(this); this.refresh(); return success; } return false; } this.show = function() { this.objects['3d'].visible = true; if (this.colliderhelper) this.colliderhelper.visible = true; } this.hide = function() { this.objects['3d'].visible = false; if (this.colliderhelper) this.colliderhelper.visible = false; } this.createDynamics = function() { if (!this.objects['dynamics'] && this.engine.systems.physics) { this.objects['dynamics'] = new elation.physics.rigidbody({ position: this.properties.position, orientation: this.properties.orientation, mass: this.properties.mass, scale: this.properties.scale, velocity: this.properties.velocity, acceleration: this.properties.acceleration, angular: this.properties.angular, angularacceleration: this.properties.angularacceleration, restitution: this.properties.restitution, material: { dynamicfriction: this.properties.dynamicfriction, staticfriction: this.properties.staticfriction, }, object: this }); //this.engine.systems.physics.add(this.objects['dynamics']); if ((this.properties.collidable || this.properties.pickable) && this.objects['3d'] && this.objects['3d'].geometry) { setTimeout(elation.bind(this, this.updateColliderFromGeometry), 0); } elation.events.add(this.objects['dynamics'], "physics_collide", this); //elation.events.add(this.objects['dynamics'], "physics_update", elation.bind(this, this.refresh)); } } this.removeDynamics = function() { if (this.objects.dynamics) { if (this.objects.dynamics.parent) { this.objects.dynamics.parent.remove(this.objects.dynamics); } else { this.engine.systems.physics.remove(this.objects.dynamics); } delete this.objects.dynamics; } } this.createForces = function() { } this.addForce = function(type, args) { return this.objects.dynamics.addForce(type, args); } this.removeForce = function(force) { return this.objects.dynamics.removeForce(force); } this.updateColliderFromGeometry = function(geom) { if (!geom) geom = this.objects['3d'].geometry; var collidergeom = false; // Determine appropriate collider for the geometry associated with this thing var dyn = this.objects['dynamics']; if (geom && dyn) { if (geom instanceof THREE.SphereGeometry) { if (!geom.boundingSphere) geom.computeBoundingSphere(); this.setCollider('sphere', {radius: geom.boundingSphere.radius}); } else if (geom instanceof THREE.PlaneGeometry) { if (!geom.boundingBox) geom.computeBoundingBox(); var size = new THREE.Vector3().subVectors(geom.boundingBox.max, geom.boundingBox.min); // Ensure minimum size if (size.x < 1e-6) size.x = .25; if (size.y < 1e-6) size.y = .25; if (size.z < 1e-6) size.z = .25; this.setCollider('box', geom.boundingBox); } else if (geom instanceof THREE.CylinderGeometry) { if (geom.radiusTop == geom.radiusBottom) { this.setCollider('cylinder', {height: geom.height, radius: geom.radiusTop}); } else { console.log('FIXME - cylinder collider only supports uniform cylinders for now'); } } else if (!dyn.collider) { if (!geom.boundingBox) geom.computeBoundingBox(); this.setCollider('box', geom.boundingBox); } } } this.setCollider = function(type, args, rigidbody, reuseMesh) { //console.log('set collider', type, args, rigidbody, this.collidable); if (!rigidbody) rigidbody = this.objects['dynamics']; if (this.properties.collidable) { rigidbody.setCollider(type, args); } if (this.properties.collidable || this.properties.pickable) { var collidergeom = false; if (type == 'sphere') { collidergeom = elation.engine.geometries.generate('sphere', { radius: args.radius / this.properties.scale.x }); } else if (type == 'box') { var size = new THREE.Vector3().subVectors(args.max, args.min); size.x /= this.scale.x; size.y /= this.scale.y; size.z /= this.scale.z; var offset = new THREE.Vector3().addVectors(args.max, args.min).multiplyScalar(.5); collidergeom = elation.engine.geometries.generate('box', { size: size, offset: offset }); } else if (type == 'cylinder') { collidergeom = elation.engine.geometries.generate('cylinder', { radius: args.radius, height: args.height, radialSegments: 12 }); if (args.offset) { collidergeom.applyMatrix4(new THREE.Matrix4().makeTranslation(args.offset.x, args.offset.y, args.offset.z)); } } else if (type == 'capsule') { collidergeom = elation.engine.geometries.generate('capsule', { radius: args.radius, length: elation.utils.any(args.length, args.height), radialSegments: 8, offset: args.offset, }); } else if (type == 'triangle') { collidergeom = elation.engine.geometries.generate('triangle', { p1: args.p1, p2: args.p2, p3: args.p3 }); } else if (type == 'mesh') { // FIXME - not sure if this is the best way to do it, but this forces mesh colliders into the picking scene collidermesh = args.mesh; this.colliders.add(args.mesh); } /* if (this.collidermesh) { this.colliders.remove(this.collidermesh); this.engine.systems.world.scene['colliders'].remove(this.colliderhelper); this.collidermesh = false; } */ // If we have children, re-add their colliders as children of our own collider, but only if we're working with the root object if (rigidbody === this.objects['dynamics']) { for (var k in this.children) { if (this.children[k].colliders) { this.colliders.add(this.children[k].colliders); } } } if (collidergeom) { let collidercolor = 0x009900; if (rigidbody.mass === 0) { collidercolor = 0x990000; } var collidermat = new THREE.MeshPhongMaterial({color: collidercolor, opacity: .2, transparent: true, emissive: 0x333300, alphaTest: .1, depthTest: false, depthWrite: false, side: THREE.DoubleSide}); if (reuseMesh && this.collidermesh) { var collidermesh = this.collidermesh; collidermesh.geometry = collidergeom; } else { var collidermesh = this.collidermesh = new THREE.Mesh(collidergeom, collidermat); if (rigidbody.position !== this.properties.position) { // Bind the mesh's position to the rigidbody that represents this part Object.defineProperties( collidermesh, { position: { enumerable: true, configurable: true, value: rigidbody.position }, quaternion: { enumerable: true, configurable: true, value: rigidbody.orientation }, scale: { enumerable: true, configurable: true, value: rigidbody.scale }, }); } collidermesh.userData.thing = this; this.colliders.add(collidermesh); } collidermesh.updateMatrix(); collidermesh.updateMatrixWorld(); var colliderhelper = this.colliderhelper; if (!colliderhelper) { //colliderhelper = this.colliderhelper = new THREE.EdgesHelper(collidermesh, 0x999900); //colliderhelper.matrix = collidermesh.matrix; //this.colliders.add(colliderhelper); } else { //THREE.EdgesHelper.call(colliderhelper, collidermesh, 0x990099); } //this.engine.systems.world.scene['colliders'].add(colliderhelper); // TODO - integrate this with the physics debug system /* elation.events.add(rigidbody, 'physics_collide', function() { collidermat.color.setHex(0x990000); colliderhelper.material.color.setHex(0x990000); setTimeout(function() { collidermat.color.setHex(0x999900); colliderhelper.material.color.setHex(0x999900); }, 100); }); elation.events.add(this, 'mouseover,mouseout', elation.bind(this, function(ev) { var color = 0xffff00; if (ev.type == 'mouseover' && ev.data.object === collidermesh) { color = 0x00ff00; } collidermat.color.setHex(0xffff00); colliderhelper.material.color.setHex(color); this.refresh(); })); */ } } } this.resetColliders = function() { while (this.colliders.children.length > 0) { this.colliders.remove(this.colliders.children[0]); } } this.physics_collide = function(ev) { let obj1 = ev.data.bodies[0].object, obj2 = ev.data.bodies[1].object, other = (obj1 == this ? obj2 : obj1); let events = elation.events.fire({type: 'collide', element: this, data: { other: other, collision: ev.data } }); if (elation.events.wasDefaultPrevented(events)) { ev.preventDefault(); } } this.loadJSON = function(url, texturepath) { if (typeof texturepath == 'undefined') { texturepath = '/media/space/textures/'; } var loader = new THREE.JSONLoader(); loader.load(url, elation.bind(this, this.processJSON), texturepath); } this.processJSON = function(geometry, materials) { geometry.computeFaceNormals(); geometry.computeVertexNormals(); var mesh = new THREE.Mesh(geometry, materials); mesh.doubleSided = false; mesh.castShadow = false; mesh.receiveShadow = false; //this.objects['3d'].updateCollisionSize(); elation.events.fire({type: "thing_load", element: this, data: mesh}); this.objects['3d'].add(mesh); this.refresh(); } this.loadJSONScene = function(url, texturepath) { if (typeof texturepath == 'undefined') { texturepath = '/media/space/textures'; } var loader = new THREE.ObjectLoader(); loader.load(url, elation.bind(this, this.processJSONScene, url)); } this.processJSONScene = function(url, scene) { this.extractEntities(scene); this.objects['3d'].add(scene); this.extractColliders(scene); var textures = this.extractTextures(scene, true); this.loadTextures(textures); elation.events.fire({ type: 'resource_load_finish', element: this, data: { type: 'model', url: url } }); this.extractAnimations(scene); this.refresh(); } this.loadCollada = function(url) { if (!THREE.ColladaLoader) { // If the loader hasn't been initialized yet, fetch it! elation.require('engine.external.three.ColladaLoader', elation.bind(this, this.loadCollada, url)); } else { var loader = new THREE.ColladaLoader(); loader.options.convertUpAxis = true; var xhr = loader.load(url, elation.bind(this, this.processCollada, url)); elation.events.fire({ type: 'resource_load_start', element: this, data: { type: 'model', url: url } }); } } this.processCollada = function(url, collada) { //collada.scene.rotation.x = -Math.PI / 2; //collada.scene.rotation.z = Math.PI; this.extractEntities(collada.scene); /* collada.scene.computeBoundingSphere(); collada.scene.computeBoundingBox(); //this.updateCollisionSize(); */ this.objects['3d'].add(collada.scene); this.extractColliders(collada.scene, true); var textures = this.extractTextures(collada.scene, true); this.loadTextures(textures); elation.events.fire({ type: 'resource_load_finish', element: this, data: { type: 'model', url: url } }); this.refresh(); } this.loadglTF = function(url) { if (!THREE.glTFLoader) { // If the loader hasn't been initialized yet, fetch it! elation.require('engine.external.three.glTFLoader-combined', elation.bind(this, this.loadglTF, url)); } else { var loader = new THREE.glTFLoader(); loader.useBufferGeometry = true; loader.load(url, elation.bind(this, this.processglTF, url)); elation.events.fire({ type: 'resource_load_start', data: { type: 'model', url: url } }); } } this.processglTF = function(url, scenedata) { this.extractEntities(scenedata.scene); //this.updateCollisionSize(); //this.objects['3d'].add(scenedata.scene); var parent = this.objects['3d'].parent; parent.remove(this.objects['3d']); this.objects['3d'] = new THREE.Object3D(); this.bindObjectProperties(this.objects['3d']); this.objects['3d'].userData.thing = this; // Correct coordinate space from various modelling programs // FIXME - hardcoded for blender's settings for now, this should come from a property var scene = scenedata.scene; scene.rotation.x = -Math.PI/2; // FIXME - enable shadows for all non-transparent materials. This should be coming from the model file... scene.traverse(function(n) { if (n instanceof THREE.Mesh) { if (n.material && !(n.material.transparent || n.material.opacity < 1.0)) { n.castShadow = true; n.receiveShadow = true; } else { n.castShadow = false; n.receiveShadow = false; } } }); /* while (scenedata.scene.children.length > 0) { var obj = scenedata.scene.children[0]; scenedata.scene.remove(obj); coordspace.add(obj); } this.objects['3d'].add(coordspace); */ this.objects['3d'].add(scene); //this.objects['3d'].quaternion.setFromEuler(scenedata.scene.rotation); var textures = this.extractTextures(scene, true); this.loadTextures(textures); parent.add(this.objects['3d']); // If no pending textures, we're already loaded, so fire the event if (this.pendingtextures == 0) { elation.events.fire({type: "thing_load", element: this, data: scenedata.scene}); } elation.events.fire({ type: 'resource_load_finish', data: { type: 'model', url: url } }); this.refresh(); } this.loadMeshName = function(meshname) { var subobj = elation.engine.geometries.getMesh(meshname); subobj.rotation.x = -Math.PI/2; subobj.rotation.y = 0; subobj.rotation.z = Math.PI; this.extractEntities(subobj); this.objects['3d'].add(subobj); this.extractColliders(subobj); elation.events.add(null, 'resource_load_complete', elation.bind(this, this.extractColliders, subobj)); if (ENV_IS_BROWSER){ var textures = this.extractTextures(subobj, true); this.loadTextures(textures); } } this.extractEntities = function(scene) { this.cameras = []; this.parts = {}; if (!scene) scene = this.objects['3d']; scene.traverse(elation.bind(this, function ( node ) { if ( node instanceof THREE.Camera ) { this.cameras.push(node); //} else if (node instanceof THREE.Mesh) { } else if (node.name !== '') { this.parts[node.name] = node; node.castShadow = this.properties.shadow; node.receiveShadow = this.properties.shadow; } if (node.material) { node.material.fog = this.properties.fog; node.material.wireframe = this.properties.wireframe; } })); //console.log('Collada loaded: ', this.parts, this.cameras, this); if (this.cameras.length > 0) { this.camera = this.cameras[0]; } //this.updateCollisionSize(); } this.extractColliders = function(obj, useParentPosition) { if (!(this.properties.collidable || this.properties.pickable)) return; var meshes = []; if (!obj) obj = this.objects['3d']; var re = new RegExp(/^[_\*](collider|trigger)-(.*)$/); obj.traverse(function(n) { if (n instanceof THREE.Mesh && n.material) { var materials = n.material; if (!elation.utils.isArray(n.material)) { materials = [n.material]; } for (var i = 0; i < materials.length; i++) { var m = materials[i]; if (m.name && m.name.match(re)) { n.geometry.computeBoundingBox(); n.geometry.computeBoundingSphere(); meshes.push(n); break; } } } }); // FIXME - hack to make demo work //this.colliders.bindPosition(this.localToWorld(new THREE.Vector3())); //var flip = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0)); var flip = new elation.physics.quaternion(); var root = new elation.physics.rigidbody({orientation: flip, object: this});// orientation: obj.quaternion.clone() }); //root.orientation.multiply(flip); for (var i = 0; i < meshes.length; i++) { var m = meshes[i].material.name.match(re), type = m[1], shape = m[2]; var rigid = new elation.physics.rigidbody({object: this}); var min = meshes[i].geometry.boundingBox.min.clone().multiply(meshes[i].scale), max = meshes[i].geometry.boundingBox.max.clone().multiply(meshes[i].scale); //console.log('type is', type, shape, min, max); var position = meshes[i].position, orientation = meshes[i].quaternion; if (useParentPosition) { position = meshes[i].parent.position; orientation = meshes[i].parent.quaternion; } rigid.position.copy(position); rigid.orientation.copy(orientation); min.x *= this.properties.scale.x; min.y *= this.properties.scale.y; min.z *= this.properties.scale.z; max.x *= this.properties.scale.x; max.y *= this.properties.scale.y; max.z *= this.properties.scale.z; rigid.position.x *= this.properties.scale.x; rigid.position.y *= this.properties.scale.y; rigid.position.z *= this.properties.scale.z; if (shape == 'box') { this.setCollider('box', {min: min, max: max}, rigid); } else if (shape == 'sphere') { this.setCollider('sphere', {radius: Math.max(max.x, max.y, max.z)}, rigid); } else if (shape == 'cylinder') { var radius = Math.max(max.x - min.x, max.z - min.z) / 2, height = max.y - min.y, pos = max.clone().add(min).multiplyScalar(.5); this.setCollider('cylinder', {radius: radius, height: height, offset: pos}, rigid); rigid.position.add(pos); // FIXME - rotate everything by 90 degrees on x axis to match default orientation var rot = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, -Math.PI/2, 0)); rigid.orientation.multiply(rot); } if (type == 'collider') { //console.log('add collider:', rigid, rigid.position.toArray(), rigid.orientation.toArray()); root.add(rigid); } else if (type == 'trigger') { var triggername = meshes[i].parent.name; var size = new THREE.Vector3().subVectors(max, min); /* size.x /= this.properties.scale.x; size.y /= this.properties.scale.y; size.z /= this.properties.scale.z; */ var quat = new THREE.Quaternion().multiplyQuaternions(obj.quaternion, rigid.orientation); var pos = rigid.position.clone().applyQuaternion(quat); /* pos.x /= this.properties.scale.x; pos.y /= this.properties.scale.y; pos.z /= this.properties.scale.z; */ this.triggers[triggername] = this.spawn('trigger', 'trigger_' + this.name + '_' + triggername, { position: pos, orientation: quat, shape: shape, size: size, scale: new THREE.Vector3(1 / this.properties.scale.x, 1 / this.properties.scale.y, 1 / this.properties.scale.z) }); } meshes[i].parent.remove(meshes[i]); meshes[i].position.copy(rigid.position); meshes[i].quaternion.copy(rigid.orientation); /* meshes[i].bindPosition(rigid.position); meshes[i].bindQuaternion(rigid.orientation); */ //meshes[i].bindScale(this.properties.scale); meshes[i].userData.thing = this; meshes[i].updateMatrix(); meshes[i].updateMatrixWorld(); //meshes[i].material = new THREE.MeshPhongMaterial({color: 0x999900, emissive: 0x666666, opacity: .5, transparent: true}); //this.colliders.add(meshes[i]); let collidercolor = 0x999999; if (rigid.mass === 0) { collidercolor = 0x990000; } meshes[i].material = new THREE.MeshPhongMaterial({color: 0x0000ff, opacity: .2, transparent: true, emissive: 0x552200, alphaTest: .1, depthTest: false, depthWrite: false}); /* this.colliderhelper = new THREE.EdgesHelper(meshes[i], 0x00ff00); this.colliders.add(this.colliderhelper); this.engine.systems.world.scene['colliders'].add(this.colliderhelper); meshes[i].updateMatrix(); meshes[i].updateMatrixWorld(); */ } if (this.objects.dynamics) { this.objects.dynamics.add(root); } /* new3d.scale.copy(obj.scale); new3d.position.copy(obj.