Skip to content

feat(rendering)!: add camera views and cull sprites outside them - #714

Merged
stormmuller merged 3 commits into
devfrom
claude/vigilant-rubin-7dc6d4
Oct 6, 2026
Merged

stormmuller merged 3 commits into
devfrom
claude/vigilant-rubin-7dc6d4

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

Implements design/camera-views.md.

Camera views (phase 1)

  • New computeCameraView(camera, position, renderContext) and getCameraView(world, camera, renderContext) (src/rendering/camera-view.ts). Each returns a CameraView:
    • bounds and size in world units, accounting for the camera's position and zoom;
    • pixelsPerUnit in CSS pixels;
    • worldToViewport and viewportToWorld, converting to and from CSS pixels from the canvas's top-left, Y-down. These invert each other exactly. The old worldToScreenSpace didn't flip Y.
  • The render system and the terrain system both build their projection from the view. createProjectionMatrix now takes the world-space Rect to show.
  • UI layout, the safe-area system and resolveCanvasPointerPosition all go through the canvas camera's view. As a result:
    • 'screenPixels' sizes and safe-area insets follow the camera's zoom, so they keep their on-screen size on a world-space canvas whose camera zooms;
    • a screen-space canvas's root rect fills its camera's view.
  • Removed: calculateVisibleWorldSize, calculatePixelsPerUnit, screenToWorldSpace, worldToScreenSpace, canvasToWorldSpace, and the unused CameraEcsComponent.scissorRect.
  • Migrated to the view: 38 docs-site demo files, /demo, physics/forces.md, ecs/game.md and the high-dpi-canvas e2e scene. rendering/world-units-and-cameras.md is rewritten around views.

View culling (phase 2)

  • The render system drops every sprite, nine-slice region and text glyph whose quad doesn't overlap the camera's view, before sorting and uploading.
  • One bounds function, computeSpriteInstanceBounds, sits next to bindSpriteInstanceData. A test checks it against the corners sprite.vert computes from the bound instance data, so the two can't drift apart.

Where this differs from the design (agreed with the author before implementing)

  • A render-target camera keeps the canvas's aspect. The present pass stretches every target over the canvas. Projecting at the target's own aspect would make any target that doesn't match the canvas look stretched on screen. Giving targets their own aspect belongs with render-resolution.md.
  • Text bounds aren't widened for outline or shadow. msdf-effects.frag draws both inside the same glyph quad, so culling the exact quads already keeps them.
  • Culling ships before sprite-textures.md, as decided. A custom vertex shader that moves vertices outside the sprite's quad may be culled while partly visible. The guide and the changelog both say so.

Performance (CPU, render system only, jsdom with a mocked GL, 20k sprites, 200 frames, two runs each)

Scenario Before After
All on screen 11.6 / 9.8 ms 13.2 / 11.9 ms
~1% on screen 10.8 / 8.0 ms 6.1 / 5.5 ms

Not done here

  • Rotated or scaled text places its glyphs wrongly: glyph-quad.ts adds each glyph's offset without rotating or scaling it. That's a pre-existing defect; culling uses the quads as they're drawn and the tests avoid locking it in.
  • The Galactic Journey demo's migration (Forge-Game-Engine/demo) follows on its own branch and needs a Forge release containing this.

Related issue(s)

Implements design/camera-views.md.

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (1786 tests)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes
  • Any new/changed public API is exported from the module's index.ts
  • Documentation under /documentation-site/docs/docs is updated
  • Demos updated and verified:
    • npm run build, then the docs site's typecheck and build;
    • every demo page loaded in Chromium with no page errors;
    • the raycasting demo's ray follows the mouse.
  • e2e: all 39 specs pass. That includes a new camera-pan-zoom test: after zooming and panning, the mouse is moved onto the rendered green square, and the scene checks it converts back inside that square in world space.

Changelog

  • Bullets added under ## [Unreleased] in CHANGELOG.md:
    • Added: camera views;
    • Changed: culling, createProjectionMatrix, UI zoom behavior;
    • Removed: the old helpers, with what to use instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GCQ9KLWEmo3mFAR7W2gMS7


Generated by Claude Code

A camera's view (the world area it shows, its pixels per unit and
conversions to and from the canvas in CSS pixels) is now computed in one
place, computeCameraView/getCameraView, instead of being repeated by the
render, terrain and UI systems and recomputed from pieces by game code.

- The render and terrain systems project each camera from its view;
  createProjectionMatrix takes the world-space rect to show.
- UI layout, safe area and pointer resolution use the canvas camera's
  view, so 'screenPixels' sizes follow the camera's zoom and a
  screen-space canvas fills its camera's view.
- The render system skips sprites, nine-slice regions and glyphs whose
  quads are outside a camera's view.
- calculateVisibleWorldSize, calculatePixelsPerUnit, screenToWorldSpace,
  worldToScreenSpace, canvasToWorldSpace and the unused
  CameraEcsComponent.scissorRect are removed; demos, docs, e2e scenes and
  /demo use the view.

A render-target camera keeps the canvas's aspect, since the present pass
stretches every target over the canvas; giving targets their own aspect
belongs with render-target sizing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCQ9KLWEmo3mFAR7W2gMS7
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.61905% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ui/systems/ui-safe-area-system.ts 75.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@stormmuller
stormmuller merged commit 9732db4 into dev Oct 6, 2026
13 checks passed
@stormmuller
stormmuller deleted the claude/vigilant-rubin-7dc6d4 branch October 6, 2026 17:51
stormmuller pushed a commit that referenced this pull request Oct 6, 2026
…j6xxo

Brings in camera views (#714). The game states demo sizes its play area
with getCameraView(...).size, since calculateVisibleWorldSize is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAAGHGoEyhYQhFcyTw37jE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants