Skip to content

Shader Complexity Heatmap

The shaderCost view renders the scene with temporary bucketed override materials, then colorizes the result as a heatmap. Use it to find programs/materials that are likely more complex than their neighbors before you reach for a dedicated GPU profiler.

import {
DEFAULT_DEBUG_VIEWS,
createDebugRenderPlan,
createDebugPipelineRuntime,
createDebugViewUniforms,
resolveDebugViewLayout,
updateDebugViewUniforms,
} from "threejs-debug-view"
const shaderCostIndex = DEFAULT_DEBUG_VIEWS.findIndex((view) => view.source === "shaderCost")
const layout = resolveDebugViewLayout("single")
const plan = createDebugRenderPlan(DEFAULT_DEBUG_VIEWS, shaderCostIndex, layout)
const uniforms = createDebugViewUniforms()
const runtime = createDebugPipelineRuntime(scene, camera, plan, layout, renderer, uniforms)
// Each frame:
updateDebugViewUniforms(uniforms, plan.activePipelineView, layout, plan.pipelineViews.length, 1)
runtime.pipeline.render()

Full walkthrough: Native Runtime. Sample pixels with readHeatmapCostFromCanvas() from the root export.

import { DEFAULT_DEBUG_VIEWS } from "threejs-debug-view"
import { DebugViews } from "threejs-debug-view/r3f"
const shaderCostIndex = DEFAULT_DEBUG_VIEWS.findIndex((view) => view.source === "shaderCost")
<DebugViews
views={DEFAULT_DEBUG_VIEWS}
activeView={shaderCostIndex}
layout="single"
/>

The heatmap is normalized from 0 to 1:

  • Black: no cost signal.
  • Green: cheaper materials.
  • Yellow: moderate cost.
  • Red: high cost.
  • White: extreme end of the configured scale.

When the active layout includes shaderCost, the R3F DebugViewLayer shows a legend ramp and a prompt to click the viewport.

  • Click a pixel to place a centered crosshair on that screen point.
  • The legend marker moves to the decoded heatmap cost for that pixel.
  • Measured Overlap and Light Overlap keep their own legend ramps and do not share the shader-cost sampling UI.

This is a viewport inspector aid, not a GPU timestamp query or pass timer.

The scorer infers active program/material signals into shader-unit buckets that commonly produce heavier shader paths:

  • ALU proxy work from material/program profile
  • texture samples and dependent texture risk
  • texture resolution and bandwidth pressure
  • transparency and alpha testing
  • physical features such as transmission, clearcoat, sheen, and iridescence
  • custom ShaderMaterial signals based on uniform count

The raw signal count is normalized against a stable global signal scale and mapped into a bounded bucket material set. That keeps the override pass predictable instead of creating one material per mesh.

shaderCost can also be assigned to a viewport pane:

<DebugViews
views={DEFAULT_DEBUG_VIEWS}
viewportViews={[
{ view: "beauty", label: "Beauty" },
{ view: "shaderCost", label: "Shader Complexity", resolutionScale: 0.5 },
]}
layout="split-h"
paneCount={2}
showLabels
/>

Use a lower resolutionScale when the heatmap is a diagnostic companion rather than the primary view.

The view answers “which visible programs/materials look more complex?” It does not answer “how many milliseconds did this draw cost?” It also does not parse GLSL/WGSL/TSL source in this version. For timing, validate with browser GPU tooling, renderer stats, or a frame profiler.