React/Pixi Rendering a Filter Texture over a sprite container throws off alignment
08:24 25 Feb 2026

I'm fairly new to PIXI and Shaders and although I have reached the effect I want but I'm struggling with a PIXI Renderer scale issue.

The image shows a CSS grid, over the top of a PIXI canvas. In the canvas I'm rendering sprites that do line up with the CSS grid perfectly. But when I apply my Shader Texture to the Sprit Container, the texture that gets rendered back to the canvas is scaled up and cropped.

I've not found any helping solution online and AI hasnt even found me an answer. Has anyone got an idea of how to fix this?

enter image description here

Project is in Next/React with Pixi v8

Layout.tsx

import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Pixel Demo",
  description: "Next.js + Pixi.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    
      
        {children}
      
    
  );
}

Page.tsx

"use client";

import { PixiStage } from "./components/PixiStage";
import { useRef } from "react";

const GRID_SIZE = 800;
const rows = 8;
const columns = 10;
const gridSquarePixelSize = 91 

export default function Home() {
  const parentRef = useRef(null);

  return (
    
{/* 800×800 CSS grid container */}
{/* Pixi canvas absolutely positioned over the grid */}
{Array.from({ length: rows * columns }, (_, index) => (
) )}
); }

PixiStage.tsx

"use client";

import { useEffect, useRef, useMemo } from 'react';
import * as PIXI from 'pixi.js';
import { Application, ApplicationRef, extend, useTick } from '@pixi/react';
import { defaultFilterVert, Filter, GlProgram, Sprite, Texture } from 'pixi.js';

extend({ Sprite, Texture });

const SIZE = 800;
const gridSquarePixelSize = 91

const fogFragmentShader = `#version 300 es
  precision mediump float;

  // Receive the texture (sprite map) and a mask indicating sprite presence.
  uniform sampler2D uSpriteMask;       // 1 if a sprite exists, 0 otherwise
  in vec2 vTextureCoord;

  out vec4 outColor;

  void main() {
      // Sample the sprite presence mask at this fragment's coordinates:
      float hasSprite = texture(uSpriteMask, vTextureCoord).r;

      // Only render color if there is a sprite, otherwise output fully transparent
      if (hasSprite > 0.5) {
          outColor = vec4(1.0, 0.5, 0.0, 1.0); // visible color (orange)
      } else {
          outColor = vec4(0.0, 0.0, 0.0, 0.0); // transparent
      }
  }
`;


export function PixiStage() {

  const appRef = useRef(null);
  const fogContainer = useMemo(() => new PIXI.Container(), []);
  const spritesRef = useRef>(new Map());
  const fogSpriteRef = useRef(null);
  const fogDebugLoggedRef = useRef(false);

  useEffect(() => {
    const sprites = spritesRef.current;
    return () => {
      fogContainer.destroy({ children: true });
      sprites.clear();
    };
  }, [fogContainer]); 


  useEffect(() => {
    const app = appRef.current?.getApplication();
    if (!app) return;
    app.renderer.screen.width = SIZE / 2;
    app.renderer.screen.height = SIZE / 2;
  }, [SIZE]);


  function updateShaderUniforms(fogFilter: PIXI.Filter, deltaSeconds: number): void {
    const uniformsResource = fogFilter.resources.fogUniforms;
    const deltaTicker = deltaSeconds * 0.001;
    uniformsResource.uniforms.uTime += deltaTicker;
    uniformsResource._dirty = true;
  }

  function Ticker(): React.ReactNode {
    useTick((ticker) => {
      const deltaMS = ticker.deltaMS;
      const deltaSeconds = deltaMS * 0.001;
      const app = appRef.current?.getApplication();
      if (!app) return;

      if (!fogDebugLoggedRef.current && fogRT !== Texture.EMPTY) {
        const renderer: any = app.renderer;
        console.log(
          '[FogRT debug]',
          'screen', renderer.screen?.width, renderer.screen?.height, 
          'canvas', renderer.view?.canvas?.width, renderer.view?.canvas?.height,
          'fogRT logical', fogRT.width, fogRT.height,
          'fogRT pixels', fogRT.source.pixelWidth, fogRT.source.pixelHeight,
        );
        fogDebugLoggedRef.current = true;
      }

      app.renderer.render({
        container: fogContainer,
        target: fogRT,
        clear: true,
      });

      if (appRef.current) {
        // Animate here
      }
      if (fogFilter) {
        fogFilter.padding = 0;
        updateShaderUniforms(fogFilter, deltaSeconds);
      }
    });

    return null;
  }

  const fogRT = useMemo(() => {
    if (typeof window === 'undefined') return Texture.EMPTY;
    if (!SIZE) return Texture.EMPTY;

    const rt = PIXI.RenderTexture.create({
      width: 800,
      height: 800,
      resolution: 1,
    });

    return rt;
  }, [SIZE]);

  const fogFilter = useMemo(() => {
    if (typeof window === 'undefined') return;
    const filter = new Filter({
      glProgram: GlProgram.from({
        vertex: defaultFilterVert,
        fragment: fogFragmentShader,
      }),
      resources: {
        fogUniforms: {
          uTime: { value: 0, type: 'f32' },
          uAlpha: { value: 1, type: 'f32' },
          uSpeed: { value: [0.2, 0.2], type: 'vec2' },
          uShift: { value: 1, type: 'f32' },
          uResolution: { value: [
            gridSquarePixelSize, 
            gridSquarePixelSize,
          ], type: 'vec2' },
        },
      },
    });

    return filter;
  }, [gridSquarePixelSize]);

  useEffect(() => {
    fogContainer.sortableChildren = false;
    for (let i = 0; i < 8; i++) {
      for (let j = 0; j < 10; j++) {
        if (i % 2 === 0 && j % 2 === 0) {
          const sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
          sprite.alpha = 1;
          sprite.x = j * gridSquarePixelSize;
          sprite.y = i * gridSquarePixelSize;
          sprite.width = gridSquarePixelSize;
          sprite.height = gridSquarePixelSize;
          sprite.anchor.set(0, 0);
          fogContainer.addChild(sprite);
        }
      }
    }
  }, [fogContainer]);
  
  return (
    
      
      
    
  );
}

Much appreciated.

reactjs fragment-shader pixi.js