@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
273 lines (249 loc) • 7.82 kB
text/typescript
/**
* Creates an SDF texture which can be used inside the [mat/raymarchingBuilder](/doc/nodes/mat/raymarchingBuilder)
*
*
*/
import {Matrix4, Quaternion} from 'three';
import {ThreeMeshBVHHelper} from './../../../core/geometry/bvh/ThreeMeshBVHHelper';
import {MeshWithBVH} from './../../../core/geometry/bvh/three-mesh-bvh';
import {Vector3, Ray, Texture, Data3DTexture, DoubleSide, Box3, Mesh} from 'three';
import {NodeContext} from './../../poly/NodeContext';
import {TypedCopNode} from './_Base';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {HitPointInfo} from 'three-mesh-bvh';
import {addSDFMetadataToContainer, createSDFTexture} from '../../../core/loader/geometry/SDF';
import {CopType} from '../../poly/registers/nodes/types/Cop';
const _bbox = new Box3();
const _rayDir = new Vector3();
const _ray = new Ray();
const _bboxSize = new Vector3();
const _resolution = new Vector3();
const _voxelSizes = new Vector3();
const _padding = new Vector3();
const objectWorldMat = new Matrix4();
const objectWorldMatInverse = new Matrix4();
const t = new Vector3();
const q = new Quaternion();
const s = new Vector3();
interface FillTextureOptions {
resolution: Vector3;
bbox: Box3;
bboxSize: Vector3;
voxelSizes: Vector3;
}
class SDFFromObjectCopParamsConfig extends NodeParamsConfig {
/** @param which SOP node to import from */
geometry = ParamConfig.NODE_PATH('', {
nodeSelection: {
context: NodeContext.SOP,
},
});
/** @param voxelSize */
voxelSize = ParamConfig.FLOAT(0.1, {
range: [0.00001, 1],
rangeLocked: [true, false],
});
/** @param padding */
padding = ParamConfig.INTEGER(2, {
range: [0, 5],
rangeLocked: [true, false],
});
/** @param resolution */
resolution = ParamConfig.VECTOR3([-1, -1, -1], {
cook: false,
editable: false,
separatorBefore: true,
});
/** @param boundMin */
boundMin = ParamConfig.VECTOR3([-1, -1, -1], {
cook: false,
editable: false,
});
/** @param boundMax */
boundMax = ParamConfig.VECTOR3([1, 1, 1], {
cook: false,
editable: false,
});
}
const ParamsConfig = new SDFFromObjectCopParamsConfig();
export class SDFFromObjectCopNode extends TypedCopNode<SDFFromObjectCopParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return CopType.SDF_FROM_OBJECT;
}
override async cook(inputContents: Texture[]) {
const geometryNode = this.pv.geometry.nodeWithContext(NodeContext.SOP, this.states.error);
if (!geometryNode) {
this.states.error.set(`node not found at path '${this.pv.geometry.path()}'`);
return;
}
const container = await geometryNode.compute();
const coreGroup = container.coreContent();
const objects = coreGroup?.threejsObjects();
if (!(coreGroup && objects && objects.length)) {
this.states.error.set(`no objects found`);
return;
}
let objectWithGeo: MeshWithBVH | undefined;
for (const object of objects) {
if ((object as MeshWithBVH).geometry) {
objectWithGeo = objectWithGeo || (object as MeshWithBVH);
}
}
if (!objectWithGeo) {
for (const object of objects) {
object.traverse((childObject) => {
if ((childObject as MeshWithBVH).geometry) {
objectWithGeo = objectWithGeo || (childObject as MeshWithBVH);
}
});
}
}
if (!objectWithGeo) {
this.states.error.set(`no object found with a geometry`);
return;
}
const geometry = (objectWithGeo as MeshWithBVH).geometry;
if (!geometry) {
this.states.error.set(`no geometry found`);
return;
}
let boundsTree = geometry.boundsTree;
if (!boundsTree) {
ThreeMeshBVHHelper.assignDefaultBVHIfNone(objectWithGeo as Mesh);
boundsTree = geometry.boundsTree;
}
// update boundMin and BoundMax
coreGroup.boundingBox(_bbox);
const _updateResolution = () => {
_bbox.getSize(_bboxSize);
_resolution.copy(_bboxSize).divideScalar(this.pv.voxelSize);
_resolution.x = Math.ceil(_resolution.x * 0.5) * 2;
_resolution.y = Math.ceil(_resolution.y * 0.5) * 2;
_resolution.z = Math.ceil(_resolution.z * 0.5) * 2;
_voxelSizes.copy(_bboxSize).divide(_resolution);
};
// compute resolution before padding
_updateResolution();
// expand the bounding box
_padding.copy(_voxelSizes).multiplyScalar(this.pv.padding);
_bbox.expandByVector(_padding);
// recompute resolution after padding adjustment
_updateResolution();
// update params
this.scene().batchUpdates(() => {
this.p.boundMin.set(_bbox.min);
this.p.boundMax.set(_bbox.max);
this.p.resolution.set(_resolution);
});
// write texture data
const timeStart = performance.now();
objectWithGeo.updateMatrixWorld(true);
objectWorldMat.copy(objectWithGeo.matrixWorld);
objectWorldMatInverse.copy(objectWithGeo.matrixWorld).invert();
objectWorldMatInverse.decompose(t, q, s);
this.createTextureTargetIfRequired(_resolution);
const texture = this._fillTexture(objectWithGeo as MeshWithBVH, {
resolution: _resolution,
bbox: _bbox,
bboxSize: _bboxSize,
voxelSizes: _voxelSizes,
});
const totalTime = performance.now() - timeStart;
console.log('SDF generation time', totalTime);
if (texture) {
this.setTexture(texture);
} else {
this.cookController.endCook();
}
}
/*
*
* FILL TEXTURE
*
*/
_fillTexture(object: MeshWithBVH, options: FillTextureOptions) {
const {resolution, bbox, bboxSize, voxelSizes} = options;
const boundsTree = object.geometry.boundsTree;
const texture = this._dataTexture(resolution);
const data = texture.image.data;
const pos = new Vector3();
const distanceResult: HitPointInfo = {
point: new Vector3(),
distance: -1,
faceIndex: -1,
};
const resx = resolution.x;
const resy = resolution.y;
const resz = resolution.z;
const minx = bbox.min.x + voxelSizes.x * 0.5;
const miny = bbox.min.y + voxelSizes.y * 0.5;
const minz = bbox.min.z + voxelSizes.z * 0.5;
const sizex = bboxSize.x;
const sizey = bboxSize.y;
const sizez = bboxSize.z;
let i = 0;
for (let z = 0; z < resz; z++) {
for (let y = 0; y < resy; y++) {
for (let x = 0; x < resx; x++) {
// fit pos
pos.x = (x / resx) * sizex + minx;
pos.y = (y / resy) * sizey + miny;
pos.z = (z / resz) * sizez + minz;
boundsTree.closestPointToPoint(pos, distanceResult);
// check if we are inside
_rayDir.copy(distanceResult.point).sub(pos);
_ray.origin.copy(pos);
const res = boundsTree.raycastFirst(_ray, DoubleSide);
const inside = res && res.face && res.face.normal.dot(_ray.direction) > 0.0;
// TODO: get distance scale by object matrix
// _rayDir.applyMatrix4(objectWorldMat);
// const d = _rayDir.length();
const d = distanceResult.distance;
data[i] = inside ? -d : d;
i++;
}
}
}
addSDFMetadataToContainer(texture, {
boundMin: bbox.min,
boundMax: bbox.max,
resolution,
});
return texture;
}
/*
*
* CREATE TEXTURE
*
*/
private __dataTexture: Data3DTexture | undefined;
private _resolutionUsed: Vector3 = new Vector3(-1, -1, -1);
_dataTexture(resolution: Vector3) {
return (this.__dataTexture = this.__dataTexture || this._createTexture(resolution));
}
private createTextureTargetIfRequired(resolution: Vector3) {
if (!this.__dataTexture || !this._textureResolutionValid(resolution)) {
this.__dataTexture = this._createTexture(resolution);
this._resolutionUsed.copy(resolution);
}
}
private _textureResolutionValid(resolution: Vector3) {
if (this.__dataTexture) {
return resolution.equals(this._resolutionUsed);
} else {
return false;
}
}
private _createTexture(resolution: Vector3) {
return createSDFTexture(resolution.x, resolution.y, resolution.z);
}
/*
*
* CALLBACK
*
*/
// static PARAM_CALLBACK_render(node: SDFFromObjectCopNode) {
// node.setDirty();
// }
}