aeronautic

Half resolution

Draw the plumes in their own pass at a share of the frame, depth-aware.

A raymarch costs per pixel, and a plume is soft enough to lose little at half the resolution. afterburner_pass draws the plumes in a pass of their own at a share of the frame, then lays them back over the full-resolution scene.

The upsample is weighted by depth: a low-resolution texel only lends its plume to pixels whose scene is at about the same depth. A nozzle's edge, or a wing in front of the flame, stays sharp.

Close up and astern of a fighter, it takes the worst case from 6.8 ms to 2.7 ms.

With React

Make the pass from the R3F scene and camera. Use its output in place of your scene pass, and hand the pass to the batch:

import { useFrame, useThree } from "@react-three/fiber";
import { afterburner_pass } from "@aeronautic/afterburner";
import { Afterburner, AfterburnerBatch } from "@aeronautic/afterburner/react";
import { useEffect, useMemo } from "react";
import { bloom } from "three/addons/tsl/display/BloomNode.js";
import { RenderPipeline, type WebGPURenderer } from "three/webgpu";

const Scene = () => {
  const { gl, scene, camera } = useThree();
  const renderer = gl as unknown as WebGPURenderer;

  const split = useMemo(
    () => afterburner_pass(scene, camera, { resolutionScale: 0.5 }),
    [scene, camera],
  );

  const pipeline = useMemo(() => {
    const pipeline = new RenderPipeline(renderer);

    // split.output is the scene with the plumes on it: bloom it, grade it…
    pipeline.outputNode = split.output.add(bloom(split.output, 0.45, 0.2, 1));

    return pipeline;
  }, [renderer, split]);

  useEffect(() => () => pipeline.dispose(), [pipeline]);
  useFrame(() => pipeline.render(), 1);

  return (
    <AfterburnerBatch preset="afterburner" pass={split}>
      <Afterburner />
    </AfterburnerBatch>
  );
};

The plumes must be in an <AfterburnerBatch pass={split}>. A nozzle outside one draws in the main scene as usual, at full resolution.

Options

optiondefaultwhat it is
resolutionScale0.5The plumes' resolution against the frame's. 0.5 is a quarter of the pixels.

What it returns

fieldwhat it is
outputThe scene with the plumes composited, at full resolution. Your pipeline's input.
sceneThe plumes' own scene. A batch given the pass puts its mesh here.
backdropThe opaque scene's colour and depth, for the plume material to read.
scenePassThe opaque scene at full resolution, without plumes.
plumePassThe plumes alone, premultiplied, at the reduced resolution.

Changing pass on a batch rebuilds it.

Without React

Give the core batch the backdrop, and add its mesh to the pass's scene rather than yours:

const split = afterburner_pass(scene, camera, { resolutionScale: 0.5 });

const batch = new AfterburnerBatch({
  preset: "afterburner",
  backdrop: split.backdrop,
});

split.scene.add(batch.mesh);

pipeline.outputNode = split.output;

When not to use it

  • A plume that only covers a few pixels gains little. The far tier is already one sample.
  • If you already render the whole frame at a reduced resolution, there is nothing to win.

On this page