在React/Pixi中,在精灵容器上渲染滤镜纹理会导致对齐错位

人工智能 2026-07-12

我对PIXI和着色器还算新手,虽然已经实现了我想要的效果,但我在PIXI渲染器的缩放问题上遇到了困难。

这张图片展示了一个CSS网格,覆盖在一个PIXI画布之上。

在画布中,我渲染的精灵与CSS网格完美对齐。

但当我把着色器纹理应用到Sprite容器上时,渲染回画布的纹理会被放大并裁剪。

我在网上没有找到有用的解决方案,连AI也没给我答案。有人知道怎么解决吗?

在此输入图片描述

项目使用Next/React,搭配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 (
    <html lang="en">
      <body className="antialiased min-h-screen flex items-center justify-center bg-neutral-950 text-neutral-100">
        {children}
      </body>
    </html>
  );
}

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<HTMLDivElement>(null);

  return (
    <main className="min-h-screen flex items-center justify-center p-4">
      {/* 800×800 CSS grid container */}
      <div
        ref={parentRef}
        className="relative border border-neutral-700 rounded-lg overflow-hiddenx bg-neutral-950x"
        style={{
          display: "grid",
          width: GRID_SIZE,
          height: GRID_SIZE,
          gridTemplateColumns: `${gridSquarePixelSize}px`,
          gridTemplateRows: `${gridSquarePixelSize}px`,
        }}
      >
        {/* Pixi canvas absolutely positioned over the grid */}
        <div
          className="grid absolute top-0 left-0 bottom-0 right-0 z-10 overflow-hiddenx pointer-events-none"
          style={{ 
            width: GRID_SIZE, 
            height: GRID_SIZE, 
            gridTemplateColumns: `repeat(${columns}, ${gridSquarePixelSize}px)`,
            gridTemplateRows: `repeat(${rows}, ${gridSquarePixelSize}px)`,
          }}
        >
          {Array.from({ length: rows * columns }, (_, index) => (
            <div
              key={index}
              className="border flex items-center justify-center border-black/20"
              style={{
                width: `${gridSquarePixelSize}px`,
                height: `${gridSquarePixelSize}px`,
              }}
            />)
          )}
        </div>
        <PixiStage />
      </div>
    </main>
  );
}

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<ApplicationRef>(null);
  const fogContainer = useMemo(() => new PIXI.Container(), []);
  const spritesRef = useRef<Map<number, PIXI.Sprite>>(new Map());
  const fogSpriteRef = useRef<PIXI.Sprite | null>(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<f32>' },
          uShift: { value: 1, type: 'f32' },
          uResolution: { value: [
            gridSquarePixelSize, 
            gridSquarePixelSize,
          ], type: 'vec2<f32>' },
        },
      },
    });

    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 (
    <Application
      ref={appRef}
      width={SIZE}
      height={SIZE}
      backgroundAlpha={0.5}
      antialias={true}
      // className="absolute z-[35] pointer-events-none"
      resolution={1}
      roundPixels={true}
    >
      <Ticker />
      <pixiSprite
        ref={fogSpriteRef}
        width={SIZE}
        height={SIZE}
        texture={fogRT}
        filters={fogFilter ? [fogFilter] : []}
      />
    </Application>
  );
}

非常感谢。

Github仓库:https://github.com/jayonlydigital/pixi-grid-test

解决方案

如果其他人也觉得有用,我找到了答案。

我进一步深入研究了po2,这也把我带到了分辨率/DPI的层面。

尽管我在PIXI的 Canvas Renderer和 Sprite Container的设置中已经考虑了分辨率,但将离屏滤镜渲染到纹理的过程中,它会根据设备的分辨率和DPI自行处理,因此我也需要在着色器中进行相应的处理。

在片元着色器中,我使用这四个步骤来识别输入和输出尺寸,然后据此缩放vTextureCoord,使其与分辨率匹配,并在应用滤镜时使画布完美贴合。

  void main() {
      // 1. CALCULATE RESOLUTION SCALE
      vec2 resScale = uInputSize.xy / uOutputFrame.zw;

      // 2. CORRECTED UV FOR SAMPLING (Aligns with Sprite Pixels)
      vec2 correctedUV = vTextureCoord * resScale;

      // 3. CORRECTED UV FOR NOISE (Fixes Aspect Ratio Distortion)
      // We multiply by (width/height) so the filter doesn't look stretched
      vec2 noiseUV = correctedUV;
      noiseUV.x *= (uOutputFrame.z / uOutputFrame.w);

      // 4. SAMPLE THE TEXTURE
      vec4 tex = texture(uSampler, correctedUV);

      ...
  }

可用的渲染图

这个问题之所以出现,是因为我把滤镜应用到了一个Sprite容器上,并且只在精灵所在的区域渲染了滤镜,其余部分保持透明。如果我把滤镜应用到舞台上覆盖整个画面的区域,就不会注意到缩放不匹配的问题。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章