content/hifi-content/thoys/production/voxel-painting/voxel-paint-tool.js
2022-02-14 02:04:11 +01:00

472 lines
20 KiB
JavaScript

//
// Created by Thijs Wenker on 4/3/2017
// Copyright 2017 High Fidelity, Inc.
//
// Revision of Seth Alves' work on VoxelPaint in 2016
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* global Entities, genericTool, Script, Vec3, Quat, textureIndexToURLs, paintBucketColors */
(function() {
Script.include('voxel-paint-shared.js?v14');
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};
VoxelPaintTool = function() {
_this = this;
_this.selectionSphere = false;
_this.slices = 20;
_this.voxelSize = 16;
_this.showPolyVoxes = false;
_this.toolRadius = 0.025;
_this.toolLength = 0.275;
_this.color = 0;
_this.tool = 0;
_this.lastUpdated = -1;
};
VoxelPaintTool.prototype = {
preload: function(id) {
_this.active = false;
_this.entityID = id;
_this.brushOverlay = null;
},
// 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.getToolEntity(), 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}
});
},
getToolEntity: function() {
var childEntities = Entities.getChildrenIDs(_this.entityID).filter(function(id) {
return !Overlays.isAddedOverlay(id) &&
AvatarList.getAvatarIdentifiers().indexOf(id) === -1 &&
MyAvatar.sessionUUID !== id;
});
return childEntities[0];
},
startEquip: function(id, params) {
_this.equipped = true;
_this.startEquipTime = Date.now();
_this.hand = params[0] === 'left' ? 0 : 1;
// FIXME: for some reason the brush gets re-equipped, which results in double overlays
// Remove existing child overlays
var childOverlays = Entities.getChildrenIDs(_this.entityID).filter(function(id) {
return Overlays.isAddedOverlay(id)
});
childOverlays.forEach(function(overlay) {
Overlays.deleteOverlay(overlay);
});
_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;
}
if (_this.canActivate === true && _this.triggerValue > 0.55) {
_this.canActivate = false;
_this.active = true;
}
// Need full trigger re-activation when the controller goes out of reach
if (_this.active && Vec3.length(_this.hand === 0 ? MyAvatar.getLeftHandPosition() :
MyAvatar.getRightHandPosition()) === 0.0) {
_this.active = false;
_this.canActivate = false;
}
},
continueEquip: function(id, params) {
var now = Date.now();
if (now - _this.lastUpdated >= LIFETIME_REFRESH_RATE) {
Entities.editEntity(id, {
lifetime: Entities.getEntityProperties(id, ['age'])['age'] + VOXEL_MANIPULATOR_LIFETIME
});
_this.lastUpdated = now;
}
_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, i;
if (toolType === 'brush') {
ids = _this.addPolyVoxIfNeeded(brushPosition, _this.toolRadius);
for (i = 0; i < ids.length; i++) {
Entities.setVoxelSphere(ids[i], brushPosition, _this.toolRadius, 255);
}
} else if (toolType === 'eraser') {
var searchRadius = 2.0;
ids = Entities.findEntities(brushPosition, searchRadius);
for (i = 0; i < ids.length; i++) {
Entities.setVoxelSphere(ids[i], 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 || parentID === _this.entityID) {
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) {
print('show selection sphere = ' + (showSelectionSphere ? '1' : '0'));
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) {
// Release the palette as well on tool drop:
Messages.sendLocalMessage('Hifi-Hand-Drop', 'both');
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 (brushPosition, editSphereRadius) {
// find all nearby entities
var searchRadius = 3.0;
var ids = Entities.findEntities(brushPosition, searchRadius);
// gather properties
var props = {};
for (var i = 0; i < ids.length; i++) {
var nearbyID = ids[i];
props[nearbyID] = Entities.getEntityProperties(nearbyID,
['type', 'name', 'localPosition',
'position', 'rotation', 'dimensions', 'userData']);
}
// find the aether
var aetherID = null;
for (i = 0; i < ids.length; i++) {
var possibleAetherID = ids[i];
if (props[possibleAetherID].name === "voxel paint aether") {
aetherID = possibleAetherID;
break;
}
}
if (!aetherID) {
return ids;
}
var aetherProps = props[aetherID];
var aetherDimensions = 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);
// find all the current polyvox entities
var withThisColorIDs = [];
this.polyvoxes = {};
for (i = 0; i < ids.length; i++) {
var possiblePolyVoxID = ids[i];
if (props[possiblePolyVoxID].name !== "voxel paint") {
continue;
}
var userData = JSON.parse(props[possiblePolyVoxID].userData);
var cFind = userData.color;
if (cFind !== this.color) {
continue;
}
var centerOffset = Vec3.sum(props[possiblePolyVoxID].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 brushOffset = Vec3.multiplyQbyV(Quat.inverse(aetherProps.rotation),
Vec3.subtract(brushPosition, aetherProps.position));
brushOffset = Vec3.sum(brushOffset, Vec3.multiply(aetherProps.dimensions, 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.86603; // 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));
this.dirtyNeighbors = {};
for (var x = lowX; x < highX; x++) {
for (var y = lowY; y < highY; y++) {
for (var z = lowZ; z < highZ; z++) {
if (Vec3.distance(Vec3.sum({x: x, y: y, z: z}, {x: 0.5, y: 0.5, z: 0.5}),
brushPosInVoxSpace) < sliceRezDistance) {
// if (Vec3.distance(Vec3.sum({x:x, y:y, z:z}, halfSliceSize), brushPosInVoxSpace) < sliceRezDistance) {
var newID = this.addPolyVox(x, y, z, this.color, sliceSize, aetherID, aetherProps.dimensions);
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, aetherID, 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]) {
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: 28800.0, // 8 hours
xTextureURL: xyzTextures[0],
yTextureURL: xyzTextures[1],
zTextureURL: xyzTextures[2],
userData: JSON.stringify({color: c}),
parentID: 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: aetherID
});
}
return this.polyvoxes[x][y][z][c];
}
};
return new VoxelPaintTool();
});