Hey everyone!
I’m currently attempting to use @playcanvas/react to render a splat (in SOG format) and programmatically move the splat and/or camera.
I can load and view the splat as expected, however if I try and apply any transform whatsoever to either the splat or the camera, then splat entirely disappears, never to return.
Here’s a minimal example of my component:
'use client';
import React from 'react';
import { Application, Entity } from '@playcanvas/react';
import { Camera, GSplat } from '@playcanvas/react/components';
import { useSplat } from '@playcanvas/react/hooks';
function Scene({ tilePos }: { tilePos: [number, number, number] }) {
const { asset, loading, error } = useSplat('/assets/tile.sog');
React.useEffect(() => {
if (error) console.error('[SOG] failed to load:', error);
if (loading) console.log('[SOG] loading…');
}, [error, loading]);
return (
<>
{/* Fixed camera */}
<Entity name="camera-root" position={[0, 1.6, 0]}>
<Entity name="camera">
<Camera nearClip={0.01} farClip={10000} />
</Entity>
</Entity>
{/* Splat */}
{asset && (
<Entity name="tile" position={tilePos} rotation={[0, -90, 0]}>
<GSplat asset={asset} />
</Entity>
)}
</>
);
}
export default function PlayCanvasSplatNudgeTest() {
const [tilePos, setTilePos] = React.useState<[number, number, number]>([0, 0, 0]);
const nudge = (dx: number, dy: number, dz: number) => {
setTilePos(([x, y, z]) => [x + dx, y + dy, z + dz]);
};
const reset = () => setTilePos([0, 0, 0]);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Application graphicsDeviceOptions={{ antialias: false }}>
<Scene tilePos={tilePos} />
</Application>
<div style={overlayStyle}>
<div style={labelStyle}>
<span>Tile pos</span>
<strong>
[{tilePos.map((v) => v.toFixed(3)).join(', ')}]
</strong>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button style={buttonStyle} onClick={() => nudge(0.02, 0, 0)}>
Nudge +X (2cm)
</button>
<button style={buttonStyle} onClick={() => nudge(-0.02, 0, 0)}>
Nudge -X (2cm)
</button>
<button style={buttonStyle} onClick={() => nudge(0, 0, 0.02)}>
Nudge +Z (2cm)
</button>
<button style={buttonStyle} onClick={reset}>
Reset
</button>
</div>
<small style={{ opacity: 0.85, marginTop: 6 }}>
Diagnostic: if the splat disappears after a 2cm nudge, we’re dealing with a transform/update/culling bug in the GSplat pipeline rather than coordinate math.
</small>
</div>
</div>
);
}
I have also tried to move the tile/camera imperatively, for example (I think the syntax is correct, going from the docs but please correct me if I’m wrong!):
function Scene({ tilePos }: { tilePos: pc.Vec3 }) {
const { asset, loading, error } = useSplat('/assets/tile.sog');
const tileRef = useRef<pc.Entity | null>(null);
// Move the tile imperatively in the app update loop
useAppEvent('update', () => {
const tile = tileRef.current;
if (!tile) return;
tile.setLocalPosition(tilePos);
tile.setLocalEulerAngles(0, -90, 0); // your correct orientation
});
if (error) console.error('[SOG] failed to load:', error);
if (loading) console.log('[SOG] loading…');
return (
<>
{/* Fixed camera */}
<Entity name="camera-root" position={[0, 1.6, 0]}>
<Entity name="camera">
<Camera nearClip={0.01} farClip={10000} />
</Entity>
</Entity>
{/* Splat */}
{asset && (
<Entity name="tile" ref={tileRef}>
<GSplat asset={asset} />
</Entity>
)}
</>
);
}
However no matter what I try, any transform at all makes the splat disappear. I have a ‘Reset’ button in this test. It correctly resets the camera/splat position to default, but once the splat has ‘gone’, it doesn’t come back.
This looks like a bug to me, but I’ve never used PlayCanvas before so would greatly appreciate any help before I jump to conclusions!
Thanks,
Olly