Physics

Raycasting

Raycasting is the most fundamental spatial query feature in physics engines. A ray can be understood as an infinitely thin line emitted from a point in a specified direction in the 3D world. Raycasting is widely used in 3D applications and is the core tool for implementing precise point selection and collision detection.

Overview

Raycasting works by projecting an infinitely thin line to detect the first intersection with colliders in the scene, providing precise point-to-point detection capabilities.

(Image source: Internet)

Use Cases

Typical applications of raycasting in game development:

  • Object Picking - Pick objects in 3D scenes when users click the screen
  • Shooting Detection - Determine if bullets can hit targets in shooting games
  • Line of Sight Detection - Check visibility and occlusion relationships between objects
  • Ground Detection - Detect ground position beneath characters
  • Aiming Assistance - Implement precise aiming and targeting functionality

Basic Usage

Function Signature

raycast(
  ray: Ray,
  distance: number = Number.MAX_VALUE,
  layerMask: Layer = Layer.Everything,
  outHitResult?: HitResult
): boolean

Parameter Description

  • ray - Ray object defining origin point and direction
  • distance - Maximum detection distance for the ray (defaults to infinite)
  • layerMask - Layer mask for filtering specific collider layers
  • outHitResult - Optional output parameter storing detailed hit information

Usage Steps

Using raycasting requires the following steps:

  1. Import necessary modules such as Ray
  2. Create a ray (can be customized or generated through camera.screenPointToRay)
  3. Call the raycast method for detection

Usage Examples

Example 1: Basic Raycasting

import { WebGLEngine, HitResult, Ray, Vector3, Vector2 } from "@galacean/engine";
import { LitePhysics } from "@galacean/engine-physics-lite";
 
const engine = await WebGLEngine.create({
  canvas: "canvas",
  physics: new LitePhysics()
});
engine.canvas.resizeByClientSize();
const scene = engine.scenes[0];
 
// Create a ray from origin pointing downward
const ray = new Ray(new Vector3(0, 10, 0), new Vector3(0, -1, 0));
 
// Basic detection
if (scene.physics.raycast(ray)) {
  console.log("Ray hit an object!");
}

Example 2: Get Detailed Hit Information

// Create HitResult object to store hit information
const hitResult = new HitResult();
 
if (scene.physics.raycast(ray, Number.MAX_VALUE, Layer.Everything, hitResult)) {
  console.log(`Hit entity: ${hitResult.entity.name}`);
  console.log(`Hit distance: ${hitResult.distance}`);
  console.log(`Hit point: ${hitResult.point.toString()}`);
  console.log(`Hit normal: ${hitResult.normal.toString()}`);
  console.log(`Hit shape: ${hitResult.shape.constructor.name}`);
}

Example 3: Mouse Click Picking

// Convert screen input to Ray
document.getElementById("canvas").addEventListener("click", (e) => {
  const ratio = window.devicePixelRatio;
  const ray = new Ray();
  const screenPoint = new Vector2(e.offsetX * ratio, e.offsetY * ratio);
  
  // Convert screen coordinates to ray
  camera.screenPointToRay(screenPoint, ray);
  
  const hit = new HitResult();
  if (scene.physics.raycast(ray, Number.MAX_VALUE, Layer.Everything, hit)) {
    console.log(`Selected: ${hit.entity.name}`);
    selectObject(hit.entity);
  }
});

Example 4: Shooting Detection

// Shooting detection function
function fireBullet(startPosition: Vector3, direction: Vector3): Entity | null {
  const ray = new Ray(startPosition, direction);
  const hitResult = new HitResult();
  const maxRange = 100.0; // Range limit
  const targetLayer = Layer.Layer2 | Layer.Layer1;
  
  if (scene.physics.raycast(ray, maxRange, targetLayer, hitResult)) {
    // Hit target, apply damage or effects
    const target = hitResult.entity;
    const distance = hitResult.distance;
    
    console.log(`Hit target: ${target.name}, distance: ${distance.toFixed(2)}m`);
    
    // Create bullet hole or effects at hit point
    createImpactEffect(hitResult.point, hitResult.normal);
    
    return target;
  }
  
  return null; // No target hit
}

Example 5: Ground Detection

// Character ground detection
function checkGroundBelow(characterPosition: Vector3): number | null {
  const rayOrigin = characterPosition.clone().add(new Vector3(0, 0.1, 0)); // Start slightly higher
  const ray = new Ray(
    rayOrigin,
    new Vector3(0, -1, 0) // Detect downward
  );
  
  const hit = new HitResult();
  const maxDistance = 5.0; // Maximum detection distance
  
  if (scene.physics.raycast(ray, maxDistance, Layer.Layer1, hit)) {
    return hit.point.y; // Return ground height
  }
  
  return null; // No ground detected
}
 
// Usage example
const groundHeight = checkGroundBelow(player.transform.position);
if (groundHeight !== null) {
  console.log(`Ground height: ${groundHeight}`);
  // Adjust character position to ground
  player.transform.position.y = groundHeight;
}

Example 6: Line of Sight Detection

// Check if there's line of sight obstruction between two objects
function hasLineOfSight(from: Vector3, to: Vector3, obstacleLayer: Layer = Layer.Layer1): boolean {
  const direction = to.clone().subtract(from).normalize();
  const distance = Vector3.distance(from, to);
  const ray = new Ray(from, direction);
  
  // If ray hits obstacle within target distance, line of sight is blocked
  return !scene.physics.raycast(ray, distance, obstacleLayer);
}
 
// Usage example
const playerPos = player.transform.position;
const enemyPos = enemy.transform.position;
 
if (hasLineOfSight(playerPos, enemyPos)) {
  console.log("Enemy can see player");
  enemy.startChasing();
} else {
  console.log("Line of sight blocked");
}

Performance Optimization Tips

  1. Limit Detection Distance - Set reasonable maximum distances based on actual needs
  2. Use Layer Mask Filtering - Only check relevant collider layers through layerMask
  3. Reuse HitResult Objects - Avoid frequent creation of new HitResult instances
  4. Batch Processing - Process similar raycasts in batches for improved efficiency
// Performance optimization example
class RaycastManager {
  private static readonly _hitResult = new HitResult();
  private static readonly _commonRay = new Ray();
  
  // Quick ground detection
  static checkGround(position: Vector3, maxDistance: number = 2.0): number | null {
    this._commonRay.origin.copyFrom(position);
    this._commonRay.direction.set(0, -1, 0);
    
    if (scene.physics.raycast(
      this._commonRay, 
      maxDistance, 
      Layer.Layer1, 
      this._hitResult
    )) {
      return this._hitResult.point.y;
    }
    return null;
  }
  
  // Quick obstacle detection
  static checkObstacle(from: Vector3, to: Vector3): boolean {
    const direction = to.clone().subtract(from);
    const distance = direction.length();
    direction.normalize();
    
    this._commonRay.origin.copyFrom(from);
    this._commonRay.direction.copyFrom(direction);
    
    return scene.physics.raycast(this._commonRay, distance, Layer.Layer1);
  }
}

Important Notes

  1. Collider Requirements - Entity must add collider components to be detected by raycasting
  2. Ray Direction - Ray direction vector should be a normalized unit vector
  3. Same Distance Handling - When a ray hits multiple collider shapes at the same distance, it returns the Entity of the first added collider shape
  4. World Coordinate System - Ray origin and direction are based on world coordinates

Comparison with Other Query Methods

FeatureRaycastingShape CastingOverlap Detection
Detection PrecisionPoint precisionVolume precisionArea precision
Return ResultsFirst hitFirst hitAll overlapping
Use CasesPicking, aimingMovement predictionArea triggers
Performance CostLowMediumMedium
Implementation ComplexitySimpleMediumMedium

Integration with InputManager

It's recommended to use InputManager for handling input events, as it provides convenient input querying methods:

Method 1: Handling Input Events in Scripts

import { Script, PointerEventData, Ray, HitResult, Layer } from "@galacean/engine";
 
export class ClickDetectionScript extends Script {
  onPointerUp(eventData: PointerEventData): void {
    const camera = this.entity.scene.findEntityByName("Camera").getComponent(Camera);
    const ray = new Ray();
    
    // Use world position from event data
    camera.screenPointToRay(eventData.pointer.position, ray);
    
    const hit = new HitResult();
    if (this.entity.scene.physics.raycast(ray, 50, Layer.Layer0, hit)) {
      console.log(`Clicked: ${hit.entity.name}`);
      this.handleObjectClick(hit.entity);
    }
  }
  
  private handleObjectClick(entity: Entity): void {
    // Handle click logic
  }
}

Method 2: Polling InputManager State

import { Script, PointerButton, Ray, HitResult, Layer, Vector2 } from "@galacean/engine";
 
export class InputPollingScript extends Script {
  onUpdate(): void {
    const inputManager = this.engine.inputManager;
    
    // Check if left mouse button was released this frame
    if (inputManager.isPointerUp(PointerButton.Primary)) {
      // Get first pointer position
      const pointers = inputManager.pointers;
      if (pointers.length > 0) {
        const pointer = pointers[0];
        const camera = this.entity.scene.findEntityByName("Camera").getComponent(Camera);
        const ray = new Ray();
        
        camera.screenPointToRay(pointer.position, ray);
        
        const hit = new HitResult();
        if (this.entity.scene.physics.raycast(ray, 50, Layer.Layer0, hit)) {
          console.log(`Clicked: ${hit.entity.name}`);
          this.handleObjectClick(hit.entity);
        }
      }
    }
  }
  
  private handleObjectClick(entity: Entity): void {
    // Handle click logic
  }
}

Raycasting is the most commonly used query function in physics engines, providing an efficient solution for precise point-to-point detection. Mastering the proper use of raycasting is crucial for implementing high-quality interactive experiences.

Was this page helpful?