polygonjs-engine
Version:
node-based webgl 3D engine https://polygonjs.com
385 lines (365 loc) • 12.3 kB
text/typescript
/**
* Allows to detect when the mouse hovers over an object
*
*/
import {TypedEventNode} from './_Base';
import {NodeContext} from '../../poly/NodeContext';
import {BaseNodeType} from '../_Base';
import {BaseParamType} from '../../params/_Base';
import {VisibleIfParamOptions, ParamOptions} from '../../params/utils/OptionsController';
import {EventContext} from '../../scene/utils/events/_BaseEventsController';
import {RaycastCPUController} from './utils/raycast/CPUController';
import {CPUIntersectWith, CPU_INTERSECT_WITH_OPTIONS} from './utils/raycast/CpuConstants';
import {RaycastGPUController} from './utils/raycast/GPUController';
import {AttribType, ATTRIBUTE_TYPES, AttribTypeMenuEntries} from '../../../core/geometry/Constant';
import {EventConnectionPoint, EventConnectionPointType} from '../utils/io/connections/Event';
import {ParamType} from '../../poly/ParamType';
const TIMESTAMP = 1000.0 / 60.0;
enum RaycastMode {
CPU = 'cpu',
GPU = 'gpu',
}
const RAYCAST_MODES: Array<RaycastMode> = [RaycastMode.CPU, RaycastMode.GPU];
function visible_for_cpu(options: VisibleIfParamOptions = {}): ParamOptions {
options['mode'] = RAYCAST_MODES.indexOf(RaycastMode.CPU);
return {visibleIf: options};
}
function visible_for_cpu_geometry(options: VisibleIfParamOptions = {}): ParamOptions {
options['mode'] = RAYCAST_MODES.indexOf(RaycastMode.CPU);
options['intersectWith'] = CPU_INTERSECT_WITH_OPTIONS.indexOf(CPUIntersectWith.GEOMETRY);
return {visibleIf: options};
}
function visible_for_cpu_plane(options: VisibleIfParamOptions = {}): ParamOptions {
options['mode'] = RAYCAST_MODES.indexOf(RaycastMode.CPU);
options['intersectWith'] = CPU_INTERSECT_WITH_OPTIONS.indexOf(CPUIntersectWith.PLANE);
return {visibleIf: options};
}
function visible_for_gpu(options: VisibleIfParamOptions = {}): ParamOptions {
options['mode'] = RAYCAST_MODES.indexOf(RaycastMode.GPU);
return {visibleIf: options};
}
export enum TargetType {
SCENE_GRAPH = 'scene graph',
NODE = 'node',
}
export const TARGET_TYPES: TargetType[] = [TargetType.SCENE_GRAPH, TargetType.NODE];
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
class RaycastParamsConfig extends NodeParamsConfig {
/** @param defines if the ray detection is done on the CPU or GPU (GPU being currently experimental) */
mode = ParamConfig.INTEGER(RAYCAST_MODES.indexOf(RaycastMode.CPU), {
menu: {
entries: RAYCAST_MODES.map((name, value) => {
return {
name,
value,
};
}),
},
});
//
//
// COMMON
//
//
/** @param mouse coordinates (0,0) being the center of the screen, (-1,-1) being the bottom left corner and (1,1) being the top right corner */
mouse = ParamConfig.VECTOR2([0, 0], {cook: false});
/** @param by default the ray is sent from the current camera, but this allows to set another camera */
overrideCamera = ParamConfig.BOOLEAN(0);
/** @param by default the ray is sent from the current camera, but this allows to set a custom ray */
overrideRay = ParamConfig.BOOLEAN(0, {
visibleIf: {
mode: RAYCAST_MODES.indexOf(RaycastMode.CPU),
overrideCamera: 1,
},
});
/** @param the camera to override to */
camera = ParamConfig.OPERATOR_PATH('/perspective_camera1', {
nodeSelection: {
context: NodeContext.OBJ,
},
dependentOnFoundNode: false,
visibleIf: {
overrideCamera: 1,
overrideRay: 0,
},
});
/** @param the ray origin */
rayOrigin = ParamConfig.VECTOR3([0, 0, 0], {
visibleIf: {
overrideCamera: 1,
overrideRay: 1,
},
});
/** @param the ray direction */
rayDirection = ParamConfig.VECTOR3([0, 0, 1], {
visibleIf: {
overrideCamera: 1,
overrideRay: 1,
},
});
//
//
// GPU
//
//
/** @param the material to use on the scene for GPU detection */
material = ParamConfig.OPERATOR_PATH('/MAT/mesh_basic_builder1', {
nodeSelection: {
context: NodeContext.MAT,
},
dependentOnFoundNode: false,
callback: (node: BaseNodeType, param: BaseParamType) => {
RaycastGPUController.PARAM_CALLBACK_update_material(node as RaycastEventNode);
},
...visible_for_gpu(),
});
/** @param the current pixel value being read */
pixelValue = ParamConfig.VECTOR4([0, 0, 0, 0], {
cook: false,
...visible_for_gpu(),
});
/** @param the value threshold for which a hit is detected */
hitThreshold = ParamConfig.FLOAT(0.5, {
cook: false,
...visible_for_gpu(),
});
//
//
// CPU
//
//
/** @param defines the hit it tested against geometry or just a plane */
intersectWith = ParamConfig.INTEGER(CPU_INTERSECT_WITH_OPTIONS.indexOf(CPUIntersectWith.GEOMETRY), {
menu: {
entries: CPU_INTERSECT_WITH_OPTIONS.map((name, value) => {
return {name, value};
}),
},
...visible_for_cpu(),
});
/** @param threshold used to test hit with points */
pointsThreshold = ParamConfig.FLOAT(1, {
range: [0, 100],
rangeLocked: [true, false],
...visible_for_cpu(),
});
//
//
// CPU PLANE
//
//
/** @param plane direction if the hit is tested against a plane */
planeDirection = ParamConfig.VECTOR3([0, 1, 0], {
...visible_for_cpu_plane(),
});
/** @param plane offset if the hit is tested against a plane */
planeOffset = ParamConfig.FLOAT(0, {
...visible_for_cpu_plane(),
});
//
//
// CPU GEOMETRY
//
//
targetType = ParamConfig.INTEGER(0, {
menu: {
entries: TARGET_TYPES.map((name, value) => {
return {name, value};
}),
},
});
/** @param node whose objects to test hit against, when testing against geometries */
targetNode = ParamConfig.NODE_PATH('/geo1', {
nodeSelection: {
context: NodeContext.OBJ,
},
dependentOnFoundNode: false,
callback: (node: BaseNodeType, param: BaseParamType) => {
RaycastCPUController.PARAM_CALLBACK_update_target(node as RaycastEventNode);
},
...visible_for_cpu_geometry({targetType: TARGET_TYPES.indexOf(TargetType.NODE)}),
});
/** @param objects to test hit against, when testing against geometries */
objectMask = ParamConfig.STRING('*geo1*', {
callback: (node: BaseNodeType, param: BaseParamType) => {
RaycastCPUController.PARAM_CALLBACK_update_target(node as RaycastEventNode);
},
...visible_for_cpu_geometry({targetType: TARGET_TYPES.indexOf(TargetType.SCENE_GRAPH)}),
});
/** @param prints which objects are targeted by this node, for debugging */
printFoundObjectsFromMask = ParamConfig.BUTTON(null, {
callback: (node: BaseNodeType, param: BaseParamType) => {
RaycastCPUController.PARAM_CALLBACK_print_resolve(node as RaycastEventNode);
},
...visible_for_cpu_geometry({targetType: TARGET_TYPES.indexOf(TargetType.SCENE_GRAPH)}),
});
/** @param toggle to hit if tested against children */
traverseChildren = ParamConfig.BOOLEAN(0, {
callback: (node: BaseNodeType, param: BaseParamType) => {
RaycastCPUController.PARAM_CALLBACK_update_target(node as RaycastEventNode);
},
...visible_for_cpu_geometry(),
});
sep = ParamConfig.SEPARATOR(null, {
...visible_for_cpu_geometry(),
});
//
//
// POSITION (common between plane and geo intersection)
//
//
/** @param toggle on to set the param to the hit position */
tpositionTarget = ParamConfig.BOOLEAN(0, {
cook: false,
...visible_for_cpu(),
});
/** @param this will be set to the hit position */
position = ParamConfig.VECTOR3([0, 0, 0], {
cook: false,
...visible_for_cpu({tpositionTarget: 0}),
});
/** @param this parameter will be set to the hit position */
positionTarget = ParamConfig.OPERATOR_PATH('', {
cook: false,
...visible_for_cpu({tpositionTarget: 1}),
paramSelection: ParamType.VECTOR3,
computeOnDirty: true,
});
/** @param toggle on to set the param to the mouse velocity (experimental) */
tvelocity = ParamConfig.BOOLEAN(0, {
cook: false,
// callback: (node: BaseNodeType, param: BaseParamType) => {
// RaycastCPUVelocityController.PARAM_CALLBACK_update_timer(node as RaycastEventNode);
// },
});
/** @param toggle on to set the param to the mouse velocity */
tvelocityTarget = ParamConfig.BOOLEAN(0, {
cook: false,
...visible_for_cpu({tvelocity: 1}),
});
/** @param this will be set to the mouse velocity */
velocity = ParamConfig.VECTOR3([0, 0, 0], {
cook: false,
...visible_for_cpu({tvelocity: 1, tvelocityTarget: 0}),
});
/** @param this will be set to the mouse velocity */
velocityTarget = ParamConfig.OPERATOR_PATH('', {
cook: false,
...visible_for_cpu({tvelocity: 1, tvelocityTarget: 1}),
paramSelection: ParamType.VECTOR3,
computeOnDirty: true,
});
//
//
// GEO ATTRIB
//
//
/** @param for geometry hit tests, a vertex attribute can be read */
geoAttribute = ParamConfig.BOOLEAN(0, visible_for_cpu_geometry());
/** @param geometry vertex attribute to read */
geoAttributeName = ParamConfig.STRING('id', {
cook: false,
...visible_for_cpu_geometry({geoAttribute: 1}),
});
/** @param type of attribute */
geoAttributeType = ParamConfig.INTEGER(ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC), {
menu: {
entries: AttribTypeMenuEntries,
},
...visible_for_cpu_geometry({geoAttribute: 1}),
});
/** @param attribute value for float */
geoAttributeValue1 = ParamConfig.FLOAT(0, {
cook: false,
...visible_for_cpu_geometry({
geoAttribute: 1,
geoAttributeType: ATTRIBUTE_TYPES.indexOf(AttribType.NUMERIC),
}),
});
/** @param attribute value for string */
geoAttributeValues = ParamConfig.STRING('', {
...visible_for_cpu_geometry({
geoAttribute: 1,
geoAttributeType: ATTRIBUTE_TYPES.indexOf(AttribType.STRING),
}),
});
}
const ParamsConfig = new RaycastParamsConfig();
export class RaycastEventNode extends TypedEventNode<RaycastParamsConfig> {
params_config = ParamsConfig;
static type() {
return 'raycast';
}
static readonly OUTPUT_HIT = 'hit';
static readonly OUTPUT_MISS = 'miss';
public readonly cpu_controller: RaycastCPUController = new RaycastCPUController(this);
public readonly gpu_controller: RaycastGPUController = new RaycastGPUController(this);
initializeNode() {
this.io.inputs.setNamedInputConnectionPoints([
new EventConnectionPoint(
'trigger',
EventConnectionPointType.BASE,
this._process_trigger_event_throttled.bind(this)
),
new EventConnectionPoint('mouse', EventConnectionPointType.MOUSE, this._process_mouse_event.bind(this)),
new EventConnectionPoint(
'update_objects',
EventConnectionPointType.BASE,
this._process_trigger_update_objects.bind(this)
),
new EventConnectionPoint(
'trigger_vel_reset',
EventConnectionPointType.BASE,
this._process_trigger_vel_reset.bind(this)
),
]);
this.io.outputs.setNamedOutputConnectionPoints([
new EventConnectionPoint(RaycastEventNode.OUTPUT_HIT, EventConnectionPointType.BASE),
new EventConnectionPoint(RaycastEventNode.OUTPUT_MISS, EventConnectionPointType.BASE),
]);
}
trigger_hit(context: EventContext<MouseEvent>) {
this.dispatch_event_to_output(RaycastEventNode.OUTPUT_HIT, context);
}
trigger_miss(context: EventContext<MouseEvent>) {
this.dispatch_event_to_output(RaycastEventNode.OUTPUT_MISS, context);
}
private _process_mouse_event(context: EventContext<MouseEvent>) {
if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) {
this.cpu_controller.update_mouse(context);
} else {
this.gpu_controller.update_mouse(context);
}
}
private _last_event_processed_at = performance.now();
private _process_trigger_event_throttled(context: EventContext<MouseEvent>) {
const previous = this._last_event_processed_at;
const now = performance.now();
this._last_event_processed_at = now;
const delta = now - previous;
if (delta < TIMESTAMP) {
setTimeout(() => {
this._process_trigger_event(context);
}, TIMESTAMP - delta);
} else {
this._process_trigger_event(context);
}
}
private _process_trigger_event(context: EventContext<MouseEvent>) {
if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) {
this.cpu_controller.process_event(context);
} else {
this.gpu_controller.process_event(context);
}
}
private _process_trigger_update_objects(context: EventContext<MouseEvent>) {
if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) {
this.cpu_controller.update_target();
}
}
private _process_trigger_vel_reset(context: EventContext<MouseEvent>) {
if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) {
this.cpu_controller.velocity_controller.reset();
}
}
}