Camera
A Camera is a device through which the player views the world.
| Type | Name | Interface Description |
|---|---|---|
| Variables | cameraType: CameraType | • Function: Gets the type of the camera, which can be either CameraType.Ortho or CameraType.Perspective. |
| Variables | clearColor: Color | • Function: The color used to clear this Camera's render target before drawing, when |
| Variables | clearType: CameraClearType | • Function: Gets the clear type of the current camera. |
| Variables | depthRenderTexture: Texture | • Function: Gets the depth render texture associated with the current camera. |
| Variables | far: number | • Function: The distance of the far clipping plane. Objects farther than this value are not rendered. |
| Variables | fov: number | • Function: The Camera's field of view in degrees. Only effective when is . range: [0.0, 180.0] default: 60.0 |
| Variables | fovType: CameraFovType | • Function: Gets the field of view type for the camera. |
| Variables | inputTexture: Texture | null | • Function: The texture blit to the render target as the base color at the start of each frame, before scene objects are rendered on top. Only takes effect when |
| Variables | near: number | • Function: The distance of the near clipping plane. Objects closer than this value are not rendered. |
| Variables | orthoHeight: number | • Function: The orthographic height of the camera if the camera type is orthographic. |
| Variables | projectionMatrix: Matrix4x4f | • Function: Get the projection matrix of the camera. |
| Variables | renderLayer: LayerSet | • Function: Gets the set of layers this Camera will render. |
| Variables | renderOrder: number | • Function: The sorting order the Camera renders in. Every frame, Cameras render in ascending order determined by their renderOrder properties. |
| Variables | renderTexture: Texture | • Function: The RenderTexture this camera renders scene objects into each frame. Defaults to the Final Render Output bound by the runtime; can be replaced with a custom RenderTexture via the setter. Its dimensions are not guaranteed to match |
| Variables | viewport: Rect | • Function: The camera's viewport, default is (0,0,1,1) |
| Functions | constructor() | |
| Functions | getCameraToWorldMatrix(): Matrix4x4f | • Function: Get the matrix that transforms from camera space to world space. Use this to calculate where in the world a specific camera space point is located. Returns The transformation matrix from camera space to world space. |
| Functions | getLookAt(): Vector3f | • Function: Gets the camera's forward direction, also known as the look-at direction. This method returns the normalized vector that the camera is currently looking toward (i.e., the default camera looks in the world-space direction of -Z in camera space, returns (0, 0, -1)). <br/>When constructing a follow-camera with , pass Returns The forward direction vector of the camera. |
| Functions | getWorldToCameraMatrix(): Matrix4x4f | • Function: Retrieves the matrix that transforms coordinates from world space to camera space. <br/>This matrix is commonly known as the "view matrix" in graphics literature. <br/>It can be used to determine the position of game objects in camera space or to specify a custom camera location independent of its transform. Returns The transformation matrix from world space to camera space. |
| Functions | getWorldToClipMatrix(): Matrix4x4f | • Function: Retrieves the transformation matrix that converts coordinates from world space to clip space. Returns The 4x4 matrix representing the world-to-clip space transformation. |
| Functions | ScreenPointToRay(screenPoint: Vector2f): Ray | • Function: Generates a ray from the screen position in this camera's view space. <br/>The screen coordinate system origin (0, 0) is at the bottom-left, with x increasing rightward and y increasing upward, measured in pixels. Parameters • Returns The calculated world space ray as a Ray object. |
| Functions | screenToWorldPoint(screenPoint: Vector3f): Vector3f | • Function: Converts a screen space position to a world space position, given an absolute depth. Parameters • Returns The corresponding world space position as a Vector3f. |
| Functions | viewportPointToRay(viewportPoint: Vector2f): Ray | • Function: Generates a ray from the specified position in this camera's viewport space. <br/>Viewport coordinates are normalized: (0, 0) is the bottom-left and (1, 1) is the top-right. Useful for generating rays from UI elements whose positions are in normalized layout space. Parameters • Returns The calculated world space ray as a Ray object. |
| Functions | viewportToWorldPoint(viewPortPoint: Vector3f): Vector3f | • Function: Converts a point from view port space to world space. <br/>Viewport coordinates are normalized: (0, 0) is the bottom-left and (1, 1) is the top-right. The Z component specifies the world-unit depth from the camera's near plane. Parameters • Returns The corresponding point in world space as a Vector3f. |
| Functions | worldToScreenPoint(worldPoint: Vector3f): Vector3f | • Function: Converts a world space position to a screen space position. Parameters • Returns The corresponding screen space position as a Vector3f. <br/>Screen position is based on the current rendered screen resolution W x H, <br/>ranging from ([0, W], [0, H]) where (0, 0) is the bottom-left and (W, H) is the top-right. <br/>The returned Z component is the distance from the camera's near plane in world units. |
| Functions | worldToViewportPoint(worldPoint: Vector3f): Vector3f | • Function: Projects a point from world space to viewport space. The returned viewport coordinates use a normalized range where Parameters • Returns The calculated point in viewport space as a Vector3f. |
Examples
constructor()
let obj = new APJS.Camera();
getLookAt(): Vector3f
const forward = cam.getLookAt();
const right = forward.cross(new APJS.Vector3f(0, 1, 0)).normalize();
ScreenPointToRay(screenPoint: Vector2f): Ray
// For a default camera at (0, 0, 40) and a default cube at (0, 0, 0)
// with box collider size (8, 8, 8), the following call logs (0, 0, 4).
// The ray to the center of the screen hits the front face of the cube first, and the front face is at z = 4.
const ray = cam.ScreenPointToRay(new APJS.Vector2f(360, 640));
const hits = APJS.Physics3D.raycast(ray, 500, true);
console.log("raycastHit:", hits[0].point);
screenToWorldPoint(screenPoint: Vector3f): Vector3f
// for default camera at (0, 0, 40), the following call returns (0, 0, 10)
// because (360, 640) is the center of the screen with resolution (1280, 720)
// and depth 30 is the depth from the near plane.
const worldPos = cam.screenToWorldPoint(new APJS.Vector3f(360, 640, 30));
viewportPointToRay(viewportPoint: Vector2f): Ray
// For a default camera at (0, 0, 40) and a default cube at (0, 0, 0)
// with box collider size (8, 8, 8), the following call logs (0, 0, 4).
// The ray to the center of the screen hits the front face of the cube first, and the front face is at z = 4.
const ray = cam.viewportPointToRay(new APJS.Vector2f(0.5, 0.5));
const hits = APJS.Physics3D.raycast(ray, 1000, true);
console.log("raycastHit:", hits[0].point);
viewportToWorldPoint(viewPortPoint: Vector3f): Vector3f
// for default camera at (0, 0, 40), the following call returns (0, 0, 30)
const worldPos = cam.viewportToWorldPoint(new APJS.Vector3f(0.5, 0.5, 10));
worldToScreenPoint(worldPoint: Vector3f): Vector3f
// for default camera at (0, 0, 40), the following call returns (360, 640, 30)
// because (0, 0, 10) is the center of the screen with resolution (1280, 720)
// and depth 30 is the depth from the near plane.
const screenPos = cam.worldToScreenPoint(new APJS.Vector3f(0, 0, 10));
// Use screenPoint.x and screenPoint.y to position a 2D UI indicator
const screenPointVec3f = cam.worldToScreenPoint(enemy.getTransform().getWorldPosition());
const screenPos = new APJS.Vector2f(screenPointVec3f.x, screenPointVec3f.y); // pixel coordinates
worldToViewportPoint(worldPoint: Vector3f): Vector3f
// for default camera at (0, 0, 40), the following call returns (0.5, 0.5, 10)
const viewportPos = cam.worldToViewportPoint(new APJS.Vector3f(0, 0, 30));
Use Case
Example 1 — AR plane-world tap-to-place pattern. The placement object lives under AR Plane (so it renders inside the AR Tracking render group, on top of the camera feed).
// AR plane-world tap-to-place + InteractableObject co-existence.
// Tap projection uses the DeviceTracker-backed AR Camera and intersects the Y=0 XOZ ground plane.
// The cube center is placed at the hit XZ plus PLACEMENT_Y, so the cube rests on the AR Plane.
// Tap is recognized on Touch.Ended only when the finger stayed within TAP_MOVE_THRESHOLD.
const GROUND_Y = 0;
const PLACEMENT_Y = 16;
const RAY_EPSILON = 0.0001;
const TAP_MOVE_THRESHOLD = 0.02;
@component()
export class TapPlaceController extends APJS.BasicScriptComponent {
@serializeProperty
placementCube!: APJS.SceneObject;
@serializeProperty
scoreText!: APJS.SceneObject;
private touchCallback!: (event: APJS.IEvent) => void;
private cubeTransform!: APJS.Transform;
private scoreTextComp!: APJS.Text;
private placementCamera!: APJS.Camera;
private hasPlacementCamera = false;
private placedCount = 0;
private touchActive = false;
private touchMoved = false;
private touchStartX = 0;
private touchStartY = 0;
private startCubePos: APJS.Vector3f | null = null;
// RecordStart: reset placedCount + return cube to its cached start position + reset score
// text + clear transient touch state. AR plane tracking is managed by DeviceTracker /
// ARCamera and resets automatically. See GameState §"RecordStart / RecordEnd Lifecycle".
private onRecordStart = (_event: APJS.IEvent) => {
this.placedCount = 0;
if (this.cubeTransform && this.startCubePos) {
this.cubeTransform.setWorldPosition(this.startCubePos);
}
if (this.scoreTextComp) {
this.scoreTextComp.text = "Score: 0";
}
this.touchActive = false;
this.touchMoved = false;
};
onStart(): void {
if (this.placementCube) {
this.cubeTransform = this.placementCube.getComponent("Transform") as APJS.Transform;
if (this.cubeTransform) {
const p = this.cubeTransform.getWorldPosition();
this.startCubePos = new APJS.Vector3f(p.x, p.y, p.z);
}
}
if (this.scoreText) {
this.scoreTextComp = this.scoreText.getComponent("Text") as APJS.Text;
}
this.resolvePlacementCamera();
this.touchCallback = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
if (t.phase === APJS.TouchPhase.Began) {
this.touchActive = true;
this.touchMoved = false;
this.touchStartX = t.position.x;
this.touchStartY = t.position.y;
return;
}
if (!this.touchActive) return;
if (t.phase === APJS.TouchPhase.Moved) {
this.touchMoved = this.touchMoved || this.touchDistanceSq(t.position.x, t.position.y) > TAP_MOVE_THRESHOLD * TAP_MOVE_THRESHOLD;
return;
}
if (t.phase === APJS.TouchPhase.Ended) {
const isTap = !this.touchMoved && this.touchDistanceSq(t.position.x, t.position.y) <= TAP_MOVE_THRESHOLD * TAP_MOVE_THRESHOLD;
this.touchActive = false;
this.touchMoved = false;
if (isTap) {
this.handleTap(t.position.x, t.position.y);
}
return;
}
if (t.phase === APJS.TouchPhase.Canceled) {
this.touchActive = false;
this.touchMoved = false;
}
};
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.touchCallback, this);
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.RecordStart, this.onRecordStart);
}
private touchDistanceSq(nx: number, ny: number): number {
const dx = nx - this.touchStartX;
const dy = ny - this.touchStartY;
return dx * dx + dy * dy;
}
private resolvePlacementCamera(): boolean {
if (this.hasPlacementCamera) {
return true;
}
const sceneObjects = this.getSceneObject().scene.getAllSceneObjects();
let fallbackCamera!: APJS.Camera;
for (let i = 0; i < sceneObjects.length; i += 1) {
const camera = sceneObjects[i].getComponent("Camera") as APJS.Camera;
if (!camera || camera.cameraType !== APJS.CameraType.Perspective) {
continue;
}
if (!fallbackCamera) {
fallbackCamera = camera;
}
if (sceneObjects[i].name === "AR Camera" || sceneObjects[i].getComponent("DeviceTracker")) {
this.placementCamera = camera;
this.hasPlacementCamera = true;
return true;
}
}
if (fallbackCamera) {
this.placementCamera = fallbackCamera;
this.hasPlacementCamera = true;
return true;
}
return false;
}
private handleTap(nx: number, ny: number): void {
if (!this.cubeTransform || !this.resolvePlacementCamera()) {
return;
}
const ray = this.placementCamera.viewportPointToRay(new APJS.Vector2f(nx, 1 - ny));
if (ray.direction.y > -RAY_EPSILON && ray.direction.y < RAY_EPSILON) {
return;
}
const t = (GROUND_Y - ray.origin.y) / ray.direction.y;
if (t <= RAY_EPSILON) {
return;
}
const hitX = ray.origin.x + ray.direction.x * t;
const hitZ = ray.origin.z + ray.direction.z * t;
const worldPoint = new APJS.Vector3f(hitX, PLACEMENT_Y, hitZ);
this.cubeTransform.setWorldPosition(worldPoint);
this.placedCount += 1;
if (this.scoreTextComp) {
this.scoreTextComp.text = "Score: " + this.placedCount;
}
}
onDestroy(): void {
if (this.touchCallback) {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.touchCallback, this);
}
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.RecordStart, this.onRecordStart);
}
}
Example 2 — Virtual joystick using ScreenTransform: detect tap on joystick top via worldToScreenPoint radius check, drag with clamped distance, output normalized direction.
@component()
export class JoystickInput extends APJS.BasicScriptComponent {
@serializeProperty
private uiCameraObject: APJS.SceneObject;
@serializeProperty
private joystickBase: APJS.SceneObject;
@serializeProperty
private joystickTop: APJS.SceneObject;
private uiCamera: APJS.Camera;
private baseTransform: APJS.ScreenTransform;
private topTransform: APJS.ScreenTransform;
private maxDistance: number = 100;
private active: boolean = false;
public direction: APJS.Vector2f = new APJS.Vector2f(0, 0);
onStart(): void {
this.uiCamera = this.uiCameraObject.getComponent("Camera") as APJS.Camera;
this.baseTransform = this.joystickBase.getComponent("ScreenTransform") as APJS.ScreenTransform;
this.topTransform = this.joystickTop.getComponent("ScreenTransform") as APJS.ScreenTransform;
const size = this.baseTransform.sizeDelta;
const topSize = this.topTransform.sizeDelta;
this.maxDistance = Math.min(size.x, size.y) / 2 - Math.min(topSize.x, topSize.y) / 2;
APJS.EventManager.getGlobalEmitter().on(APJS.EventType.Touch, this.onTouch);
}
private isInRadius(vp: APJS.Vector2f, st: APJS.ScreenTransform, radius: number): boolean {
const worldPos = st.getWorldPosition();
const screenPos = this.uiCamera.worldToScreenPoint(worldPos);
const origin = this.uiCamera.worldToScreenPoint(new APJS.Vector3f(0, 0, 0));
const sw = origin.x * 2;
const sh = origin.y * 2;
const dx = vp.x * sw - screenPos.x;
const dy = vp.y * sh - screenPos.y;
return Math.sqrt(dx * dx + dy * dy) <= radius;
}
private onTouch = (event: APJS.IEvent) => {
const t = event.args[0] as APJS.TouchData;
const vp = new APJS.Vector2f(t.position.x, 1 - t.position.y);
if (t.phase === APJS.TouchPhase.Began) {
this.active = this.isInRadius(vp, this.topTransform, this.maxDistance);
} else if (t.phase === APJS.TouchPhase.Moved && this.active) {
const baseWorld = this.baseTransform.getWorldPosition();
const baseScreen = this.uiCamera.worldToScreenPoint(baseWorld);
const origin = this.uiCamera.worldToScreenPoint(new APJS.Vector3f(0, 0, 0));
const sw = origin.x * 2;
const sh = origin.y * 2;
const dx = vp.x * sw - baseScreen.x;
const dy = vp.y * sh - baseScreen.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const clamped = Math.min(dist, this.maxDistance);
if (dist > 0) {
this.direction.set(dx / dist, dy / dist);
}
const topWorld = this.topTransform.getWorldPosition();
const topScreen = this.uiCamera.worldToScreenPoint(topWorld);
const newScreen = new APJS.Vector3f(baseScreen.x + this.direction.x * clamped, baseScreen.y + this.direction.y * clamped, topScreen.z);
this.topTransform.setWorldPosition(this.uiCamera.screenToWorldPoint(newScreen));
} else if (t.phase === APJS.TouchPhase.Ended && this.active) {
this.active = false;
this.direction.set(0, 0);
const baseWorld = this.baseTransform.getWorldPosition();
const topWorld = this.topTransform.getWorldPosition();
this.topTransform.setWorldPosition(new APJS.Vector3f(baseWorld.x, baseWorld.y, topWorld.z));
}
};
onDestroy(): void {
APJS.EventManager.getGlobalEmitter().off(APJS.EventType.Touch, this.onTouch);
}
}