Custom Render Pipelines
Build custom render pipelines for post-processing, deferred rendering, and effects.
Introduction
AAA games use custom render pipelines for bloom, SSAO, motion blur, and deferred lighting. This guide explains how professional engines compose render passes, use frame buffers, and apply post-processing — then builds a custom pipeline in Appley.
Advanced Guide 21 of 25
Screenshot
How Professional Games Do It
U
n
F
o
S
e
C
r
I
m
C
h
U
s
F
o
O
p
B
l
H
a
R
e
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Custom render pipeline with deferred lighting + post-processing
on start {
// Create G-buffer render targets (deferred rendering)
gbuffer = {
albedo: renderTarget.create(screen.width, screen.height, { format: "rgba8", name: "gbuffer_albedo" }),
normals: renderTarget.create(screen.width, screen.height, { format: "rgba16f", name: "gbuffer_normals" }),
position: renderTarget.create(screen.width, screen.height, { format: "rgba16f", name: "gbuffer_position" }),
depth: renderTarget.create(screen.width, screen.height, { format: "depth32", name: "gbuffer_depth" })
}
// Post-processing targets (half resolution for performance)
halfRes = renderTarget.create(screen.width / 2, screen.height / 2, { format: "rgba16f" })
bloomTarget = renderTarget.create(screen.width / 4, screen.height / 4, { format: "rgba16f" })
ssaoTarget = renderTarget.create(screen.width / 2, screen.height / 2, { format: "r8" })
// Final HDR composite target
hdrComposite = renderTarget.create(screen.width, screen.height, { format: "rgba16f" })
}
// Custom render pipeline — called every frame
func renderPipeline() {
// === PASS 1: Geometry -> G-Buffer ===
renderTarget.setMultiple([gbuffer.albedo, gbuffer.normals, gbuffer.position])
renderTarget.setDepth(gbuffer.depth)
renderTarget.clearAll()
for obj in scene.getVisibleObjects() {
// Shader writes albedo, normal, position to MRT
obj.renderWithShader("gbuffer_fill")
}
// === PASS 2: Deferred Lighting ===
renderTarget.activate(hdrComposite)
renderTarget.clear(color(0, 0, 0, 0))
// Full-screen quad with lighting shader
fullscreenQuad.renderWithShader("deferred_light", {
albedoTex: gbuffer.albedo,
normalTex: gbuffer.normals,
positionTex: gbuffer.position,
depthTex: gbuffer.depth,
lights: scene.getLights(),
ambientColor: scene.getAmbientColor()
})
// === PASS 3: SSAO (half resolution) ===
renderTarget.activate(ssaoTarget)
fullscreenQuad.renderWithShader("ssao", {
normalTex: gbuffer.normals,
positionTex: gbuffer.position,
sampleRadius: 0.5,
samples: 16
})
// Blur SSAO (separate horizontal + vertical for performance)
blurTarget = renderTarget.create(screen.width / 2, screen.height / 2, { format: "r8" })
renderTarget.activate(blurTarget)
fullscreenQuad.renderWithShader("blur_horizontal", { source: ssaoTarget, radius: 4 })
renderTarget.activate(ssaoTarget)
fullscreenQuad.renderWithShader("blur_vertical", { source: blurTarget, radius: 4 })
// Apply SSAO to composite
renderTarget.activate(hdrComposite)
fullscreenQuad.renderWithShader("apply_ssao", { source: hdrComposite, ssao: ssaoTarget })
// === PASS 4: Bloom (quarter resolution) ===
// Extract bright areas
renderTarget.activate(bloomTarget)
fullscreenQuad.renderWithShader("bloom_extract", { source: hdrComposite, threshold: 1.0 })
// Blur bright areas (multiple passes for wide bloom)
for i = 0 to 3 {
tempTarget = renderTarget.create(screen.width / 4, screen.height / 4, { format: "rgba16f" })
renderTarget.activate(tempTarget)
fullscreenQuad.renderWithShader("blur_gaussian", { source: bloomTarget, radius: 8 })
renderTarget.activate(bloomTarget)
fullscreenQuad.renderWithShader("blur_gaussian", { source: tempTarget, radius: 8 })
}
// Add bloom to composite
renderTarget.activate(hdrComposite)
fullscreenQuad.renderWithShader("add_bloom", { source: hdrComposite, bloom: bloomTarget, intensity: 0.5 })
// === PASS 5: Post-processing chain ===
// Motion blur (uses velocity buffer from previous frame)
fullscreenQuad.renderWithShader("motion_blur", { source: hdrComposite, velocity: velocityBuffer, intensity: 0.3 })
// Color grading (LUT)
fullscreenQuad.renderWithShader("color_grade", { source: hdrComposite, lut: "filmic_lut" })
// Vignette
fullscreenQuad.renderWithShader("vignette", { source: hdrComposite, intensity: 0.4 })
// === PASS 6: Tone mapping (HDR -> LDR) ===
renderTarget.activate(screen) // Final output to screen
fullscreenQuad.renderWithShader("tone_map_aces", { source: hdrComposite, exposure: 1.2 })
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[Render Pipeline — 6 Passes]
PASS 1: Geometry -> G-Buffer
--> [Set: MRT (albedo + normals + position + depth)]
--> [For Each: visible object]
--> [Render: with gbuffer_fill shader]
PASS 2: Deferred Lighting
--> [Activate: HDR composite target]
--> [Fullscreen Quad: deferred_light shader]
--> [Read: G-buffer textures + lights array]
--> [Compute: per-pixel lighting]
PASS 3: SSAO (half-res)
--> [Fullscreen Quad: ssao shader (16 samples)]
--> [Blur: horizontal + vertical (separated)]
--> [Apply: SSAO to HDR composite]
PASS 4: Bloom (quarter-res)
--> [Extract: bright areas > threshold]
--> [Blur: 4 iterations of Gaussian blur]
--> [Add: bloom to composite]
PASS 5: Post-Processing Chain
--> [Motion Blur: from velocity buffer]
--> [Color Grade: LUT lookup]
--> [Vignette: edge darkening]
PASS 6: Tone Mapping
--> [ACES: HDR -> LDR with exposure]
--> [Output: to screen]Professional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Use half resolution for SSAO and quarter for bloom. SSAO at half-res is visually identical to full-res but 4x cheaper. Bloom at quarter-res is fine because it is inherently blurry. This saves enormous GPU time.
- Use deferred rendering for many lights. Forward rendering handles 2-3 lights per object efficiently. Deferred handles 50+ lights because lighting is computed in screen space, not per-object. Choose deferred for lit scenes with many light sources.
- Use separated Gaussian blurs. A 9-tap 2D blur is 81 taps. Separated into horizontal (9 taps) + vertical (9 taps) = 18 taps. Same result, 4.5x faster. Always separate 2D blurs.
- Use ACES tone mapping. ACES preserves color saturation in highlights better than Reinhard. It is the film industry standard and used by most modern games. Set exposure to control overall brightness.
Common Mistakes
Full-resolution post-processing
SSAO and bloom at full resolution are 4-16x more expensive than needed. Use half or quarter resolution. The visual difference is negligible — these effects are inherently low-frequency.
Forward rendering with many lights
Forward rendering computes lighting per object per light. 50 lights * 100 objects = 5000 lighting computations. Deferred does 50 lights * 1 million pixels = manageable. Use deferred for multi-light scenes.
