@kitware/vtk.js
Version:
Visualization Toolkit for the Web
348 lines (318 loc) • 12.8 kB
JavaScript
import { m as macro } from '../../macros2.js';
import vtkHardwareSelector from '../Core/HardwareSelector.js';
import vtkWebGPUBuffer from './Buffer.js';
import vtkWebGPUHardwareSelectionPass from './HardwareSelectionPass.js';
import vtkSelectionNode from '../../Common/DataModel/SelectionNode.js';
import vtkDataSet from '../../Common/DataModel/DataSet.js';
const {
SelectionContent,
SelectionField
} = vtkSelectionNode;
const {
FieldAssociations
} = vtkDataSet;
const {
vtkErrorMacro
} = macro;
function getInfoHash(info) {
return `${info.propID} ${info.compositeID}`;
}
function convert(xx, yy, buffdata, channel) {
const offset = ((buffdata.height - yy - 1) * buffdata.colorBufferWidth + xx) * 4 + channel;
return buffdata.colorValues[offset];
}
function getPixelInformationWithData(buffdata, inDisplayPosition, maxDistance, outSelectedPosition) {
// Base case
const maxDist = maxDistance < 0 ? 0 : maxDistance;
if (maxDist === 0) {
outSelectedPosition[0] = inDisplayPosition[0];
outSelectedPosition[1] = inDisplayPosition[1];
if (inDisplayPosition[0] < 0 || inDisplayPosition[0] >= buffdata.width || inDisplayPosition[1] < 0 || inDisplayPosition[1] >= buffdata.height) {
return null;
}
const actorid = convert(inDisplayPosition[0], inDisplayPosition[1], buffdata, 0);
if (actorid <= 0) {
// the pixel did not hit any actor.
return null;
}
const info = {};
info.propID = actorid;
let compositeID = convert(inDisplayPosition[0], inDisplayPosition[1], buffdata, 1);
if (compositeID < 0 || compositeID > 0xffffff) {
compositeID = 0;
}
info.compositeID = compositeID;
if (buffdata.captureZValues) {
const offset = (buffdata.height - inDisplayPosition[1] - 1) * buffdata.zbufferBufferWidth + inDisplayPosition[0];
info.zValue = buffdata.depthValues[offset];
info.zValue = buffdata.webGPURenderer.convertToOpenGLDepth(info.zValue);
info.displayPosition = inDisplayPosition;
}
return info;
}
// Iterate over successively growing boxes.
// They recursively call the base case to handle single pixels.
const dispPos = [inDisplayPosition[0], inDisplayPosition[1]];
const curPos = [0, 0];
let info = getPixelInformationWithData(buffdata, inDisplayPosition, 0, outSelectedPosition);
if (info) {
return info;
}
for (let dist = 1; dist < maxDist; ++dist) {
// Vertical sides of box.
for (let y = dispPos[1] > dist ? dispPos[1] - dist : 0; y <= dispPos[1] + dist; ++y) {
curPos[1] = y;
if (dispPos[0] >= dist) {
curPos[0] = dispPos[0] - dist;
info = getPixelInformationWithData(buffdata, curPos, 0, outSelectedPosition);
if (info) {
return info;
}
}
curPos[0] = dispPos[0] + dist;
info = getPixelInformationWithData(buffdata, curPos, 0, outSelectedPosition);
if (info) {
return info;
}
}
// Horizontal sides of box.
for (let x = dispPos[0] >= dist ? dispPos[0] - (dist - 1) : 0; x <= dispPos[0] + (dist - 1); ++x) {
curPos[0] = x;
if (dispPos[1] >= dist) {
curPos[1] = dispPos[1] - dist;
info = getPixelInformationWithData(buffdata, curPos, 0, outSelectedPosition);
if (info) {
return info;
}
}
curPos[1] = dispPos[1] + dist;
info = getPixelInformationWithData(buffdata, curPos, 0, outSelectedPosition);
if (info) {
return info;
}
}
}
// nothing hit.
outSelectedPosition[0] = inDisplayPosition[0];
outSelectedPosition[1] = inDisplayPosition[1];
return null;
}
//-----------------------------------------------------------------------------
function convertSelection(fieldassociation, dataMap, buffdata) {
const sel = [];
let count = 0;
dataMap.forEach((value, key) => {
const child = vtkSelectionNode.newInstance();
child.setContentType(SelectionContent.INDICES);
switch (fieldassociation) {
case FieldAssociations.FIELD_ASSOCIATION_CELLS:
child.setFieldType(SelectionField.CELL);
break;
case FieldAssociations.FIELD_ASSOCIATION_POINTS:
child.setFieldType(SelectionField.POINT);
break;
default:
vtkErrorMacro('Unknown field association');
}
child.getProperties().propID = value.info.propID;
const wprop = buffdata.webGPURenderer.getPropFromID(value.info.propID);
child.getProperties().prop = wprop.getRenderable();
child.getProperties().compositeID = value.info.compositeID;
child.getProperties().pixelCount = value.pixelCount;
if (buffdata.captureZValues) {
child.getProperties().displayPosition = [value.info.displayPosition[0], value.info.displayPosition[1], value.info.zValue];
child.getProperties().worldPosition = buffdata.webGPURenderWindow.displayToWorld(value.info.displayPosition[0], value.info.displayPosition[1], value.info.zValue, buffdata.renderer);
}
child.setSelectionList(value.attributeIDs);
sel[count] = child;
count++;
});
return sel;
}
//----------------------------------------------------------------------------
function generateSelectionWithData(buffdata, fx1, fy1, fx2, fy2) {
const x1 = Math.floor(fx1);
const y1 = Math.floor(fy1);
const x2 = Math.floor(fx2);
const y2 = Math.floor(fy2);
const dataMap = new Map();
const outSelectedPosition = [0, 0];
for (let yy = y1; yy <= y2; yy++) {
for (let xx = x1; xx <= x2; xx++) {
const pos = [xx, yy];
const info = getPixelInformationWithData(buffdata, pos, 0, outSelectedPosition);
if (info) {
const hash = getInfoHash(info);
if (!dataMap.has(hash)) {
dataMap.set(hash, {
info,
pixelCount: 1,
attributeIDs: [info.attributeID]
});
} else {
const dmv = dataMap.get(hash);
dmv.pixelCount++;
if (buffdata.captureZValues) {
if (info.zValue < dmv.info.zValue) {
dmv.info = info;
}
}
if (dmv.attributeIDs.indexOf(info.attributeID) === -1) {
dmv.attributeIDs.push(info.attributeID);
}
}
}
}
}
return convertSelection(buffdata.fieldAssociation, dataMap, buffdata);
}
// ----------------------------------------------------------------------------
// vtkWebGPUHardwareSelector methods
// ----------------------------------------------------------------------------
function vtkWebGPUHardwareSelector(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPUHardwareSelector');
//----------------------------------------------------------------------------
publicAPI.endSelection = () => {
model.WebGPURenderer.setSelector(null);
};
//----------------------------------------------------------------------------
// note we ignore the x,y arguments as WebGPU has to do buffer copies
// of the entire depth bufer. We could realloc hardware selection textures
// based on the passed in size etc but it gets messy so for now we always
// render the full size window and copy it to the buffers.
publicAPI.getSourceDataAsync = async renderer => {
if (!renderer || !model._WebGPURenderWindow) {
vtkErrorMacro('Renderer and view must be set before calling Select.');
return false;
}
// todo revisit making selection part of core
// then we can do this in core
model._WebGPURenderWindow.getRenderable().preRender();
if (!model._WebGPURenderWindow.getInitialized()) {
model._WebGPURenderWindow.initialize();
await new Promise(resolve => {
model._WebGPURenderWindow.onInitialized(resolve);
});
}
const webGPURenderer = model._WebGPURenderWindow.getViewNodeFor(renderer);
if (!webGPURenderer) {
return false;
}
// Initialize renderer for selection.
// change the renderer's background to black, which will indicate a miss
const originalSuppress = webGPURenderer.getSuppressClear();
webGPURenderer.setSuppressClear(true);
model._selectionPass.traverse(model._WebGPURenderWindow, webGPURenderer);
// restore original background
webGPURenderer.setSuppressClear(originalSuppress);
const device = model._WebGPURenderWindow.getDevice();
const texture = model._selectionPass.getColorTexture();
const depthTexture = model._selectionPass.getDepthTexture();
// as this is async we really don't want to store things in
// the class as multiple calls may start before resolving
// so anything specific to this request gets put into the
// result object (by value in most cases)
const result = {
area: [0, 0, texture.getWidth() - 1, texture.getHeight() - 1],
captureZValues: model.captureZValues,
fieldAssociation: model.fieldAssociation,
renderer,
webGPURenderer,
webGPURenderWindow: model._WebGPURenderWindow,
width: texture.getWidth(),
height: texture.getHeight()
};
// must be a multiple of 256 bytes, so 16 texels with rgba32uint
result.colorBufferWidth = 16 * Math.floor((result.width + 15) / 16);
result.colorBufferSizeInBytes = result.colorBufferWidth * result.height * 4 * 4;
const colorBuffer = vtkWebGPUBuffer.newInstance({
label: 'hardwareSelectColorBuffer'
});
colorBuffer.setDevice(device);
/* eslint-disable no-bitwise */
/* eslint-disable no-undef */
colorBuffer.create(result.colorBufferSizeInBytes, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
/* eslint-enable no-bitwise */
/* eslint-enable no-undef */
const cmdEnc = model._WebGPURenderWindow.getCommandEncoder();
cmdEnc.copyTextureToBuffer({
texture: texture.getHandle()
}, {
buffer: colorBuffer.getHandle(),
bytesPerRow: 16 * result.colorBufferWidth,
rowsPerImage: result.height
}, {
width: result.width,
height: result.height,
depthOrArrayLayers: 1
});
let zbuffer;
if (model.captureZValues) {
result.zbufferBufferWidth = 64 * Math.floor((result.width + 63) / 64);
zbuffer = vtkWebGPUBuffer.newInstance({
label: 'hardwareSelectDepthBuffer'
});
zbuffer.setDevice(device);
result.zbufferSizeInBytes = result.height * result.zbufferBufferWidth * 4;
/* eslint-disable no-bitwise */
/* eslint-disable no-undef */
zbuffer.create(result.zbufferSizeInBytes, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
/* eslint-enable no-bitwise */
/* eslint-enable no-undef */
cmdEnc.copyTextureToBuffer({
texture: depthTexture.getHandle(),
aspect: 'depth-only'
}, {
buffer: zbuffer.getHandle(),
bytesPerRow: 4 * result.zbufferBufferWidth,
rowsPerImage: result.height
}, {
width: result.width,
height: result.height,
depthOrArrayLayers: 1
});
}
device.submitCommandEncoder(cmdEnc);
/* eslint-disable no-undef */
const cLoad = colorBuffer.mapAsync(GPUMapMode.READ);
if (model.captureZValues) {
const zLoad = zbuffer.mapAsync(GPUMapMode.READ);
await Promise.all([cLoad, zLoad]);
result.depthValues = new Float32Array(zbuffer.getMappedRange().slice());
zbuffer.unmap();
} else {
await cLoad;
}
/* eslint-enable no-undef */
result.colorValues = new Uint32Array(colorBuffer.getMappedRange().slice());
colorBuffer.unmap();
result.generateSelection = (fx1, fy1, fx2, fy2) => generateSelectionWithData(result, fx1, fy1, fx2, fy2);
return result;
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
// WebGPURenderWindow: null,
};
// ----------------------------------------------------------------------------
function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Build VTK API
vtkHardwareSelector.extend(publicAPI, model, initialValues);
model._selectionPass = vtkWebGPUHardwareSelectionPass.newInstance();
macro.setGet(publicAPI, model, ['_WebGPURenderWindow']);
macro.moveToProtected(publicAPI, model, ['WebGPURenderWindow']);
// Object methods
vtkWebGPUHardwareSelector(publicAPI, model);
}
// ----------------------------------------------------------------------------
const newInstance = macro.newInstance(extend, 'vtkWebGPUHardwareSelector');
// ----------------------------------------------------------------------------
var vtkWebGPUHardwareSelector$1 = {
newInstance,
extend
};
export { vtkWebGPUHardwareSelector$1 as default, extend, newInstance };