Shader graph: UV, Sample texture, Texture size, Time, Sin/Floor/Fract/Step; edge mode on Output (#78)
game-maker / suite (push) Successful in 8m38s

Fragment-side coordinate nodes instead of vertex nodes: the quad stays the
quad, a UV read plus a sample at a computed coordinate covers flip, scroll,
wave, pixelate and scanlines. Time is a uniform only when a Time node reaches
the Output; a viewport that drew a Time shader redraws from the ticker so the
Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a
second texture variant in TextureLibrary because WebGL binds no TextureStyle
resource of the shader. The compiler now emits #version 300 es first: Pixi
only compiles as ES 3.00 when the fragment says so, and textureSize needs it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-13 16:18:22 +02:00
1 parent 0e5e5ef925
commit f85606427b
12 files changed
+586 -38

No files matched your search

+2
View File
@@ -325,6 +325,8 @@ export class Editor implements PlayHost {
this.measureFps(dt);
this.play.tick(dt);
this.editorRun.tick(dt);
this.viewport.animate();
this.gameView.animate();
this.console.refresh();
if (this.tab === 'animation') this.animationEditor.tick(dt);
}
+5
View File
@@ -3,6 +3,7 @@ import { resolveObjects } from '../core/model/instance';
import { findObjectHolder } from '../core/model/Project';
import type { FrameRect } from '../core/model/texture';
import { activeDrag, Button, ui, Widget } from '../ui';
import { shaderLibrary } from '../viewport/ShaderLibrary';
import { textureLibrary } from '../viewport/TextureLibrary';
import type { CentreTab, Editor } from './Editor';
import { currentImagePicker } from './texture/ImagePicker';
@@ -410,6 +411,10 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
walk(app.stage);
return n;
},
/** Holds the shader clock at `at` seconds, so a Time shader's pixels can be measured; null releases it. */
shaderTime: (at: number | null) => shaderLibrary.freezeTime(at),
/** Which views redraw every tick because a Time shader is on screen. */
animates: () => ({ scene: viewport.animates, game: gameView.animates }),
lowLatency: () =>
(app.renderer as unknown as { gl?: WebGL2RenderingContext }).gl?.getContextAttributes()
?.desynchronized === true,
+16 -2
View File
@@ -7,7 +7,7 @@ import type { EditorState } from '../state';
import { type Size, Theme, Widget } from '../../ui';
import type { DrawItem } from '../../viewport/items';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import type { ShaderSource } from '../../viewport/shaders';
import { type ShaderSource, wornShader } from '../../viewport/shaders';
import { disposeSpriteMesh, drawItem, newSpriteMesh, syncSpriteMesh } from '../../viewport/symbols';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { type View, worldBounds, worldToScreen } from '../../viewport/view';
@@ -32,6 +32,7 @@ export abstract class WorldViewport extends Widget {
/** Pictures of sprite items: siblings as well, each right under its node. */
private readonly sprites = new Map<string, Mesh>();
private visibleCount = 0;
private animated = false;
protected lastItems: DrawItem[] = [];
protected constructor(protected readonly state: EditorState) {
@@ -45,8 +46,9 @@ export abstract class WorldViewport extends Widget {
state.on('project', () => this.redraw());
state.on('selection', () => this.redraw());
state.on('mode', () => this.redraw());
// While a Time shader is on screen the ticker redraws every frame anyway (`animate`).
state.on('runtime', () => {
if (this.visible) this.redraw();
if (this.visible && !this.animated) this.redraw();
});
// Unconditional: a picture arriving fires once per image, not per frame.
textureLibrary.onChange(() => this.redraw());
@@ -87,6 +89,16 @@ export abstract class WorldViewport extends Widget {
return { rendering: this.currentView() !== null, visibleItems: this.visibleCount };
}
/** True after a redraw that drew a sprite wearing a Time shader: the picture moves by itself. */
get animates(): boolean {
return this.animated;
}
/** Called every tick: redraws while the last redraw found a Time shader on screen. */
animate(): void {
if (this.visible && this.animated) this.redraw();
}
layerOf(objectId: string): 'objects' | 'overlay' | null {
const node = this.nodes.get(objectId);
if (!node) return null;
@@ -131,6 +143,7 @@ export abstract class WorldViewport extends Widget {
this.contentMask.clear();
this.content.visible = view !== null;
this.visibleCount = 0;
this.animated = false;
if (!view) return;
const { items, background } = this.collectFrame();
@@ -219,6 +232,7 @@ export abstract class WorldViewport extends Widget {
this.objects.addChild(sprite);
}
sprite.visible = true;
if (wornShader(item.props, shaders)?.time) this.animated = true;
syncSpriteMesh(sprite, item, size, textureLibrary, shaders);
}