---
order: 2
title: Custom Post-Processing
---

In the post-processing system, the effect ([Effect](/apis/core/#PostProcessEffect)) is responsible for maintaining the data layer, and the pipeline ([Pass](/apis/core/#PostProcessPass)) is responsible for writing the rendering logic. In the pipeline, calling the [getBlendEffect](/apis/core/#PostProcessManager-getBlendEffect) method can get the final post-processing data after **global/local** mixing.

```mermaid
graph TD;
    subgraph Pass
        A1[Custom Pass ...]
        A2[Uber Pass]
        A3[Custom Pass ...]
    end

    subgraph Effect
        B1[Bloom Effect]
        B2[Tonemapping Effect]
        B3[Custom Effect]
    end

    A1 --> A2 --> A3
    B1 --> B2 --> B3

    A1 ---|Blend| B1
```

The engine has built-in [PostProcessUberPass](/apis/core/#PostProcessUberPass), which is used with [BloomEffect](/apis/core/#BloomEffect) and [TonemappingEffect](/apis/core/#TonemappingEffect) data. If you want to customize post-processing effects, we need to create a new Pass and then create an Effect based on whether the data needs to be fused.

## A Demo

Here we will simply implement a grayscale image post-processing effect~

<Comparison
  leftSrc="https://gw.alipayobjects.com/zos/OasisHub/f5d3ea3d-47a4-4618-b3d6-0ea46321e786/image-20250115162952605.png"
  leftText="Uber Pass"
  rightSrc="https://gw.alipayobjects.com/zos/OasisHub/75ccba72-b70b-49f6-812a-e00305e89201/image-20250115163026345.png"
  rightText="Uber Pass + Custom Pass"
/>

### 1. Add script

Let's create a script first. Next, we will write our custom post-processing Shader and Pass in this script file.

<Image src="https://gw.alipayobjects.com/zos/OasisHub/6df5fd1c-a24c-4e32-a62e-841b61fe76c7/image-20250124111456360.png" />

We refer to [Adding post-processing methods](/en/docs/graphics/postProcess/postProcess/#1-adding-a-post-processing-component) to add global or local post-processing components, and hang the script on this entity:

<Image src="https://gw.alipayobjects.com/zos/OasisHub/62a36df2-fdc5-4c13-9a32-c41f963d18b5/image-20250124112043221.png" />

### 2. Shader Writing

The algorithm is not special, but you need to pay attention to `renderer_BlitTexture` This built-in variable is the post-processing rendering result of the previous Pass. Here it is the result after Bloom and Tonemapping. We display this result in grayscale.

```glsl showLineNumbers filename="GrayScale.shader"
Shader "Custom/GrayScale" {
  SubShader "Default" {
    Pass "0" {
      DepthState = {
        Enabled = false;
        WriteEnabled = false;
      }

      VertexShader = vert;
      FragmentShader = frag;

      struct Attributes {
        vec4 POSITION_UV;
      };

      struct Varyings {
        vec2 v_uv;
      };

      sampler2D renderer_BlitTexture;

      Varyings vert(Attributes attr) {
        Varyings v;
        gl_Position = vec4(attr.POSITION_UV.xy, 0.0, 1.0);
        v.v_uv = attr.POSITION_UV.zw;
        return v;
      }

      void frag(Varyings v) {
        vec4 color = texture2D(renderer_BlitTexture, v.v_uv);
        float grayScale = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
        gl_FragColor = vec4(vec3(grayScale), 1.0);
      }
    }
  }
}
```

After creating the Galacean Shader source above (e.g. in the editor's `.shader` file), register it in code:

```ts filename="CustomPostProcessPass.ts"
Shader.create(grayScaleShaderSource); // Pass the Galacean Shader string shown above
const customShader = Shader.find("Custom/GrayScale");
```

### 3. Create a new Pass

We create a new Pass, directly [Blitter](/apis/core/#Blitter) to the screen in the [onRender hook](/apis/core/#PostProcessUberPass-onRender), and then add this Pass to the engine.

```ts showLineNumbers {6,10} filename="CustomPostProcessPass.ts"
class CustomPass extends PostProcessPass {
  private _blitMaterial: Material;

  constructor(engine: Engine) {
    super(engine);
    this._blitMaterial = new Material(this.engine, Shader.find("Custom/GrayScale"));
  }

  onRender(_, srcTexture: Texture2D, dst: RenderTarget): void {
    Blitter.blitTexture(this.engine, srcTexture, dst, undefined, undefined, this._blitMaterial, 0);
  }
}

const customPass = new CustomPass(engine);
engine.addPostProcessPass(customPass);
```

The execution order of Pass is executed after Uber Pass by default, that is [`PostProcessPassEvent.AfterUber`](/apis/core/#PostProcessPassEvent-AfterUber), we can also manually modify the execution order of the pipeline:

```ts filename="CustomPostProcessPass.ts"
customPass.event = PostProcessPassEvent.BeforeUber;
```

Whether the Pass is effective is determined by the Pass's [isActive](/apis/core/#PostProcessUberPass-isActive) by default. We can also modify the effectiveness logic, such as whether the intensity is greater than 0:

```ts showLineNumbers {2-9} filename="CustomPostProcessPass.ts"
class CustomPass extends PostProcessPass {
  override isValid(postProcessManager: PostProcessManager): boolean {
    if (!this.isActive) {
      return false;
    }

    const customEffectBlend = postProcessManager.getBlendEffect(CustomEffect);
    return customEffectBlend?.intensity > 0;
  }
}
```

### 4. Blend data

The above steps 2 and 3 can already customize the post-processing effect. Here is an advanced version of Blend data.

Take `intensity` as an example. We define a `CustomEffect`, which is specifically used to fuse intensity. The data to be fused is also very simple. The engine has encapsulated a series of post-processing parameters, such as [floating point type parameters](/apis/core/#PostProcessEffectFloatParameter).

```ts showLineNumbers {2,7} filename="CustomPostProcessPass.ts"
class CustomEffect extends PostProcessEffect {
  intensity = new PostProcessEffectFloatParameter(0.8);
}

// Add this effect to the post-processing component. postProcess can be created separately,
// or it can be the same as Bloom and other effects. It depends on the Blend requirements~
postProcess.addEffect(CustomEffect);
```

After defining the data, you need to change the `onRender` hook of the custom Pass to get the Blend data:

```ts showLineNumbers {3-6} filename="CustomPostProcessPass.ts"
class CustomPass extends PostProcessPass {
  onRender(camera: Camera, srcTexture: Texture2D, dst: RenderTarget): void {
    const postProcessManager = camera.scene.postProcessManager;
    const customEffectBlend = postProcessManager.getBlendEffect(CustomEffect);
    if (customEffectBlend) {
      this._blitMaterial.shaderData.setFloat("u_intensity", customEffectBlend.intensity.value);
    }

    Blitter.blitTexture(this.engine, srcTexture, dst, undefined, undefined, this._blitMaterial, 0);
  }
}
```

As you can see, we continuously set the fused intensity in the `onRender` hook of the custom pipeline, and then consume this data in the shader. We only need to add `u_intensity` uniform and update the fragment shader:

```glsl showLineNumbers {7,12} filename="GrayScale.shader"
      // ...same as above, with these changes in the Pass:

      float u_intensity;

      void frag(Varyings v) {
        vec4 color = texture2D(renderer_BlitTexture, v.v_uv);
        float grayScale = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
        gl_FragColor = vec4(mix(color.rgb, vec3(grayScale), u_intensity), 1.0);
      }
```

If your post-processing component is in local mode, we can also use [Blend Distance](/apis/core/#PostProcess-blendDistance) to set the distance at which the camera is close to the collision body to start blending:

<Image src="https://gw.alipayobjects.com/zos/OasisHub/76b084c1-f517-47d7-8067-7f838db002f8/2025-01-15%25252017.38.30.gif" />
