/* global Entities, Script, Vec3, Quat, Controller, VoxelPaintTool, Overlays, PALETTE_COLORS, VOXEL_TOOLS, MODELS_PATH */ (function() { Script.include(Script.resolvePath('voxel-paint-shared.js')); var _this; var NULL_UUID = '{00000000-0000-0000-0000-000000000000}'; var TRIGGER_CONTROLS = [ Controller.Standard.LT, Controller.Standard.RT ]; var RELOAD_THRESHOLD = 0.3; var SELECTION_SPHERE_DIMENSIONS = {x: 0.05, y: 0.05, z: 0.05}; // function AABoxTouchesSphere(center, radius, AAMin, AAMax) { // // Avro's algorithm from this paper: http://www.mrtc.mdh.se/projects/3Dgraphics/paperF.pdf // var e0 = { // x: Math.max(AAMin.x - center.x, 0), // y: Math.max(AAMin.y - center.y, 0), // z: Math.max(AAMin.z - center.z, 0) // }; // var e1 = { // x: Math.max(center.x - AAMax.x, 0), // y: Math.max(center.y - AAMax.y, 0), // z: Math.max(center.z - AAMax.z, 0) // }; // var e = Vec3.sum(e0, e1); // return Vec3.length(e) <= radius; // } VoxelPaintTool = function() { _this = this; _this.selectionSphere = false; // _this.slices = 20; // current // _this.voxelSize = 16; // current _this.slices = 10; _this.voxelSize = 26; // _this.slices = 6; // _this.voxelSize = 32; _this.showPolyVoxes = false; _this.toolRadius = 0.025; _this.toolLength = 0.275; _this.color = 0; _this.tool = 0; _this.toolEntity = null; _this.aetherID = null; _this.aetherProps = null; _this.propsCache = {}; }; VoxelPaintTool.prototype = { preload: function(id) { _this.active = false; _this.entityID = id; _this.brushOverlay = null; _this.previousBrushPositionSet = false; }, // remote called function setColor: function(id, params) { _this.color = params[0]; if (VOXEL_TOOLS[_this.tool].type === 'brush') { Overlays.editOverlay(_this.brushOverlay, { color: PALETTE_COLORS[_this.color].color }); } }, setTool: function(id, params) { _this.tool = params[0]; var voxelTool = VOXEL_TOOLS[_this.tool]; Entities.editEntity(_this.toolEntity, voxelTool.properties); _this.toolRadius = voxelTool.toolRadius; _this.toolLength = voxelTool.toolLength; if (voxelTool.type === 'brush') { Overlays.editOverlay(_this.brushOverlay, { color: PALETTE_COLORS[_this.color].color }); } else if (voxelTool.type === 'eraser') { Overlays.editOverlay(_this.brushOverlay, { color: {red: 0, blue: 0, green: 0} }); } Overlays.editOverlay(_this.brushOverlay, { dimensions: _this.showSelectionSphere ? SELECTION_SPHERE_DIMENSIONS : { x: _this.toolRadius * 2, y: _this.toolRadius * 2, z: _this.toolRadius * 2 }, localPosition: {x: 0, y: _this.toolLength, z: 0} }); }, startEquip: function(id, params) { _this.toolEntity = Entities.getChildrenIDs(id)[0]; _this.equipped = true; _this.startEquipTime = Date.now(); _this.hand = params[0] == 'left' ? 0 : 1; _this.brushOverlay = Overlays.addOverlay('sphere', { parentID: id, dimensions: {x: _this.toolRadius * 2, y: _this.toolRadius * 2, z: _this.toolRadius * 2}, localPosition: {x: 0, y: _this.toolLength, z: 0}, color: PALETTE_COLORS[_this.color].color, solid: true, alpha: 1.0, ignoreRayIntersection: true }); }, toggleWithTriggerPressure: function() { _this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[_this.hand]); if (_this.triggerValue < RELOAD_THRESHOLD) { _this.targetEntity = null; _this.targetAvatar = null; _this.canActivate = true; _this.active = false; _this.previousBrushPositionSet = false; } if (_this.canActivate === true && _this.triggerValue > 0.55) { _this.canActivate = false; _this.active = true; } }, continueEquip: function(id, params) { _this.toggleWithTriggerPressure(); var properties = Entities.getEntityProperties(id, ['position', 'rotation']); if (_this.active) { var brushPosition = Vec3.sum(Vec3.multiplyQbyV(properties.rotation, { x: 0, y: _this.toolLength, z: 0 }), properties.position); var toolType = VOXEL_TOOLS[_this.tool].type; var ids; if (toolType === 'brush') { if (!_this.previousBrushPositionSet) { _this.previousBrushPositionSet = true; _this.previousBrushPosition = brushPosition; } ids = _this.addPolyVoxIfNeeded(_this.previousBrushPosition, brushPosition, _this.toolRadius); for (var i = 0; i < ids.length; i++) { Entities.setVoxelCapsule(ids[i], _this.previousBrushPosition, brushPosition, _this.toolRadius, 255); } _this.previousBrushPosition = brushPosition; } else if (toolType === 'eraser') { var searchRadius = 2.0; ids = Entities.findEntities(brushPosition, searchRadius); for (var j = 0; j < ids.length; j++) { Entities.setVoxelSphere(ids[j], brushPosition, _this.toolRadius, 0); } } } else { var beamLength = _this.toolLength + _this.toolRadius; var pickRay = { origin: properties.position, direction: Quat.getUp(properties.rotation), length: _this.toolLength + _this.toolRadius }; var result = Overlays.findRayIntersection(pickRay); if (result.intersects && result.distance < beamLength) { var parentID = Overlays.getProperty(result.overlayID, 'parentID'); if (parentID === null || parentID === NULL_UUID) { return; } Entities.callEntityMethod(parentID, 'pointingAtOverlay', [JSON.stringify({ voxelPaintToolID: id, currentTool: _this.tool, currentColor: _this.color, selectedOverlayID: result.overlayID })]); _this.showSelectionSphere(true); } else { _this.showSelectionSphere(false); } } }, showSelectionSphere: function(showSelectionSphere) { if (_this.selectionSphere === showSelectionSphere) { return; } _this.selectionSphere = showSelectionSphere; if (_this.selectionSphere) { Overlays.editOverlay(_this.brushOverlay, { dimensions: SELECTION_SPHERE_DIMENSIONS }); return; } Overlays.editOverlay(_this.brushOverlay, { dimensions: {x: _this.toolRadius * 2, y: _this.toolRadius * 2, z: _this.toolRadius * 2} }); }, cleanup: function() { if (_this.brushOverlay !== null) { Overlays.deleteOverlay(_this.brushOverlay); _this.brushOverlay = null; } }, releaseEquip: function(id, params) { _this.cleanup(); // FIXME: it seems to release quickly while auto-attaching, added this to make sure it doesn't delete the // entity at that time var MIN_CLEANUP_TIME = 1000; if (_this.equipped && (Date.now() - _this.startEquipTime) > MIN_CLEANUP_TIME) { Entities.deleteEntity(_this.entityID); } }, unload: function() { _this.cleanup(); }, getPolyVox: function (x, y, z, c) { if (!this.polyvoxes) { return null; } if (!this.polyvoxes[x]) { return null; } if (!this.polyvoxes[x][y]) { return null; } if (!this.polyvoxes[x][y][z]) { return null; } return this.polyvoxes[x][y][z][c]; }, linkToNeighbors: function (x, y, z, c) { if (x < 0 || x >= this.slices || y < 0 || y >= this.slices || z < 0 || z >= this.slices) { return; } // link all the polyvoxes to their neighbors var polyvox = this.getPolyVox(x, y, z, c); if (polyvox) { var neighborProperties = {}; if (x > 0) { var xNNeighborID = this.getPolyVox(x - 1, y, z, c); if (xNNeighborID) { neighborProperties.xNNeighborID = xNNeighborID; } } if (x < this.slices - 1) { var xPNeighborID = this.getPolyVox(x + 1, y, z, c); if (xPNeighborID) { neighborProperties.xPNeighborID = xPNeighborID; } } if (y > 0) { var yNNeighborID = this.getPolyVox(x, y - 1, z, c); if (yNNeighborID) { neighborProperties.yNNeighborID = yNNeighborID; } } if (y < this.slices - 1) { var yPNeighborID = this.getPolyVox(x, y + 1, z, c); if (yPNeighborID) { neighborProperties.yPNeighborID = yPNeighborID; } } if (z > 0) { var zNNeighborID = this.getPolyVox(x, y, z - 1, c); if (zNNeighborID) { neighborProperties.zNNeighborID = zNNeighborID; } } if (z < this.slices - 1) { var zPNeighborID = this.getPolyVox(x, y, z + 1, c); if (zPNeighborID) { neighborProperties.zPNeighborID = zPNeighborID; } } Entities.editEntity(polyvox, neighborProperties); } }, clamp: function (v) { return Math.min(Math.max(v, 0), this.slices - 1); }, addPolyVoxIfNeeded: function (previousBrushPosition, brushPosition, editSphereRadius) { if (!this.aetherID) { var aetherSearchRadius = 3.0; var possibleAetherIDs = Entities.findEntities(brushPosition, aetherSearchRadius); for (var j = 0; j < possibleAetherIDs.length; j++) { var possibleAetherID = possibleAetherIDs[j]; var possibleAetherProps = Entities.getEntityProperties(possibleAetherID, ['name']); if (possibleAetherProps.name == "voxel paint aether") { this.aetherID = possibleAetherID; this.aetherProps = Entities.getEntityProperties(this.aetherID, ['position', 'rotation', 'dimensions']); break; } } } if (!this.aetherID) { print("error -- voxel-paint-tool can't find aether"); return Entities.findEntities(brushPosition, editSphereRadius); } var aetherDimensions = this.aetherProps.dimensions; var aetherHalfDimensions = Vec3.multiply(aetherDimensions, 0.5); var sliceSize = Vec3.multiply(aetherDimensions, 1.0 / this.slices); var halfSliceSize = Vec3.multiply(sliceSize, 0.5); var sliceRadius = Math.sqrt(Vec3.dot(halfSliceSize, halfSliceSize)); var capsuleLength = 2 * editSphereRadius + Vec3.length(Vec3.subtract(brushPosition, previousBrushPosition)); var capsuleCenter = Vec3.multiply(Vec3.sum(brushPosition, previousBrushPosition), 0.5); var ids = Entities.findEntities(capsuleCenter, capsuleLength + sliceRadius + 0.05); // find all the current polyvox entities var withThisColorIDs = []; this.polyvoxes = {}; for (var i = 0; i < ids.length; i++) { var possiblePolyVoxID = ids[i]; var props; if (possiblePolyVoxID in this.propsCache) { props = this.propsCache[possiblePolyVoxID]; } else { props = Entities.getEntityProperties(possiblePolyVoxID, ['name', 'localPosition', 'userData']); if (props.name != "voxel paint") { continue; } this.propsCache[possiblePolyVoxID] = props; } var userData = JSON.parse(props.userData); var cFind = userData.color; if (cFind != this.color) { continue; } var centerOffset = Vec3.sum(props.localPosition, aetherHalfDimensions); var lowCornerOffset = Vec3.subtract(centerOffset, halfSliceSize); var xFind = Math.round(lowCornerOffset.x / sliceSize.x); var yFind = Math.round(lowCornerOffset.y / sliceSize.y); var zFind = Math.round(lowCornerOffset.z / sliceSize.z); if (!this.polyvoxes[xFind]) { this.polyvoxes[xFind] = {}; } if (!this.polyvoxes[xFind][yFind]) { this.polyvoxes[xFind][yFind] = {}; } if (!this.polyvoxes[xFind][yFind][zFind]) { this.polyvoxes[xFind][yFind][zFind] = {}; } this.polyvoxes[xFind][yFind][zFind][cFind] = possiblePolyVoxID; withThisColorIDs.push(possiblePolyVoxID); } var previousBrushOffset = Vec3.multiplyQbyV(Quat.inverse(this.aetherProps.rotation), Vec3.subtract(this.previousBrushPosition, this.aetherProps.position)); previousBrushOffset = Vec3.sum(previousBrushOffset, Vec3.multiply(aetherDimensions, 0.5)); var previousBrushPosInVoxSpace = { x: previousBrushOffset.x / sliceSize.x, y: previousBrushOffset.y / sliceSize.y, z: previousBrushOffset.z / sliceSize.z }; var brushOffset = Vec3.multiplyQbyV(Quat.inverse(this.aetherProps.rotation), Vec3.subtract(brushPosition, this.aetherProps.position)); brushOffset = Vec3.sum(brushOffset, Vec3.multiply(aetherDimensions, 0.5)); var brushPosInVoxSpace = { x: brushOffset.x / sliceSize.x, y: brushOffset.y / sliceSize.y, z: brushOffset.z / sliceSize.z }; var radiusInVoxelSpace = editSphereRadius / sliceSize.x; var sliceHalfDiag = 0.866; // Vec3.length({x:0.5, y:0.5, z:0.5}); var sliceRezDistance = radiusInVoxelSpace + sliceHalfDiag; var lowX = this.clamp(Math.floor(brushPosInVoxSpace.x - sliceRezDistance)); var highX = this.clamp(Math.ceil(brushPosInVoxSpace.x + sliceRezDistance)); var lowY = this.clamp(Math.floor(brushPosInVoxSpace.y - sliceRezDistance)); var highY = this.clamp(Math.ceil(brushPosInVoxSpace.y + sliceRezDistance)); var lowZ = this.clamp(Math.floor(brushPosInVoxSpace.z - sliceRezDistance)); var highZ = this.clamp(Math.ceil(brushPosInVoxSpace.z + sliceRezDistance)); lowX = Math.min(lowX, this.clamp(Math.floor(previousBrushPosInVoxSpace.x - sliceRezDistance))); highX = Math.max(highX, this.clamp(Math.ceil(previousBrushPosInVoxSpace.x + sliceRezDistance))); lowY = Math.min(lowY, this.clamp(Math.floor(previousBrushPosInVoxSpace.y - sliceRezDistance))); highY = Math.max(highY, this.clamp(Math.ceil(previousBrushPosInVoxSpace.y + sliceRezDistance))); lowZ = Math.min(lowZ, this.clamp(Math.floor(previousBrushPosInVoxSpace.z - sliceRezDistance))); highZ = Math.max(highZ, this.clamp(Math.ceil(previousBrushPosInVoxSpace.z + sliceRezDistance))); this.dirtyNeighbors = {}; for (var x = lowX; x < highX; x++) { for (var y = lowY; y < highY; y++) { for (var z = lowZ; z < highZ; z++) { var touches; touches = Entities.AABoxIntersectsCapsule({x:x, y:y, z:z}, {x:1.0, y:1.0, z:1.0}, previousBrushPosInVoxSpace, brushPosInVoxSpace, radiusInVoxelSpace); if (touches) { var newID = this.addPolyVox(x, y, z, this.color, sliceSize, aetherDimensions); if (newID) { withThisColorIDs.push(newID); // keep track of which PolyVoxes need their neighbors hooked up for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { for (var dz = -1; dz <= 1; dz++) { this.dirtyNeighbors["" + (x+dx) + "," + (y+dy) + "," + (z+dz)] = true; } } } } } } } } for (var neighborKey in this.dirtyNeighbors) { if (this.dirtyNeighbors.hasOwnProperty(neighborKey)) { var xyz = neighborKey.split(","); var nX = parseInt(xyz[0]); var nY = parseInt(xyz[1]); var nZ = parseInt(xyz[2]); this.linkToNeighbors(nX, nY, nZ, this.color); } } return withThisColorIDs; }, addPolyVox: function (x, y, z, c, sliceSize, aetherDimensions) { if (!this.polyvoxes[x]) { this.polyvoxes[x] = {}; } if (!this.polyvoxes[x][y]) { this.polyvoxes[x][y] = {}; } if (!this.polyvoxes[x][y][z]) { this.polyvoxes[x][y][z] = {}; } if (this.polyvoxes[x][y][z][c]) { // we already have a polyvox for this position and color return null; } var halfSliceSize = Vec3.multiply(sliceSize, 0.5); var localPosition = Vec3.sum({x: x * sliceSize.x, y: y * sliceSize.y, z: z * sliceSize.z}, halfSliceSize); localPosition = Vec3.subtract(localPosition, Vec3.multiply(aetherDimensions, 0.5)); var xyzTextures = typeof PALETTE_COLORS[c].textures === 'string' ? [ PALETTE_COLORS[c].textures, PALETTE_COLORS[c].textures, PALETTE_COLORS[c].textures ] : PALETTE_COLORS[c].textures; this.polyvoxes[x][y][z][c] = Entities.addEntity({ type: "PolyVox", name: "voxel paint", // offset each color slightly to try to avoid z-buffer fighting localPosition: Vec3.sum(localPosition, Vec3.multiply({x: c, y: c, z: c}, 0.002)), dimensions: sliceSize, voxelVolumeSize: { x: this.voxelSize, y: this.voxelSize, z: this.voxelSize }, voxelSurfaceStyle: 0, collisionless: true, lifetime: 900, // 15 minutes xTextureURL: xyzTextures[0], yTextureURL: xyzTextures[1], zTextureURL: xyzTextures[2], userData: JSON.stringify({color: c}), parentID: this.aetherID }); if (this.showPolyVoxes) { Entities.addEntity({ name: "voxel paint debug cube", type: "Model", modelURL: MODELS_PATH + 'unitBoxTransparent.fbx', localPosition: localPosition, dimensions: sliceSize, collisionless: true, // lifetime: 60.0 parentID: this.aetherID }); } return this.polyvoxes[x][y][z][c]; } }; return new VoxelPaintTool(); });