UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

277 lines (249 loc) 7.43 kB
import { storageTextureSampleScalarType } from './compute-shader.js'; import { assertUniformName } from './uniforms.js'; import type { TextureData, TextureDefinition, TextureDefinitionMap, TextureUpdateMode, TextureValue } from './types.js'; /** * Texture definition with defaults and normalized numeric limits applied. */ export interface NormalizedTextureDefinition { /** * Normalized source value. */ source: TextureValue; /** * Effective color space. */ colorSpace: 'srgb' | 'linear'; /** * Effective texture format. */ format: GPUTextureFormat; /** * Effective flip-y flag. */ flipY: boolean; /** * Effective mipmap toggle. */ generateMipmaps: boolean; /** * Effective premultiplied-alpha flag. */ premultipliedAlpha: boolean; /** * Effective dynamic update strategy. */ update?: TextureUpdateMode; /** * Effective anisotropy level. */ anisotropy: number; /** * Effective filter mode. */ filter: GPUFilterMode; /** * Effective U address mode. */ addressModeU: GPUAddressMode; /** * Effective V address mode. */ addressModeV: GPUAddressMode; /** * Whether this texture is a storage texture (writable by compute). */ storage: boolean; /** * Whether this texture should be exposed as a fragment-stage sampled binding. */ fragmentVisible: boolean; /** * Explicit width for storage textures. Undefined when derived from source. */ width?: number; /** * Explicit height for storage textures. Undefined when derived from source. */ height?: number; } /** * Default sampling filter for textures when no explicit value is provided. */ const DEFAULT_TEXTURE_FILTER: GPUFilterMode = 'linear'; /** * Default addressing mode for textures when no explicit value is provided. */ const DEFAULT_TEXTURE_ADDRESS_MODE: GPUAddressMode = 'clamp-to-edge'; /** * Validates and returns sorted texture keys. * * @param textures - Texture definition map. * @returns Lexicographically sorted texture keys. */ export function resolveTextureKeys(textures: TextureDefinitionMap): string[] { const keys = Object.keys(textures).sort(); for (const key of keys) { assertUniformName(key); } return keys; } /** * Applies defaults and clamps to a single texture definition. * * @param definition - Optional texture definition. * @returns Normalized definition with deterministic defaults. */ export function normalizeTextureDefinition( definition: TextureDefinition | undefined ): NormalizedTextureDefinition { const isStorage = definition?.storage === true; const defaultFormat = definition?.colorSpace === 'linear' ? 'rgba8unorm' : 'rgba8unorm-srgb'; const format = definition?.format ?? defaultFormat; const sampleScalar = isStorage ? storageTextureSampleScalarType(format) : 'f32'; const explicitFragmentVisible = definition?.fragmentVisible; if (explicitFragmentVisible === true && sampleScalar !== 'f32') { throw new Error( `Texture with storage format "${format}" cannot be fragmentVisible: ` + `fragment shader uses texture_2d<f32>, which is incompatible with ${sampleScalar} sample type. ` + `Set fragmentVisible: false or use a float-sampled storage format.` ); } const fragmentVisible = explicitFragmentVisible ?? sampleScalar === 'f32'; const normalized: NormalizedTextureDefinition = { source: definition?.source ?? null, colorSpace: definition?.colorSpace ?? 'srgb', format, flipY: definition?.flipY ?? true, generateMipmaps: definition?.generateMipmaps ?? false, premultipliedAlpha: definition?.premultipliedAlpha ?? false, anisotropy: Math.max(1, Math.min(16, Math.floor(definition?.anisotropy ?? 1))), filter: definition?.filter ?? DEFAULT_TEXTURE_FILTER, addressModeU: definition?.addressModeU ?? DEFAULT_TEXTURE_ADDRESS_MODE, addressModeV: definition?.addressModeV ?? DEFAULT_TEXTURE_ADDRESS_MODE, storage: isStorage, fragmentVisible }; if (definition?.width !== undefined) { normalized.width = definition.width; } if (definition?.height !== undefined) { normalized.height = definition.height; } if (definition?.update !== undefined) { normalized.update = definition.update; } return normalized; } /** * Normalizes all texture definitions for already-resolved texture keys. * * @param textures - Source texture definitions. * @param textureKeys - Texture keys to normalize. * @returns Normalized map keyed by `textureKeys`. */ export function normalizeTextureDefinitions( textures: TextureDefinitionMap, textureKeys: string[] ): Record<string, NormalizedTextureDefinition> { const out: Record<string, NormalizedTextureDefinition> = {}; for (const key of textureKeys) { out[key] = normalizeTextureDefinition(textures[key]); } return out; } /** * Checks whether a texture value is a structured `{ source, width?, height? }` object. */ export function isTextureData(value: TextureValue): value is TextureData { return typeof value === 'object' && value !== null && 'source' in value; } /** * Converts supported texture input variants to normalized `TextureData`. * * @param value - Texture value input. * @returns Structured texture data or `null`. */ export function toTextureData(value: TextureValue): TextureData | null { if (value === null) { return null; } if (isTextureData(value)) { return value; } return { source: value }; } /** * Resolves effective runtime texture update strategy. */ export function resolveTextureUpdateMode(input: { source: TextureData['source']; override?: TextureUpdateMode; defaultMode?: TextureUpdateMode; }): TextureUpdateMode { if (input.override) { return input.override; } if (input.defaultMode) { return input.defaultMode; } if (isVideoTextureSource(input.source)) { return 'perFrame'; } return 'once'; } /** * Resolves texture dimensions from explicit values or source metadata. * * @param data - Texture payload. * @returns Positive integer width/height. * @throws {Error} When dimensions cannot be resolved to positive values. */ export function resolveTextureSize(data: TextureData): { width: number; height: number; } { const source = data.source as { width?: number; height?: number; naturalWidth?: number; naturalHeight?: number; videoWidth?: number; videoHeight?: number; }; const width = data.width ?? source.naturalWidth ?? source.videoWidth ?? source.width ?? 0; const height = data.height ?? source.naturalHeight ?? source.videoHeight ?? source.height ?? 0; if (width <= 0 || height <= 0) { throw new Error('Texture source must have positive width and height'); } return { width, height }; } /** * Computes the number of mipmap levels for a base texture size. * * @param width - Base width. * @param height - Base height. * @returns Total mip level count (minimum `1`). */ export function getTextureMipLevelCount(width: number, height: number): number { let levels = 1; let currentWidth = Math.max(1, width); let currentHeight = Math.max(1, height); while (currentWidth > 1 || currentHeight > 1) { currentWidth = Math.max(1, Math.floor(currentWidth / 2)); currentHeight = Math.max(1, Math.floor(currentHeight / 2)); levels += 1; } return levels; } /** * Checks whether the source is an `HTMLVideoElement`. */ export function isVideoTextureSource(source: TextureData['source']): source is HTMLVideoElement { return typeof HTMLVideoElement !== 'undefined' && source instanceof HTMLVideoElement; }