UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

425 lines (405 loc) 13.6 kB
/** * 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 {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'; import {Poly} from '../../Poly'; import {EventContext} from '../../../core/event/EventContextType'; 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 defines if the mouse parameter is update when the cursor screen position changes */ tmouse = ParamConfig.BOOLEAN(1, {cook: false}); /** @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, visibleIf: {tmouse: 1}, }); /** @param by default the ray is sent from the current camera, but this allows to set another camera */ // overrideCamera = ParamConfig.BOOLEAN(0, visible_for_cpu()); /** @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.NODE_PATH('', { // nodeSelection: { // context: NodeContext.OBJ, // types: CAMERA_TYPES, // }, // dependentOnFoundNode: false, // ...visible_for_cpu({ // 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 */ overrideMaterial = ParamConfig.BOOLEAN(0, { callback: (node: BaseNodeType, param: BaseParamType) => { RaycastGPUController.PARAM_CALLBACK_updateMaterial(node as RaycastEventNode); }, ...visible_for_gpu(), }); /** @param the material to use on the scene for GPU detection */ material = ParamConfig.NODE_PATH('', { nodeSelection: { context: NodeContext.MAT, }, dependentOnFoundNode: false, computeOnDirty: true, callback: (node: BaseNodeType, param: BaseParamType) => { RaycastGPUController.PARAM_CALLBACK_updateMaterial(node as RaycastEventNode); }, ...visible_for_gpu({overrideMaterial: 1}), }); /** @param the current pixel color being read */ pixelColor = ParamConfig.COLOR([0, 0, 0], { cook: false, ...visible_for_gpu(), }); pixelAlpha = ParamConfig.FLOAT(0, { range: [0, 1], 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}; }), }, ...visible_for_cpu_geometry(), }); /** @param node whose objects to test hit against, when testing against geometries */ targetNode = ParamConfig.NODE_PATH('', { nodeSelection: { context: NodeContext.OBJ, }, dependentOnFoundNode: false, callback: (node: BaseNodeType, param: BaseParamType) => { RaycastCPUController.PARAM_CALLBACK_updateTarget(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_updateTarget(node as RaycastEventNode); }, objectMask: true, ...visible_for_cpu_geometry({targetType: TARGET_TYPES.indexOf(TargetType.SCENE_GRAPH)}), }); /** @param toggle to hit if tested against children */ traverseChildren = ParamConfig.BOOLEAN(true, { callback: (node: BaseNodeType, param: BaseParamType) => { RaycastCPUController.PARAM_CALLBACK_updateTarget(node as RaycastEventNode); }, ...visible_for_cpu_geometry(), separatorAfter: true, }); // // // POSITION (common between plane and geo intersection) // // /** @param toggle on to update hit position */ tposition = ParamConfig.BOOLEAN(1, { cook: false, ...visible_for_cpu(), }); /** @param toggle on to set the param to the hit position */ tpositionTarget = ParamConfig.BOOLEAN(0, { cook: false, ...visible_for_cpu({tposition: 1}), }); /** @param this will be set to the hit position */ position = ParamConfig.VECTOR3([0, 0, 0], { cook: false, ...visible_for_cpu({tposition: 1, tpositionTarget: 0}), }); /** @param this parameter will be set to the hit position */ positionTarget = ParamConfig.PARAM_PATH('', { // positionTarget param should not be dependent // on found Param, otherwise, as soon as the target param is change, // this param would have to cook dependentOnFoundParam: false, cook: false, ...visible_for_cpu({tposition: 1, 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, ...visible_for_cpu(), // 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.PARAM_PATH('', { dependentOnFoundParam: false, 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> { override paramsConfig = ParamsConfig; static override type() { return 'raycast'; } static readonly INPUT_TRIGGER = 'trigger'; static readonly INPUT_MOUSE = 'mouse'; static readonly INPUT_UPDATE_OBJECTS = 'updateObjects'; static readonly INPUT_TRIGGER_VEL_RESET = 'triggerVelReset'; static readonly OUTPUT_HIT = 'hit'; static readonly OUTPUT_MISS = 'miss'; public readonly cpuController: RaycastCPUController = new RaycastCPUController(this); public readonly gpuController: RaycastGPUController = new RaycastGPUController(this); override initializeNode() { this.io.inputs.setNamedInputConnectionPoints([ new EventConnectionPoint( RaycastEventNode.INPUT_TRIGGER, EventConnectionPointType.BASE, this._processTriggerEventThrottled.bind(this) ), new EventConnectionPoint( RaycastEventNode.INPUT_MOUSE, EventConnectionPointType.MOUSE, this._processMouseEvent.bind(this) ), new EventConnectionPoint( RaycastEventNode.INPUT_UPDATE_OBJECTS, EventConnectionPointType.BASE, this._processTriggerUpdateObjects.bind(this) ), new EventConnectionPoint( RaycastEventNode.INPUT_TRIGGER_VEL_RESET, EventConnectionPointType.BASE, this._processTriggerVelReset.bind(this) ), ]); this.io.outputs.setNamedOutputConnectionPoints([ new EventConnectionPoint(RaycastEventNode.OUTPUT_HIT, EventConnectionPointType.BASE), new EventConnectionPoint(RaycastEventNode.OUTPUT_MISS, EventConnectionPointType.BASE), ]); } triggerHit(context: EventContext<MouseEvent>) { this.dispatchEventToOutput(RaycastEventNode.OUTPUT_HIT, context); } triggerMiss(context: EventContext<MouseEvent>) { this.dispatchEventToOutput(RaycastEventNode.OUTPUT_MISS, context); } private _processMouseEvent(context: EventContext<MouseEvent>) { if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) { this.cpuController.updateMouse(context); } else { this.gpuController.updateMouse(context); } } private _lastEventProcessedAt = -1; private _processTriggerEventThrottled(context: EventContext<MouseEvent>) { const now = Poly.performance.performanceManager().now(); const getDelta = (now: number) => { const previous = this._lastEventProcessedAt; const delta = now - previous; return delta; }; const delta = getDelta(now); if (delta < TIMESTAMP) { setTimeout(() => { const delta = getDelta(now); if (delta < TIMESTAMP) { this._processTriggerEvent(context); } }, TIMESTAMP - delta); } else { this._processTriggerEvent(context); } } private _processTriggerEvent(context: EventContext<MouseEvent>) { this._processMouseEvent(context); this._lastEventProcessedAt = Poly.performance.performanceManager().now(); if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) { this.cpuController.processEvent(context); } else { this.gpuController.processEvent(context); } } private _processTriggerUpdateObjects(context: EventContext<MouseEvent>) { if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) { this.cpuController.updateTarget(); } } private _processTriggerVelReset(context: EventContext<MouseEvent>) { if (this.pv.mode == RAYCAST_MODES.indexOf(RaycastMode.CPU)) { this.cpuController.velocityController.reset(); } } }