Created
April 23, 2025 16:00
-
-
Save agrancini-sc/cf1f73a928342b28f2ff9b098c23cd29 to your computer and use it in GitHub Desktop.
Directional Shadow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import required modules | |
const WorldQueryModule = require("LensStudio:WorldQueryModule"); | |
const EPSILON = 0.01; | |
/** | |
* DirectionalWorldQuery | |
* | |
* A utility that performs world queries in a direction defined by two scene objects. | |
* It can then project rays from a third object in that same direction. | |
*/ | |
@component | |
export class DirectionalWorldQuery extends BaseScriptComponent { | |
// Direction definition inputs | |
@input | |
directionStart: SceneObject; | |
@input | |
directionEnd: SceneObject; | |
// Ray projection input | |
@input | |
rayStart: SceneObject; | |
// Object to position at hit location | |
@input | |
objectHitPoint: SceneObject; | |
// Ray length parameter | |
@input | |
rayLength: number = 100.0; | |
// Debug options | |
@input | |
debugEnabled: boolean = true; | |
// Whether to enable filtering for the hit test | |
@input | |
filterEnabled: boolean = true; | |
// Private properties | |
private hitTestSession: HitTestSession; | |
private direction: vec3; | |
private isDirectionSet: boolean = false; | |
/** | |
* Called when the script is initialized | |
*/ | |
onAwake() { | |
print("DirectionalWorldQuery: Initializing..."); | |
// Create new hit test session | |
this.hitTestSession = this.createHitTestSession(this.filterEnabled); | |
// Validate required inputs | |
if (!this.directionStart || !this.directionEnd || !this.rayStart) { | |
print("ERROR: Please set directionStart, directionEnd, and rayStart inputs"); | |
return; | |
} | |
if (!this.objectHitPoint) { | |
print("ERROR: Please set objectHitPoint input"); | |
return; | |
} | |
// Make sure the hit object has a visible component | |
const visual = this.objectHitPoint.getComponent("Component.RenderMeshVisual"); | |
if (!visual) { | |
print("WARNING: objectHitPoint does not have a RenderMeshVisual component. It may not be visible when placed."); | |
} | |
// Disable target object initially | |
// this.objectHitPoint.enabled = false; | |
// Create update event | |
this.createEvent("UpdateEvent").bind(this.onUpdate.bind(this)); | |
print("DirectionalWorldQuery: Initialization complete"); | |
} | |
/** | |
* Creates a hit test session with the specified options | |
*/ | |
createHitTestSession(filterEnabled) { | |
// Create hit test session with options | |
var options = HitTestSessionOptions.create(); | |
options.filter = filterEnabled; | |
var session = WorldQueryModule.createHitTestSessionWithOptions(options); | |
return session; | |
} | |
/** | |
* Updates the direction vector based on direction start and end objects | |
*/ | |
updateDirection() { | |
const startPos = this.directionStart.getTransform().getWorldPosition(); | |
const endPos = this.directionEnd.getTransform().getWorldPosition(); | |
// Calculate direction vector | |
this.direction = endPos.sub(startPos).normalize(); | |
this.isDirectionSet = true; | |
// IMPORTANT: Invert the direction to point downward if it's pointing upward | |
// This is needed because we want to cast the ray toward surfaces, not away from them | |
if (this.direction.y > 0) { | |
this.direction = new vec3( | |
-this.direction.x, | |
-this.direction.y, | |
-this.direction.z | |
); | |
print("Direction inverted to point downward"); | |
} | |
if (this.debugEnabled) { | |
print("Direction: " + this.direction.toString() + | |
" (from " + startPos.toString() + | |
" to " + endPos.toString() + ")"); | |
} | |
} | |
/** | |
* Handles hit test results | |
*/ | |
onHitTestResult(results) { | |
if (results === null) { | |
if (this.debugEnabled) { | |
print("DirectionalWorldQuery: No hit detected"); | |
} | |
this.objectHitPoint.enabled = false; | |
} else { | |
if (this.debugEnabled) { | |
print("DirectionalWorldQuery: Hit detected at " + results.position.toString()); | |
} | |
// Get hit information | |
const hitPosition = results.position; | |
const hitNormal = results.normal; | |
// Identify the direction the object should look at based on the normal of the hit location | |
var lookDirection; | |
if (1 - Math.abs(hitNormal.normalize().dot(vec3.up())) < EPSILON) { | |
lookDirection = vec3.forward(); | |
} else { | |
lookDirection = hitNormal.cross(vec3.up()); | |
} | |
// Calculate rotation | |
const toRotation = quat.lookAt(lookDirection, hitNormal); | |
// Set position and rotation | |
this.objectHitPoint.getTransform().setWorldPosition(hitPosition); | |
this.objectHitPoint.getTransform().setWorldRotation(toRotation); | |
// Make sure the object is enabled and visible | |
this.objectHitPoint.enabled = true; | |
if (this.debugEnabled) { | |
print("Hit position: " + hitPosition.toString()); | |
print("Hit normal: " + hitNormal.toString()); | |
print("Object placed at: " + this.objectHitPoint.getTransform().getWorldPosition().toString()); | |
} | |
} | |
} | |
/** | |
* Performs a world query from the ray start object in the direction defined by direction start and end objects | |
*/ | |
performWorldQuery() { | |
if (!this.isDirectionSet) { | |
this.updateDirection(); | |
} | |
const rayStart = this.rayStart.getTransform().getWorldPosition(); | |
// Calculate ray end point by extending the direction from the start point | |
const scaledDirection = new vec3( | |
this.direction.x * this.rayLength, | |
this.direction.y * this.rayLength, | |
this.direction.z * this.rayLength | |
); | |
const rayEnd = rayStart.add(scaledDirection); | |
if (this.debugEnabled) { | |
print("Ray: from " + rayStart.toString() + " to " + rayEnd.toString()); | |
} | |
// Try multiple ray lengths if no hit is detected | |
const tryRayLengths = [this.rayLength, this.rayLength * 0.5, this.rayLength * 0.1, this.rayLength * 2]; | |
let hitDetected = false; | |
// First try with the configured ray length | |
this.hitTestSession.hitTest(rayStart, rayEnd, (results) => { | |
if (results !== null) { | |
hitDetected = true; | |
this.onHitTestResult(results); | |
} else if (this.debugEnabled) { | |
print("No hit detected with primary ray length, trying alternatives..."); | |
} | |
}); | |
// If no hit was detected, try alternative ray lengths | |
if (!hitDetected) { | |
for (let i = 0; i < tryRayLengths.length && !hitDetected; i++) { | |
const length = tryRayLengths[i]; | |
if (length === this.rayLength) continue; // Skip the one we already tried | |
const altScaledDirection = new vec3( | |
this.direction.x * length, | |
this.direction.y * length, | |
this.direction.z * length | |
); | |
const altRayEnd = rayStart.add(altScaledDirection); | |
if (this.debugEnabled) { | |
print("Trying alternative ray length: " + length); | |
print("Alternative ray: from " + rayStart.toString() + " to " + altRayEnd.toString()); | |
} | |
this.hitTestSession.hitTest(rayStart, altRayEnd, this.onHitTestResult.bind(this)); | |
} | |
} | |
} | |
/** | |
* Called every frame | |
*/ | |
onUpdate() { | |
// Update direction in case objects have moved | |
this.updateDirection(); | |
// Perform world query | |
this.performWorldQuery(); | |
} | |
/** | |
* Clean up resources when the script is destroyed | |
*/ | |
onDestroy() { | |
// No need to clean up resources as we're not creating any dynamically | |
} | |
} | |
---------------- | |
/** | |
* ScaleBasedOnDistance.ts | |
* | |
* This utility scales an object based on the distance between two other objects. | |
*/ | |
@component | |
export class ScaleBasedOnDistance extends BaseScriptComponent { | |
// Store the last calculated scale to avoid unnecessary updates | |
private lastScale: number = -1; | |
// Define inputs using Lens Studio syntax | |
@input | |
public startObject: SceneObject; | |
@input | |
public endObject: SceneObject; | |
@input | |
public minScale: number = 0.5; | |
@input | |
public maxScale: number = 2.0; | |
@input | |
public objectToScale: SceneObject; | |
@input | |
public closestIsBigger: boolean = true; | |
@input | |
public minDistance: number = 0; // Minimum distance that corresponds to min/max scale | |
@input | |
public maxDistance: number = 100; // Maximum distance that corresponds to max/min scale | |
/** | |
* Initialize the script | |
*/ | |
private init(): void { | |
// Validate inputs | |
if (!this.startObject) { | |
print("Error: Start Object is not set"); | |
return; | |
} | |
if (!this.endObject) { | |
print("Error: End Object is not set"); | |
return; | |
} | |
if (!this.objectToScale) { | |
print("Error: Object To Scale is not set"); | |
return; | |
} | |
if (this.minScale > this.maxScale) { | |
print("Warning: Min Scale is greater than Max Scale. Swapping values."); | |
const temp = this.minScale; | |
this.minScale = this.maxScale; | |
this.maxScale = temp; | |
} | |
if (this.minDistance > this.maxDistance) { | |
print("Warning: Min Distance is greater than Max Distance. Swapping values."); | |
const temp = this.minDistance; | |
this.minDistance = this.maxDistance; | |
this.maxDistance = temp; | |
} | |
} | |
/** | |
* Called every frame | |
*/ | |
private update(): void { | |
if (!this.startObject || !this.endObject || !this.objectToScale) { | |
return; | |
} | |
// Calculate the distance between the two objects | |
const distance = this.calculateDistance(this.startObject, this.endObject); | |
// Calculate the scale based on the distance | |
const scale = this.calculateScale(distance); | |
// Only update if the scale has changed significantly | |
if (Math.abs(scale - this.lastScale) > 0.001) { | |
this.lastScale = scale; | |
// Apply the scale to the object using vec3 type | |
const uniformScale = new vec3(scale, scale, scale); | |
this.objectToScale.getTransform().setLocalScale(uniformScale); | |
} | |
} | |
/** | |
* Calculate the distance between two scene objects | |
* @param obj1 The first scene object | |
* @param obj2 The second scene object | |
* @returns The distance between the objects in world units | |
*/ | |
private calculateDistance(obj1: SceneObject, obj2: SceneObject): number { | |
const pos1 = obj1.getTransform().getWorldPosition(); | |
const pos2 = obj2.getTransform().getWorldPosition(); | |
// Calculate Euclidean distance | |
const dx = pos2.x - pos1.x; | |
const dy = pos2.y - pos1.y; | |
const dz = pos2.z - pos1.z; | |
return Math.sqrt(dx * dx + dy * dy + dz * dz); | |
} | |
/** | |
* Calculate the scale based on the distance | |
* @param distance The distance between the two objects | |
* @returns The calculated scale value | |
*/ | |
private calculateScale(distance: number): number { | |
// Clamp the distance to our defined range | |
const clampedDistance = Math.max(this.minDistance, Math.min(this.maxDistance, distance)); | |
// Calculate normalized position in the distance range (0 to 1) | |
const normalizedDistance = (clampedDistance - this.minDistance) / (this.maxDistance - this.minDistance); | |
// Apply linear interpolation between min and max scale | |
let scale: number; | |
if (this.closestIsBigger) { | |
// Closer distance = bigger scale (inverse relationship) | |
scale = this.maxScale - normalizedDistance * (this.maxScale - this.minScale); | |
} else { | |
// Closer distance = smaller scale (direct relationship) | |
scale = this.minScale + normalizedDistance * (this.maxScale - this.minScale); | |
} | |
// Since we already clamped the distance, the scale should be within bounds | |
return scale; | |
} | |
constructor() { | |
super(); | |
this.init(); | |
this.createEvent("UpdateEvent").bind(this.update.bind(this)); | |
} | |
} | |
------------------- | |
import {withAlpha, withoutAlpha} from "../SpectaclesInteractionKit/Utils/color" | |
import InteractorLineRenderer, {VisualStyle} from "../SpectaclesInteractionKit/Components/Interaction/InteractorLineVisual/InteractorLineRenderer" | |
/** | |
* This class provides visual representation for interactor lines. It allows customization of the line's material, colors, width, length, and visual style. The class integrates with the InteractionManager and WorldCameraFinderProvider to manage interactions and camera positioning. | |
*/ | |
@component | |
export class Line extends BaseScriptComponent { | |
@input | |
public startPointObject!: SceneObject | |
@input | |
public endPointObject!: SceneObject | |
@input | |
private lineMaterial!: Material | |
@input("vec3", "{1, 1, 0}") | |
@widget(new ColorWidget()) | |
public _beginColor: vec3 = new vec3(1, 1, 0) | |
@input("vec3", "{1, 1, 0}") | |
@widget(new ColorWidget()) | |
public _endColor: vec3 = new vec3(1, 1, 0) | |
@input | |
private lineWidth: number = 0.5 | |
@input | |
private lineLength: number = 160 | |
@input | |
@widget( | |
new ComboBoxWidget() | |
.addItem("Full", 0) | |
.addItem("Split", 1) | |
.addItem("FadedEnd", 2), | |
) | |
public lineStyle: number = 2 | |
@input | |
private shouldStick: boolean = true | |
private _enabled = true | |
private isShown = false | |
private defaultScale = new vec3(1, 1, 1) | |
private maxLength: number = 500 | |
private line!: InteractorLineRenderer | |
private transform!: Transform | |
/** | |
* Sets whether the visual can be shown, so developers can show/hide the ray in certain parts of their lens. | |
*/ | |
set isEnabled(isEnabled: boolean) { | |
this._enabled = isEnabled | |
} | |
/** | |
* Gets whether the visual is active (can be shown if hand is in frame and we're in far field targeting mode). | |
*/ | |
get isEnabled(): boolean { | |
return this._enabled | |
} | |
/** | |
* Sets how the visuals for the line drawer should be shown. | |
*/ | |
set visualStyle(style: VisualStyle) { | |
this.line.visualStyle = style | |
} | |
/** | |
* Gets the current visual style. | |
*/ | |
get visualStyle(): VisualStyle { | |
return this.line.visualStyle | |
} | |
/** | |
* Sets the color of the visual from the start. | |
*/ | |
set beginColor(color: vec3) { | |
this.line.startColor = withAlpha(color, 1) | |
} | |
/** | |
* Gets the color of the visual from the start. | |
*/ | |
get beginColor(): vec3 { | |
return withoutAlpha(this.line.startColor) | |
} | |
/** | |
* Sets the color of the visual from the end. | |
*/ | |
set endColor(color: vec3) { | |
this.line.endColor = withAlpha(color, 1) | |
} | |
/** | |
* Gets the color of the visual from the end. | |
*/ | |
get endColor(): vec3 { | |
return withoutAlpha(this.line.endColor) | |
} | |
onAwake() { | |
this.transform = this.sceneObject.getTransform() | |
this.defaultScale = this.transform.getWorldScale() | |
this.line = new InteractorLineRenderer({ | |
material: this.lineMaterial, | |
points: [this.startPointObject.getTransform().getLocalPosition(), | |
this.endPointObject.getTransform().getLocalPosition()], | |
startColor: withAlpha(this._beginColor, 1), | |
endColor: withAlpha(this._endColor, 1), | |
startWidth: this.lineWidth, | |
endWidth: this.lineWidth, | |
}) | |
this.line.getSceneObject().setParent(this.sceneObject) | |
if (this.lineStyle !== undefined) { | |
this.line.visualStyle = this.lineStyle | |
} | |
if (this.lineLength && this.lineLength > 0) { | |
this.defaultScale = new vec3(1, this.lineLength / this.maxLength, 1) | |
} | |
// Create update event to update the line on every frame | |
this.createEvent("UpdateEvent").bind(this.onUpdate.bind(this)) | |
} | |
/** | |
* Called every frame to update the line | |
*/ | |
onUpdate() { | |
if (!this.startPointObject || !this.endPointObject || !this.line) { | |
return | |
} | |
try { | |
// Update the line points based on the current positions of the start and end objects | |
this.line.points = [ | |
this.startPointObject.getTransform().getLocalPosition(), | |
this.endPointObject.getTransform().getLocalPosition() | |
] | |
} catch (e) { | |
print("Error updating line: " + e) | |
} | |
} | |
onDestroy(): void { | |
this.line.destroy() | |
this.sceneObject.destroy() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment