How Supersplat Implements WebGL Render Passes in `render.ts`
Supersplat orchestrates WebGL render passes through the registerRenderEvents function in src/render.ts, registering three command events—render.offscreen, render.image, and render.video—that leverage PlayCanvas's off-screen camera mode, pixel readback, and the WebCodecs API for high-performance capture and encoding.
Supersplat is a Gaussian splat editor built on the PlayCanvas engine within the playcanvas/supersplat repository. Its rendering pipeline isolates capture operations from the main viewport using sophisticated WebGL render passes that handle everything from raw pixel extraction to hardware-accelerated video encoding.
Off-Screen Rendering Architecture
The foundation of Supersplat's render pass system is off-screen rendering, which isolates capture operations from the on-screen display loop. When a render command is invoked, the system initializes a separate framebuffer using scene.camera.startOffscreenMode(width, height) defined in src/scene.ts. This method creates a detached render target that allows the pipeline to toggle overlays, gizmos, and clear colors independently without affecting the visible canvas.
After rendering completes, the system calls scene.camera.endOffscreenMode() to restore the default camera configuration. This architecture ensures that screenshot and video capture never interfere with the interactive viewport while maintaining full access to the scene's rendering context.
The Three Core Render Commands
The registerRenderEvents function in src/render.ts exposes three high-level commands that UI components can invoke via the event system. Each command represents a distinct render pass optimized for specific output formats.
render.offscreen: Raw Pixel Extraction
The render.offscreen command captures raw pixel data from the current view without compression. The implementation follows this sequence:
- Buffer Creation: Invokes
scene.camera.startOffscreenModeto allocate an off-screen framebuffer. - Forced Rendering: Sets
scene.forceRender = trueand awaits thepostrenderevent to ensure the frame is fully processed. - Pixel Readback: Copies the main render target to a work target using
scene.dataProcessor.copyRt(mainTarget, workTarget), then reads the color buffer into aUint8ArrayviaworkTarget.colorBuffer.read. - Coordinate Correction: Vertically flips the image buffer to convert from WebGL's bottom-left origin to the top-left origin expected by image formats.
This command returns the raw RGBA pixel buffer, making it ideal for further processing or custom encoding pipelines.
render.image: PNG Screenshot Generation
The render.image command builds upon the off-screen foundation to export compressed PNG screenshots. Located in the same src/render.ts file, this pass:
- Configures Transparency: Optionally disables the clear color when
transparentBgis enabled, allowing alpha channel preservation. - Compresses Output: Feeds the flipped pixel buffer into
PngCompressor.compressfromsrc/png-compressor.ts, which performs lossless compression. - Triggers Download: Generates a filename based on the current selection state and initiates a download via
downloadFile.
The command accepts parameters for resolution, transparency, and debug overlay visibility, providing a complete screenshot utility that runs entirely client-side.
render.video: Hardware-Accelerated Video Encoding
The most complex render pass, render.video, encodes frame sequences to MP4, WebM, MOV, or MKV containers using the WebCodecs API and the mediabunny library. This implementation:
- Initializes Encoder: Creates a
VideoEncoderinstance andOutputcontainer using lookup tablesFORMAT_CONFIGandCODEC_CONFIG(lines 13–27) to select appropriate codec and container formats based on user preferences. - Locks Render Loop: Sets
scene.lockedRenderMode = trueto prevent interference from the standard render cycle during batch processing. - Processes Frames Sequentially: For each frame:
- Advances the timeline via
events.invoke('plysequence.setFrameAsync')to load PLY sequence data. - Sorts splats when the camera moves using
instance.sortto maintain visual correctness. - Renders the frame, reads pixels, flips vertically, and creates a
VideoFrameobject fed to the encoder.
- Advances the timeline via
- Handles Back-Pressure: Monitors
encoder.encodeQueueSizeand recreates the encoder if codec reclamation occurs (lines 30–42). - Persists Tab State: Uses
navigator.locks.requestwhen available to keep the tab alive during long encoding sessions, preventing background throttling.
After processing all frames, the encoder flushes, finalizes the container, and triggers a download if a writable file stream wasn't provided.
Pixel Processing Pipeline
All three render passes share a common pixel processing stage implemented in src/render.ts. After the off-screen render completes, the system executes scene.dataProcessor.copyRt to transfer the rendered image from the main render target to a temporary work target. This copy operation is necessary because the main target may be bound to the canvas or other pipeline stages.
The readback operation workTarget.colorBuffer.read transfers GPU memory to CPU-accessible Uint8Array. Because WebGL coordinates originate at the bottom-left while PNG and video codecs expect top-left orientation, the code performs an in-place vertical flip of the buffer before passing it to compression or encoding functions.
Auxiliary Render Pass Integration
While src/render.ts handles high-level capture passes, Supersplat implements secondary rendering effects through SimpleRenderPass located in src/utils/simple-render-pass.ts. This lightweight wrapper is utilized by modules such as src/underlay.ts, src/outline.ts, and src/picker.ts to create specialized buffers for selection highlighting, outline effects, and picking operations without duplicating the off-screen logic.
These auxiliary passes demonstrate the modular architecture: core capture logic resides in render.ts, while effects and tools build reusable render pass objects that integrate with PlayCanvas's compositor.
Practical Implementation Examples
To capture a transparent PNG screenshot with debug overlays enabled:
await events.invoke('render.image', {
width: 1920,
height: 1080,
transparentBg: true,
showDebug: true
});
To record a 10-second MP4 video at 30fps using H.264 encoding:
const fileHandle = await window.showSaveFilePicker({
suggestedName: 'capture.mp4',
types: [{ accept: { 'video/mp4': ['.mp4'] } }]
});
const writable = await fileHandle.createWritable();
await events.invoke('render.video', {
startFrame: 0,
endFrame: 300,
frameRate: 30,
width: 1280,
height: 720,
bitrate: 5_000_000,
transparentBg: false,
showDebug: false,
format: 'mp4',
codec: 'h264'
}, writable);
await writable.close();
Summary
- Supersplat implements WebGL render passes in
src/render.tsthrough theregisterRenderEventsfunction, exposingrender.offscreen,render.image, andrender.videocommands. - Off-screen rendering uses
scene.camera.startOffscreenModeto isolate capture from the viewport, with pixel readback viacopyRtand vertical flipping for coordinate system alignment. - PNG export leverages
PngCompressorfromsrc/png-compressor.tsfor lossless compression with optional transparency. - Video encoding utilizes the WebCodecs API and mediabunny library, supporting multiple containers (MP4, WebM, MOV, MKV) and codecs (H.264, etc.) with back-pressure handling and Web Locks integration.
- Auxiliary effects use
SimpleRenderPassfromsrc/utils/simple-render-pass.tsfor modular secondary rendering operations.
Frequently Asked Questions
How does Supersplat handle coordinate system differences when reading WebGL buffers?
Supersplat performs an explicit vertical flip of the pixel buffer after reading from workTarget.colorBuffer.read because WebGL defines the origin (0,0) at the bottom-left corner, while PNG and video codecs expect the top-left origin. This transformation occurs before compression or encoding to ensure the output image displays correctly.
What is the purpose of scene.lockedRenderMode in video rendering?
The scene.lockedRenderMode = true flag prevents the standard PlayCanvas render loop from interfering during batch video encoding. This lock ensures that frame-by-frame rendering occurs deterministically without asynchronous updates or viewport changes that could corrupt the video sequence, particularly when advancing PLY sequence frames or sorting splats per camera movement.
Can Supersplat export video with transparency?
Yes, the render.video command supports transparent backgrounds by disabling the clear color when transparentBg is set to true, similar to the PNG capture mode. However, transparency support depends on the selected codec and container format—codecs like VP9 in WebM support alpha channels, while H.264 in MP4 does not.
Where does the PNG compression logic reside outside of render.ts?
The PNG compression implementation is located in src/png-compressor.ts and exported as the PngCompressor class. The render.image command imports this module and calls PngCompressor.compress to convert raw RGBA buffers into compressed ArrayBuffer data, keeping the compression algorithm separate from the render pass orchestration logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →