AppleyAppley Docs
Docs/Optimization

Custom Render Pipelines

Build custom render pipelines for post-processing, deferred rendering, and effects.

35 min read Advanced Updated 2026-07-01 v2.0 ~150 min to complete

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

This is part of the Advanced Documentation collection. Category: Optimization. Estimated completion: 150 minutes. Difficulty: Advanced/Professional.

Screenshot

Custom Render Pipelines
Custom Render Pipelines

How Professional Games Do It

1

U

n

2

F

o

3

S

e

4

C

r

5

I

m

6

C

h

7

U

s

8

F

o

9

O

p

10

B

l

11

H

a

12

R

e

FluxScript Implementation

Here is how to build this system in Appley using FluxScript (Appley Scripting):

Custom Render Pipelines — FluxScript
// 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:

Custom Render Pipelines — Node Graph
[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.