在React/Pixi中,在精灵容器上渲染滤镜纹理会导致对齐错位
我对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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。