Skip to content

Instantly share code, notes, and snippets.

@agrancini-sc
Created April 23, 2025 16:00
Show Gist options
  • Save agrancini-sc/cf1f73a928342b28f2ff9b098c23cd29 to your computer and use it in GitHub Desktop.
Save agrancini-sc/cf1f73a928342b28f2ff9b098c23cd29 to your computer and use it in GitHub Desktop.
Directional Shadow
// 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