diff --git a/packages/dev/core/src/Engines/WebGPU/Extensions/engine.computeShader.pure.ts b/packages/dev/core/src/Engines/WebGPU/Extensions/engine.computeShader.pure.ts index e96c2aa952de..c94a41e9cbdb 100644 --- a/packages/dev/core/src/Engines/WebGPU/Extensions/engine.computeShader.pure.ts +++ b/packages/dev/core/src/Engines/WebGPU/Extensions/engine.computeShader.pure.ts @@ -6,14 +6,201 @@ import { type IComputeEffectCreationOptions, type IComputeShaderPath, ComputeEff import { type IComputeContext } from "../../../Compute/IComputeContext"; import { type IComputePipelineContext } from "../../../Compute/IComputePipelineContext"; import { type Nullable } from "../../../types"; -import { type ComputeBindingList, type ComputeBindingMapping, type ComputeCompilationMessages } from "../../Extensions/engine.computeShader.pure"; +import { ComputeBindingType, type ComputeBindingList, type ComputeBindingMapping, type ComputeCompilationMessages } from "../../Extensions/engine.computeShader.pure"; +import { Constants } from "../../constants"; import { WebGPUComputeContext } from "../webgpuComputeContext"; import { WebGPUComputePipelineContext } from "../webgpuComputePipelineContext"; import { type WebGPUPerfCounter } from "../webgpuPerfCounter"; import { type DataBuffer } from "../../../Buffers/dataBuffer"; import { WebGPUEngine } from "../../webgpuEngine.pure"; +import { type InternalTexture } from "../../../Materials/Textures/internalTexture"; +import { type BaseTexture } from "../../../Materials/Textures/baseTexture.pure"; let _Registered = false; + +const _GetComputeStorageBufferType = (source: string, group: number, binding: number): WebGPUConstants.BufferBindingType => { + const bindingPattern = `@binding\\(${binding}\\)\\s*@group\\(${group}\\)`; + const groupPattern = `@group\\(${group}\\)\\s*@binding\\(${binding}\\)`; + const declarationPattern = `(?:${bindingPattern}|${groupPattern})\\s*var`; + const access = source.match(new RegExp(declarationPattern))?.[1]; + + return access === "read" ? WebGPUConstants.BufferBindingType.ReadOnlyStorage : WebGPUConstants.BufferBindingType.Storage; +}; + +const _GetComputeTextureViewDimension = (source: string, group: number, binding: number): WebGPUConstants.TextureViewDimension => { + const bindingPattern = `@binding\\(${binding}\\)\\s*@group\\(${group}\\)`; + const groupPattern = `@group\\(${group}\\)\\s*@binding\\(${binding}\\)`; + const declaration = source.match(new RegExp(`(?:${bindingPattern}|${groupPattern})\\s*var\\s+\\w+\\s*:\\s*(texture_\\w+)`))?.[1]; + + switch (declaration) { + case "texture_2d_array": + case "texture_depth_2d_array": + return WebGPUConstants.TextureViewDimension.E2dArray; + case "texture_cube": + return WebGPUConstants.TextureViewDimension.Cube; + case "texture_cube_array": + return WebGPUConstants.TextureViewDimension.CubeArray; + case "texture_3d": + return WebGPUConstants.TextureViewDimension.E3d; + default: + return WebGPUConstants.TextureViewDimension.E2d; + } +}; + +const _GetInternalTexture = (texture: BaseTexture | InternalTexture): Nullable => { + const internalTexture = texture as InternalTexture; + if (internalTexture._hardwareTexture !== undefined) { + return internalTexture; + } + + return (texture as BaseTexture)._texture; +}; + +const _GetComputeTextureSampleType = (texture: BaseTexture | InternalTexture, textureFloatLinearFiltering: boolean): WebGPUConstants.TextureSampleType => { + const internalTexture = _GetInternalTexture(texture); + + if (!internalTexture) { + return WebGPUConstants.TextureSampleType.Float; + } + + const textureIsDepth = internalTexture.format >= Constants.TEXTUREFORMAT_DEPTH24_STENCIL8 && internalTexture.format <= Constants.TEXTUREFORMAT_DEPTH32FLOAT_STENCIL8; + if (textureIsDepth) { + return WebGPUConstants.TextureSampleType.Depth; + } + + if (internalTexture.type === Constants.TEXTURETYPE_FLOAT && !textureFloatLinearFiltering) { + return WebGPUConstants.TextureSampleType.UnfilterableFloat; + } + + return WebGPUConstants.TextureSampleType.Float; +}; + +const _GetComputePipelineLayout = ( + device: GPUDevice, + bindings: ComputeBindingList, + bindingsMapping: ComputeBindingMapping | undefined, + shaderSource: string, + textureFloatLinearFiltering: boolean +): Nullable<{ layout: GPUPipelineLayout; key: string }> => { + if (!bindingsMapping) { + return null; + } + + let needsExplicitLayout = false; + const bindGroupLayoutEntries: GPUBindGroupLayoutEntry[][] = []; + + const addEntry = (group: number, entry: GPUBindGroupLayoutEntry) => { + const entries = (bindGroupLayoutEntries[group] ??= []); + const existingIndex = entries.findIndex((item) => item.binding === entry.binding); + if (existingIndex >= 0) { + entries[existingIndex] = entry; + } else { + entries.push(entry); + } + }; + + for (const key in bindings) { + const binding = bindings[key]; + const location = bindingsMapping[key]; + if (!location) { + continue; + } + + const group = location.group; + const bindingIndex = location.binding; + + switch (binding.type) { + case ComputeBindingType.Texture: + case ComputeBindingType.TextureWithoutSampler: + case ComputeBindingType.InternalTexture: { + const sampleType = _GetComputeTextureSampleType(binding.object as BaseTexture | InternalTexture, textureFloatLinearFiltering); + needsExplicitLayout ||= sampleType === WebGPUConstants.TextureSampleType.UnfilterableFloat || sampleType === WebGPUConstants.TextureSampleType.Depth; + + if (binding.type === ComputeBindingType.Texture) { + addEntry(group, { + binding: bindingIndex - 1, + visibility: WebGPUConstants.ShaderStage.Compute, + sampler: { + type: + sampleType === WebGPUConstants.TextureSampleType.UnfilterableFloat + ? WebGPUConstants.SamplerBindingType.NonFiltering + : WebGPUConstants.SamplerBindingType.Filtering, + }, + }); + } + + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + texture: { + sampleType, + viewDimension: _GetComputeTextureViewDimension(shaderSource, group, bindingIndex), + multisampled: false, + }, + }); + break; + } + case ComputeBindingType.StorageTexture: { + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + storageTexture: { + access: WebGPUConstants.StorageTextureAccess.WriteOnly, + format: ((binding.object as BaseTexture)._texture!._hardwareTexture as any).format, + viewDimension: _GetComputeTextureViewDimension(shaderSource, group, bindingIndex), + }, + }); + break; + } + case ComputeBindingType.UniformBuffer: + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + buffer: { type: WebGPUConstants.BufferBindingType.Uniform }, + }); + break; + case ComputeBindingType.StorageBuffer: + case ComputeBindingType.DataBuffer: + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + buffer: { type: _GetComputeStorageBufferType(shaderSource, group, bindingIndex) }, + }); + break; + case ComputeBindingType.Sampler: + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + sampler: { type: WebGPUConstants.SamplerBindingType.Filtering }, + }); + break; + case ComputeBindingType.ExternalTexture: + addEntry(group, { + binding: bindingIndex, + visibility: WebGPUConstants.ShaderStage.Compute, + externalTexture: {}, + }); + break; + } + } + + if (!needsExplicitLayout) { + return null; + } + + const bindGroupLayouts: GPUBindGroupLayout[] = []; + for (let i = 0; i < bindGroupLayoutEntries.length; i++) { + const entries = bindGroupLayoutEntries[i] ?? []; + entries.sort((a, b) => a.binding - b.binding); + bindGroupLayouts[i] = device.createBindGroupLayout({ entries }); + } + + return { + layout: device.createPipelineLayout({ bindGroupLayouts }), + key: JSON.stringify(bindGroupLayoutEntries), + }; +}; + /** * Register side effects for enginesWebGPUExtensionsEngineComputeShader. * Safe to call multiple times; only the first call has an effect. @@ -110,9 +297,14 @@ export function RegisterEnginesWebGPUExtensionsEngineComputeShader(): void { const contextPipeline = effect._pipelineContext as WebGPUComputePipelineContext; const computeContext = context as WebGPUComputeContext; - if (!contextPipeline.computePipeline) { + const explicitLayout = _GetComputePipelineLayout(this._device, bindings, bindingsMapping, contextPipeline.sources?.compute ?? "", this._caps.textureFloatLinearFiltering); + const layoutKey = explicitLayout?.key ?? "auto"; + + if (!contextPipeline.computePipeline || contextPipeline.computePipelineLayoutKey !== layoutKey) { + contextPipeline.computePipelineLayoutKey = layoutKey; + computeContext.clear(); contextPipeline.computePipeline = this._device.createComputePipeline({ - layout: WebGPUConstants.AutoLayoutMode.Auto, + layout: explicitLayout?.layout ?? WebGPUConstants.AutoLayoutMode.Auto, compute: contextPipeline.stage!, }); } diff --git a/packages/dev/core/src/Engines/WebGPU/webgpuComputePipelineContext.ts b/packages/dev/core/src/Engines/WebGPU/webgpuComputePipelineContext.ts index 52c7039ff3ec..ad90dfae6a4c 100644 --- a/packages/dev/core/src/Engines/WebGPU/webgpuComputePipelineContext.ts +++ b/packages/dev/core/src/Engines/WebGPU/webgpuComputePipelineContext.ts @@ -15,6 +15,8 @@ export class WebGPUComputePipelineContext implements IComputePipelineContext { public computePipeline: GPUComputePipeline; + public computePipelineLayoutKey: string; + // eslint-disable-next-line no-restricted-syntax public get isAsync() { return false; @@ -36,6 +38,7 @@ export class WebGPUComputePipelineContext implements IComputePipelineContext { constructor(engine: WebGPUEngine) { this._name = "unnamed"; this.engine = engine; + this.computePipelineLayoutKey = ""; } public _getComputeShaderCode(): string | null { diff --git a/packages/dev/core/src/Materials/Textures/Procedurals/noiseProceduralTexture.pure.ts b/packages/dev/core/src/Materials/Textures/Procedurals/noiseProceduralTexture.pure.ts index 3374dfef6a0f..9b0d30cd425b 100644 --- a/packages/dev/core/src/Materials/Textures/Procedurals/noiseProceduralTexture.pure.ts +++ b/packages/dev/core/src/Materials/Textures/Procedurals/noiseProceduralTexture.pure.ts @@ -6,14 +6,24 @@ import { EngineStore } from "../../../Engines/engineStore"; import { type Texture } from "../../../Materials/Textures/texture.pure"; import { type IProceduralTextureCreationOptions, ProceduralTexture } from "./proceduralTexture.pure"; import { RegisterClass } from "../../../Misc/typeStore"; +import { ShaderLanguage } from "../../../Materials/shaderLanguage"; -let _NoiseProceduralTextureShaderPromise: Promise | undefined; +let _NoiseProceduralTextureGlslShaderPromise: Promise | undefined; +let _NoiseProceduralTextureWgslShaderPromise: Promise | undefined; -async function _EnsureNoiseProceduralTextureShaderAsync(): Promise { - _NoiseProceduralTextureShaderPromise ??= (async () => { +async function _EnsureNoiseProceduralTextureShaderAsync(shaderLanguage: ShaderLanguage = ShaderLanguage.GLSL): Promise { + if (shaderLanguage === ShaderLanguage.WGSL) { + _NoiseProceduralTextureWgslShaderPromise ??= (async () => { + await import("../../../ShadersWGSL/noise.fragment"); + })(); + await _NoiseProceduralTextureWgslShaderPromise; + return; + } + + _NoiseProceduralTextureGlslShaderPromise ??= (async () => { await import("../../../Shaders/noise.fragment"); })(); - await _NoiseProceduralTextureShaderPromise; + await _NoiseProceduralTextureGlslShaderPromise; } /** @@ -44,9 +54,11 @@ export class NoiseProceduralTexture extends ProceduralTexture { * @param generateMipMaps defines if mipmaps must be generated (true by default) */ constructor(name: string, size: number = 256, scene: Nullable = EngineStore.LastCreatedScene, fallbackTexture?: Texture, generateMipMaps?: boolean) { + const shaderLanguage = scene?.getEngine().isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL; const creationOptions: IProceduralTextureCreationOptions = { fallbackTexture, - extraInitializationsAsync: _EnsureNoiseProceduralTextureShaderAsync, + shaderLanguage, + extraInitializationsAsync: () => _EnsureNoiseProceduralTextureShaderAsync(shaderLanguage), }; super(name, size, "noise", scene, creationOptions, generateMipMaps); diff --git a/packages/dev/core/src/Materials/effect.pure.ts b/packages/dev/core/src/Materials/effect.pure.ts index 35439858a69a..84500a799446 100644 --- a/packages/dev/core/src/Materials/effect.pure.ts +++ b/packages/dev/core/src/Materials/effect.pure.ts @@ -393,9 +393,18 @@ export class Effect implements IDisposable { this.uniqueId = Effect._UniqueIdSeed++; if (!cachedPipeline) { - // Floating promise - should be checked here. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this._processShaderCodeAsync(null, false, null, extraInitializationsAsync); + void (async () => { + try { + await this._processShaderCodeAsync(null, false, null, extraInitializationsAsync); + } catch (error) { + const message = error?.message ?? String(error); + const asyncError = new Error(`Effect async shader preparation failed for "${String(this.name)}": ${message}`); + if (error && typeof error.stack === "string") { + asyncError.stack = `${asyncError.message}\nCaused by: ${error.stack}`; + } + this._processCompilationErrors(asyncError); + } + })(); } else { this._pipelineContext = cachedPipeline; this._pipelineContext.setEngine(this._engine); @@ -710,8 +719,8 @@ export class Effect implements IDisposable { } /** - * Gets the pipeline generation options for this effect - * @returns the pipeline generation options + * Gets the pipeline generation options for this effect. + * @returns the pipeline generation options for this effect */ public getPipelineGenerationOptions(): IPipelineGenerationOptions { return { @@ -884,7 +893,7 @@ export class Effect implements IDisposable { } private _processCompilationErrors(e: any, previousPipelineContext: Nullable = null) { - this._compilationError = e.message; + this._compilationError = typeof e?.stack === "string" ? e.stack : (e?.message ?? String(e)); const attributesNames = this._attributesNames; const fallbacks = this._fallbacks; diff --git a/packages/dev/core/src/Materials/meshDebugPluginMaterial.pure.ts b/packages/dev/core/src/Materials/meshDebugPluginMaterial.pure.ts index 4a55dac96cd3..053b65713447 100644 --- a/packages/dev/core/src/Materials/meshDebugPluginMaterial.pure.ts +++ b/packages/dev/core/src/Materials/meshDebugPluginMaterial.pure.ts @@ -26,8 +26,8 @@ flat varying float dbg_vPass; const vertexDefinitionsWebGPU = `#if defined(DBG_ENABLED) attribute dbg_initialPass: f32; varying dbg_vBarycentric: vec3f; -varying dbg_vVertexWorldPos: vec3f; -varying dbg_vPass: f32; +flat varying dbg_vVertexWorldPos: vec3f; +flat varying dbg_vPass: f32; #endif`; const vertexMainEnd = `#if defined(DBG_ENABLED) @@ -152,8 +152,8 @@ flat varying float dbg_vPass; const fragmentDefinitionsWebGPU = `#if defined(DBG_ENABLED) varying dbg_vBarycentric: vec3f; -varying dbg_vVertexWorldPos: vec3f; -varying dbg_vPass: f32; +flat varying dbg_vVertexWorldPos: vec3f; +flat varying dbg_vPass: f32; #if !defined(DBG_MULTIPLY) fn dbg_applyShading(color: vec3f) -> vec3f { @@ -179,10 +179,10 @@ varying dbg_vPass: f32; #if DBG_MODE == 2 || DBG_MODE == 3 fn dbg_cornerFactor() -> f32 { - var worldPos = fragmentInputs.vPositionW; - float dist = length(worldPos - fragmentInputs.dbg_vVertexWorldPos); - float camDist = length(worldPos - scene.vEyePosition.xyz); - float d = sqrt(camDist) * .001; + let worldPos = fragmentInputs.vPositionW; + let dist = length(worldPos - fragmentInputs.dbg_vVertexWorldPos); + let camDist = length(worldPos - scene.vEyePosition.xyz); + let d = sqrt(camDist) * .001; return smoothstep((uniforms.dbg_thicknessRadiusScale.y * d), ((uniforms.dbg_thicknessRadiusScale.y * 1.01) * d), dist); } #endif @@ -191,7 +191,7 @@ varying dbg_vPass: f32; fn dbg_checkerboardFactor(uv: vec2f) -> f32 { var f = fract(uv * uniforms.dbg_thicknessRadiusScale.z); f -= .5; - return (f.x * f.y) > 0. ? 1. : 0.; + return select(0.0, 1.0, (f.x * f.y) > 0.0); } #endif #endif`; @@ -233,9 +233,11 @@ var dbg_color = vec3f(1.); #if DBG_MODE == 1 dbg_color = mix(uniforms.dbg_wireframeTrianglesColor, vec3f(1.), dbg_edgeFactor()); #elif DBG_MODE == 2 || DBG_MODE == 3 - var dbg_cornerFactor = dbg_cornerFactor(); - if (fragmentInputs.dbg_vPass == 0. && dbg_cornerFactor == 1.) discard; - dbg_color = mix(uniforms.dbg_vertexColor, vec3(1.), dbg_cornerFactor); + let dbg_cornerFactor = dbg_cornerFactor(); + if (fragmentInputs.dbg_vPass == 0.0 && dbg_cornerFactor == 1.0) { + discard; + } + dbg_color = mix(uniforms.dbg_vertexColor, vec3f(1.), dbg_cornerFactor); #if DBG_MODE == 3 dbg_color *= mix(uniforms.dbg_wireframeVerticesColor, vec3f(1.), dbg_edgeFactor()); #endif @@ -253,7 +255,7 @@ var dbg_color = vec3f(1.); fragmentOutputs.color *= vec4f(dbg_color, 1.); #else #if DBG_MODE != 6 - fragmentOutputs.color = vec4f(dbg_applyShading(dbg_shadedDiffuseColor) * dbg_color, 1.); + fragmentOutputs.color = vec4f(dbg_applyShading(uniforms.dbg_shadedDiffuseColor) * dbg_color, 1.); #else fragmentOutputs.color = vec4f(dbg_color, 1.); #endif diff --git a/packages/dev/core/src/Misc/tools.pure.ts b/packages/dev/core/src/Misc/tools.pure.ts index 0c815def0eb4..8b19ed1c8ce4 100644 --- a/packages/dev/core/src/Misc/tools.pure.ts +++ b/packages/dev/core/src/Misc/tools.pure.ts @@ -599,9 +599,15 @@ export class Tools { Tools.LoadFile( scriptUrl, (data) => { - Function(data as string).apply(null); - if (onSuccess) { - onSuccess(); + try { + Function(data as string).apply(null); + if (onSuccess) { + onSuccess(); + } + } catch (exception) { + if (onError) { + onError("LoadScript Error", exception); + } } }, undefined, diff --git a/packages/dev/core/src/Particles/gpuParticleSystem.pure.ts b/packages/dev/core/src/Particles/gpuParticleSystem.pure.ts index 8a2bda6fc4a5..45ab6fa3cbf5 100644 --- a/packages/dev/core/src/Particles/gpuParticleSystem.pure.ts +++ b/packages/dev/core/src/Particles/gpuParticleSystem.pure.ts @@ -16,7 +16,7 @@ import { Attractor } from "./attractor"; import { Logger } from "../Misc/logger"; import { BoxParticleEmitter } from "../Particles/EmitterTypes/boxParticleEmitter"; import { type IDisposable, Scene } from "../scene.pure"; -import { type Effect } from "../Materials/effect.pure"; +import { type Effect, type IEffectCreationOptions } from "../Materials/effect.pure"; import { ImageProcessingConfiguration } from "../Materials/imageProcessingConfiguration.pure"; import { RawTexture } from "../Materials/Textures/rawTexture"; import { Constants } from "../Engines/constants"; @@ -27,6 +27,7 @@ import { CustomParticleEmitter } from "./EmitterTypes/customParticleEmitter"; import { AbstractEngine } from "../Engines/abstractEngine.pure"; import { type DataBuffer } from "../Buffers/dataBuffer"; import { DrawWrapper } from "../Materials/drawWrapper"; +import { ShaderLanguage } from "../Materials/shaderLanguage"; import { type UniformBufferEffectCommonAccessor } from "../Materials/uniformBufferEffectCommonAccessor"; import { type IGPUParticleSystemPlatform } from "./IGPUParticleSystemPlatform"; import { GetClass } from "../Misc/typeStore"; @@ -96,6 +97,7 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable private _actualFrame = 0; private _drawWrappers: { [blendMode: number]: DrawWrapper }; private _customWrappers: { [blendMode: number]: Nullable }; + private _renderShadersLoaded = false; private readonly _rawTextureWidth = 256; @@ -1498,7 +1500,30 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable this.fillUniformsAttributesAndSamplerNames(uniforms, attributes, samplers); - drawWrapper.setEffect(this._engine.createEffect("gpuRenderParticles", attributes, uniforms, samplers, join), join); + const shaderLanguage = this._engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL; + drawWrapper.setEffect( + this._engine.createEffect( + "gpuRenderParticles", + { + attributes, + uniformsNames: uniforms, + samplers, + defines: join, + shaderLanguage, + extraInitializationsAsync: this._renderShadersLoaded + ? undefined + : async () => { + if (shaderLanguage === ShaderLanguage.WGSL) { + await Promise.all([import("../ShadersWGSL/gpuRenderParticles.vertex"), import("../ShadersWGSL/gpuRenderParticles.fragment")]); + } + + this._renderShadersLoaded = true; + }, + }, + this._engine + ), + join + ); } return drawWrapper; diff --git a/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.fragment.fx b/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.fragment.fx new file mode 100644 index 000000000000..d7b408d301bf --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.fragment.fx @@ -0,0 +1,39 @@ +var diffuseSamplerSampler: sampler; +var diffuseSampler: texture_2d; + +varying vUV: vec2f; +varying vColor: vec4f; + +#include +#include +#include +#include +#include +#include + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + #include + + let textureColor: vec4f = textureSample(diffuseSampler, diffuseSamplerSampler, input.vUV); + var baseColor: vec4f = textureColor * input.vColor; + +#ifdef BLENDMULTIPLYMODE + let alpha: f32 = input.vColor.a * textureColor.a; + baseColor = vec4f(baseColor.rgb * alpha + vec3f(1.0) * (1.0 - alpha), baseColor.a); +#endif + + #include + #include(color,baseColor) + +#ifdef IMAGEPROCESSINGPOSTPROCESS + baseColor = vec4f(toLinearSpaceVec3(baseColor.rgb), baseColor.a); +#else +#ifdef IMAGEPROCESSING + baseColor = vec4f(toLinearSpaceVec3(baseColor.rgb), baseColor.a); + baseColor = applyImageProcessing(baseColor); +#endif +#endif + + fragmentOutputs.color = baseColor; +} diff --git a/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.vertex.fx b/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.vertex.fx new file mode 100644 index 000000000000..44fa46267190 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/gpuRenderParticles.vertex.fx @@ -0,0 +1,189 @@ +uniform view: mat4x4f; +uniform projection: mat4x4f; +uniform translationPivot: vec2f; +uniform worldOffset: vec3f; +#ifdef LOCAL +uniform emitterWM: mat4x4f; +#endif + +// Particles state +attribute position: vec3f; +attribute age: f32; +attribute life: f32; +attribute size: vec3f; +#if !defined(BILLBOARD) || defined(BILLBOARDSTRETCHED_LOCAL) +attribute initialDirection: vec3f; +#endif +#ifdef BILLBOARDSTRETCHED +attribute direction: vec3f; +#endif +attribute angle: f32; +#ifdef ANIMATESHEET +attribute cellIndex: f32; +#endif +attribute offset: vec2f; +attribute uv: vec2f; + +varying vUV: vec2f; +varying vColor: vec4f; +varying vPositionW: vec3f; + +#if defined(BILLBOARD) && !defined(BILLBOARDY) && !defined(BILLBOARDSTRETCHED) +uniform invView: mat4x4f; +#endif + +#include +#include +#include + +#ifdef COLORGRADIENTS +var colorGradientSamplerSampler: sampler; +var colorGradientSampler: texture_2d; +#ifdef COLORGRADIENTS_COLOR2 +attribute seed: vec4f; +#endif +#else +uniform colorDead: vec4f; +attribute color: vec4f; +#endif + +#ifdef ANIMATESHEET +uniform sheetInfos: vec3f; +#endif + +#ifdef BILLBOARD +uniform eyePosition: vec3f; +#endif + +fn particleBasePosition() -> vec3f { +#ifdef LOCAL + return (uniforms.emitterWM * vec4f(vertexInputs.position, 1.0)).xyz + uniforms.worldOffset; +#else + return vertexInputs.position + uniforms.worldOffset; +#endif +} + +fn rotate(yaxis: vec3f, rotatedCorner: vec3f) -> vec3f { + let xaxis: vec3f = normalize(cross(vec3f(0.0, 1.0, 0.0), yaxis)); + let zaxis: vec3f = normalize(cross(yaxis, xaxis)); + let rotMatrix: mat3x3f = mat3x3f(xaxis, yaxis, zaxis); + return particleBasePosition() + rotMatrix * rotatedCorner; +} + +#ifdef BILLBOARDSTRETCHED +fn rotateAlign(toCamera: vec3f, rotatedCorner: vec3f) -> vec3f { + let normalizedToCamera: vec3f = normalize(toCamera); +#ifdef BILLBOARDSTRETCHED_LOCAL + let normalizedCrossDirToCamera: vec3f = normalize(cross(normalize(vertexInputs.initialDirection), normalizedToCamera)); + let row1: vec3f = normalize(vertexInputs.initialDirection); +#else + let normalizedCrossDirToCamera: vec3f = normalize(cross(normalize(vertexInputs.direction), normalizedToCamera)); + let row1: vec3f = normalize(cross(normalizedToCamera, normalizedCrossDirToCamera)); +#endif + + let rotMatrix: mat3x3f = mat3x3f(normalizedCrossDirToCamera, row1, normalizedToCamera); + return particleBasePosition() + rotMatrix * rotatedCorner; +} +#endif + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { +#ifdef EMITRATECTRL + let shouldCullParticle: bool = vertexInputs.life > 0.0 && vertexInputs.age >= vertexInputs.life; +#endif + +#ifdef ANIMATESHEET + let rowOffset: f32 = floor(vertexInputs.cellIndex / uniforms.sheetInfos.z); + let columnOffset: f32 = vertexInputs.cellIndex - rowOffset * uniforms.sheetInfos.z; + let uvScale: vec2f = uniforms.sheetInfos.xy; + let uvOffset: vec2f = vec2f(vertexInputs.uv.x, 1.0 - vertexInputs.uv.y); + vertexOutputs.vUV = (uvOffset + vec2f(columnOffset, rowOffset)) * uvScale; +#else + vertexOutputs.vUV = vertexInputs.uv; +#endif + + let ratio: f32 = min(1.0, vertexInputs.age / vertexInputs.life); +#ifdef COLORGRADIENTS +#ifdef COLORGRADIENTS_COLOR2 + let vColor1: vec4f = textureSampleLevel(colorGradientSampler, colorGradientSamplerSampler, vec2f(ratio, 0.25), 0.0); + let vColor2: vec4f = textureSampleLevel(colorGradientSampler, colorGradientSamplerSampler, vec2f(ratio, 0.75), 0.0); + vertexOutputs.vColor = mix(vColor1, vColor2, vertexInputs.seed.x); +#else + vertexOutputs.vColor = textureSampleLevel(colorGradientSampler, colorGradientSamplerSampler, vec2f(ratio, 0.0), 0.0); +#endif +#else + vertexOutputs.vColor = vertexInputs.color * vec4f(1.0 - ratio) + uniforms.colorDead * vec4f(ratio); +#endif + + let cornerPos: vec2f = (vertexInputs.offset - uniforms.translationPivot) * vertexInputs.size.yz * vertexInputs.size.x; + +#ifdef BILLBOARD + var rotatedCorner: vec4f; + rotatedCorner.w = 0.0; + +#ifdef BILLBOARDY + rotatedCorner.x = cornerPos.x * cos(vertexInputs.angle) - cornerPos.y * sin(vertexInputs.angle); + rotatedCorner.z = cornerPos.x * sin(vertexInputs.angle) + cornerPos.y * cos(vertexInputs.angle); + rotatedCorner.y = 0.0; + rotatedCorner.x += uniforms.translationPivot.x; + rotatedCorner.z += uniforms.translationPivot.y; + + var yaxis: vec3f = vertexInputs.position + uniforms.worldOffset - uniforms.eyePosition; + yaxis.y = 0.0; + vertexOutputs.vPositionW = rotate(normalize(yaxis), rotatedCorner.xyz); + + let viewPosition: vec4f = uniforms.view * vec4f(vertexOutputs.vPositionW, 1.0); +#elif defined(BILLBOARDSTRETCHED) + rotatedCorner.x = cornerPos.x * cos(vertexInputs.angle) - cornerPos.y * sin(vertexInputs.angle); + rotatedCorner.y = cornerPos.x * sin(vertexInputs.angle) + cornerPos.y * cos(vertexInputs.angle); + rotatedCorner.z = 0.0; + rotatedCorner.x += uniforms.translationPivot.x; + rotatedCorner.y += uniforms.translationPivot.y; + + let toCamera: vec3f = vertexInputs.position + uniforms.worldOffset - uniforms.eyePosition; + vertexOutputs.vPositionW = rotateAlign(toCamera, rotatedCorner.xyz); + + let viewPosition: vec4f = uniforms.view * vec4f(vertexOutputs.vPositionW, 1.0); +#else + rotatedCorner.x = cornerPos.x * cos(vertexInputs.angle) - cornerPos.y * sin(vertexInputs.angle); + rotatedCorner.y = cornerPos.x * sin(vertexInputs.angle) + cornerPos.y * cos(vertexInputs.angle); + rotatedCorner.z = 0.0; + rotatedCorner.x += uniforms.translationPivot.x; + rotatedCorner.y += uniforms.translationPivot.y; + + let viewPosition: vec4f = uniforms.view * vec4f(particleBasePosition(), 1.0) + rotatedCorner; + vertexOutputs.vPositionW = (uniforms.invView * viewPosition).xyz; +#endif + +#else + var rotatedCorner: vec3f; + rotatedCorner.x = cornerPos.x * cos(vertexInputs.angle) - cornerPos.y * sin(vertexInputs.angle); + rotatedCorner.y = 0.0; + rotatedCorner.z = cornerPos.x * sin(vertexInputs.angle) + cornerPos.y * cos(vertexInputs.angle); + rotatedCorner.x += uniforms.translationPivot.x; + rotatedCorner.z += uniforms.translationPivot.y; + + let yaxis: vec3f = normalize(vertexInputs.initialDirection); + vertexOutputs.vPositionW = rotate(yaxis, rotatedCorner); + + let viewPosition: vec4f = uniforms.view * vec4f(vertexOutputs.vPositionW, 1.0); +#endif + + vertexOutputs.position = uniforms.projection * viewPosition; + +#if defined(CLIPPLANE) || defined(CLIPPLANE2) || defined(CLIPPLANE3) || defined(CLIPPLANE4) || defined(CLIPPLANE5) || defined(CLIPPLANE6) || defined(FOG) + let worldPos: vec4f = vec4f(vertexOutputs.vPositionW, 1.0); +#endif + #include + #include + #include + +#ifdef EMITRATECTRL + if (shouldCullParticle) { + vertexOutputs.position = vec4f(0.0, 0.0, 2.0, 1.0); + vertexOutputs.vColor = vec4f(0.0); + vertexOutputs.vUV = vec2f(0.0); + vertexOutputs.vPositionW = vec3f(0.0); + } +#endif +} diff --git a/packages/dev/core/src/ShadersWGSL/lightingVolume.compute.fx b/packages/dev/core/src/ShadersWGSL/lightingVolume.compute.fx index 2445aff20b26..e09605831d98 100644 --- a/packages/dev/core/src/ShadersWGSL/lightingVolume.compute.fx +++ b/packages/dev/core/src/ShadersWGSL/lightingVolume.compute.fx @@ -8,7 +8,7 @@ struct Params { orthoMax: vec3f, }; -@group(0) @binding(0) var shadowMap : texture_2d; +@group(0) @binding(0) var shadowMap : texture_depth_2d; @group(0) @binding(1) var params : Params; @group(0) @binding(2) var positions : array; @@ -27,7 +27,7 @@ fn updateFarPlaneVertices(@builtin(global_invocation_id) global_id : vec3u) { let stepY = floor(params.step * f32(coord.y)); let depthCoord = vec2u(u32(floor(f32(coord.x) * params.step)), u32(stepY)); - var depth = textureLoad(shadowMap, depthCoord, 0).r; + var depth = textureLoad(shadowMap, depthCoord, 0); #ifdef MOVE_FAR_DEPTH_TO_NEAR if (depth == 1.0) { depth = 0.0; diff --git a/packages/dev/core/src/ShadersWGSL/noise.fragment.fx b/packages/dev/core/src/ShadersWGSL/noise.fragment.fx new file mode 100644 index 000000000000..e3c9377f4054 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/noise.fragment.fx @@ -0,0 +1,55 @@ +// Source: https://www.shadertoy.com/view/4lB3zz + +uniform brightness: f32; +uniform persistence: f32; +uniform timeScale: f32; + +varying vUV: vec2f; + +fn hash22(input: vec2f) -> vec2f { + var p = vec2f(dot(input, vec2f(127.1, 311.7)), dot(input, vec2f(269.5, 183.3))); + p = -vec2f(1.0) + 2.0 * fract(sin(p) * 43758.5453123); + return sin(p * 6.283 + uniforms.timeScale); +} + +fn interpolationNoise(p: vec2f) -> f32 { + let pi = floor(p); + let pf = p - pi; + + let w = pf * pf * (vec2f(3.0) - 2.0 * pf); + + let f00 = dot(hash22(pi + vec2f(0.0, 0.0)), pf - vec2f(0.0, 0.0)); + let f01 = dot(hash22(pi + vec2f(0.0, 1.0)), pf - vec2f(0.0, 1.0)); + let f10 = dot(hash22(pi + vec2f(1.0, 0.0)), pf - vec2f(1.0, 0.0)); + let f11 = dot(hash22(pi + vec2f(1.0, 1.0)), pf - vec2f(1.0, 1.0)); + + let xm1 = mix(f00, f10, w.x); + let xm2 = mix(f01, f11, w.x); + + return mix(xm1, xm2, w.y); +} + +fn perlinNoise2D(x: f32, y: f32) -> f32 { + var sum = 0.0; + var frequency = 0.0; + var amplitude = 0.0; + for (var i = 0; i < OCTAVES; i++) { + frequency = pow(2.0, f32(i)); + amplitude = pow(uniforms.persistence, f32(i)); + sum = sum + interpolationNoise(vec2f(x * frequency, y * frequency)) * amplitude; + } + + return sum; +} + +#define CUSTOM_FRAGMENT_DEFINITIONS + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + let x = abs(input.vUV.x); + let y = abs(input.vUV.y); + + let noise = uniforms.brightness + (1.0 - uniforms.brightness) * perlinNoise2D(x, y); + + fragmentOutputs.color = vec4f(noise, noise, noise, 1.0); +} diff --git a/packages/dev/core/test/unit/Particles/gpuParticleSystemAgeGradients.test.ts b/packages/dev/core/test/unit/Particles/gpuParticleSystemAgeGradients.test.ts index 4aa12882df00..1f29c3767f49 100644 --- a/packages/dev/core/test/unit/Particles/gpuParticleSystemAgeGradients.test.ts +++ b/packages/dev/core/test/unit/Particles/gpuParticleSystemAgeGradients.test.ts @@ -1,11 +1,20 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { NullEngine } from "core/Engines"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { NullEngine } from "core/Engines/nullEngine"; import { GPUParticleSystem } from "core/Particles/gpuParticleSystem"; import { Scene } from "core/scene"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; // Side-effect import to register the WebGL2ParticleSystem class import "core/Particles/webgl2ParticleSystem"; +function createReadyEffect(engine: NullEngine): any { + return { + isReady: () => true, + dispose: () => {}, + getEngine: () => engine, + }; +} + describe("GPUParticleSystem Emit Rate Gradients", () => { let engine: NullEngine; let scene: Scene; @@ -142,6 +151,45 @@ describe("GPUParticleSystem Emit Rate Gradients", () => { }); }); +describe("GPUParticleSystem WebGPU render shader", () => { + let engine: NullEngine; + let scene: Scene; + + beforeEach(() => { + engine = new NullEngine({ + renderHeight: 256, + renderWidth: 256, + textureSize: 256, + deterministicLockstep: false, + lockstepMaxSteps: 1, + }); + Object.defineProperty(engine, "isWebGPU", { + configurable: true, + value: true, + }); + scene = new Scene(engine); + }); + + afterEach(() => { + scene.dispose(); + engine.dispose(); + }); + + it("creates the render effect with WGSL under WebGPU", () => { + const ps = new GPUParticleSystem("test", { capacity: 100 }, scene); + const createEffect = vi.spyOn(engine, "createEffect").mockReturnValue(createReadyEffect(engine)); + + ps._getWrapper(0); + + expect(createEffect).toHaveBeenCalled(); + const options = createEffect.mock.calls[0][1] as any; + expect(options.shaderLanguage).toBe(ShaderLanguage.WGSL); + expect(options.extraInitializationsAsync).toBeTypeOf("function"); + + ps.dispose(); + }); +}); + describe("GPUParticleSystem Start Size Gradients", () => { let engine: NullEngine; let scene: Scene; diff --git a/packages/dev/gui/src/3D/materials/fluent/fluentMaterial.ts b/packages/dev/gui/src/3D/materials/fluent/fluentMaterial.ts index f6251b5df22e..19af008d37fc 100644 --- a/packages/dev/gui/src/3D/materials/fluent/fluentMaterial.ts +++ b/packages/dev/gui/src/3D/materials/fluent/fluentMaterial.ts @@ -14,6 +14,7 @@ import { type Mesh } from "core/Meshes/mesh"; import { type Scene } from "core/scene"; import { RegisterClass } from "core/Misc/typeStore"; import { Color3, Color4 } from "core/Maths/math.color"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; import "./shaders/fluent.vertex"; import "./shaders/fluent.fragment"; @@ -36,6 +37,8 @@ export class FluentMaterialDefines extends MaterialDefines { * Class used to render controls with fluent design */ export class FluentMaterial extends PushMaterial { + private _shadersLoaded = false; + /** * Gets or sets inner glow intensity. A value of 0 means no glow (default is 0.5) */ @@ -207,6 +210,7 @@ export class FluentMaterial extends PushMaterial { samplers: samplers, defines: defines, maxSimultaneousLights: 4, + shaderLanguage: this._shaderLanguage, }); const join = defines.toString(); @@ -223,6 +227,16 @@ export class FluentMaterial extends PushMaterial { onCompiled: this.onCompiled, onError: this.onError, indexParameters: { maxSimultaneousLights: 4 }, + shaderLanguage: this._shaderLanguage, + extraInitializationsAsync: this._shadersLoaded + ? undefined + : async () => { + if (this.shaderLanguage === ShaderLanguage.WGSL) { + await Promise.all([import("./wgsl/fluent.vertex"), import("./wgsl/fluent.fragment")]); + } + + this._shadersLoaded = true; + }, }, engine ), @@ -305,6 +319,10 @@ export class FluentMaterial extends PushMaterial { return false; } + /** + * Disposes the material. + * @param forceDisposeEffect specifies if effects should be forcefully disposed + */ public override dispose(forceDisposeEffect?: boolean): void { super.dispose(forceDisposeEffect); } @@ -324,6 +342,13 @@ export class FluentMaterial extends PushMaterial { } // Statics + /** + * Creates a fluent material from parsed material data. + * @param source defines the JSON representation of the material + * @param scene defines the hosting scene + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @returns a new fluent material + */ public static override Parse(source: any, scene: Scene, rootUrl: string): FluentMaterial { return SerializationHelper.Parse(() => new FluentMaterial(source.name, scene), source, scene, rootUrl); } diff --git a/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.fragment.fx b/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.fragment.fx new file mode 100644 index 000000000000..3cf5db650d77 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.fragment.fx @@ -0,0 +1,68 @@ +varying vUV: vec2f; + +uniform albedoColor: vec4f; + +#ifdef INNERGLOW +uniform innerGlowColor: vec4f; +#endif + +#ifdef BORDER +varying scaleInfo: vec2f; +uniform edgeSmoothingValue: f32; +uniform borderMinValue: f32; +#endif + +#ifdef HOVERLIGHT +varying worldPosition: vec3f; +uniform hoverPosition: vec3f; +uniform hoverColor: vec4f; +uniform hoverRadius: f32; +#endif + +#ifdef TEXTURE +uniform textureMatrix: mat4x4f; +var albedoSamplerSampler: sampler; +var albedoSampler: texture_2d; +#endif + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + var albedo: vec3f = uniforms.albedoColor.rgb; + var alpha: f32 = uniforms.albedoColor.a; + +#ifdef TEXTURE + let finalUV: vec2f = (uniforms.textureMatrix * vec4f(input.vUV, 1.0, 0.0)).xy; + albedo = textureSample(albedoSampler, albedoSamplerSampler, finalUV).rgb; +#endif + +#ifdef HOVERLIGHT + let pointToHover: f32 = (1.0 - clamp(length(uniforms.hoverPosition - input.worldPosition) / uniforms.hoverRadius, 0.0, 1.0)) * uniforms.hoverColor.a; + albedo = clamp(albedo + uniforms.hoverColor.rgb * pointToHover, vec3f(0.0), vec3f(1.0)); +#else + let pointToHover: f32 = 1.0; +#endif + +#ifdef BORDER + let borderPower: f32 = 10.0; + let inverseBorderPower: f32 = 1.0 / borderPower; + var borderColor: vec3f = albedo * borderPower; + let distanceToEdge: vec2f = abs(input.vUV - vec2f(0.5)) * 2.0; + let borderValue: f32 = max( + smoothstep(input.scaleInfo.x - uniforms.edgeSmoothingValue, input.scaleInfo.x + uniforms.edgeSmoothingValue, distanceToEdge.x), + smoothstep(input.scaleInfo.y - uniforms.edgeSmoothingValue, input.scaleInfo.y + uniforms.edgeSmoothingValue, distanceToEdge.y) + ); + borderColor = borderColor * borderValue * max(uniforms.borderMinValue * inverseBorderPower, pointToHover); + albedo += borderColor; + alpha = max(alpha, borderValue); +#endif + +#ifdef INNERGLOW + var uvGlow: vec2f = (input.vUV - vec2f(0.5)) * (uniforms.innerGlowColor.a * 2.0); + uvGlow = uvGlow * uvGlow; + uvGlow = uvGlow * uvGlow; + + albedo += mix(vec3f(0.0), uniforms.innerGlowColor.rgb, uvGlow.x + uvGlow.y); +#endif + + fragmentOutputs.color = vec4f(albedo, alpha); +} diff --git a/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.vertex.fx b/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.vertex.fx new file mode 100644 index 000000000000..a3a587987adc --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluent/wgsl/fluent.vertex.fx @@ -0,0 +1,71 @@ +// Attributes +attribute position: vec3f; +attribute normal: vec3f; +attribute uv: vec2f; + +// Uniforms +uniform world: mat4x4f; +uniform viewProjection: mat4x4f; + +// Output +varying vUV: vec2f; + +#ifdef BORDER +varying scaleInfo: vec2f; +uniform borderWidth: f32; +uniform scaleFactor: vec3f; +#endif + +#ifdef HOVERLIGHT +varying worldPosition: vec3f; +#endif + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + vertexOutputs.vUV = vertexInputs.uv; + +#ifdef BORDER + var scale: vec3f = uniforms.scaleFactor; + let minScale: f32 = min(min(scale.x, scale.y), scale.z); + let maxScale: f32 = max(max(scale.x, scale.y), scale.z); + let minOverMiddleScale: f32 = minScale / (scale.x + scale.y + scale.z - minScale - maxScale); + let areaYZ: f32 = scale.y * scale.z; + let areaXZ: f32 = scale.x * scale.z; + let areaXY: f32 = scale.x * scale.y; + var scaledBorderWidth: f32 = uniforms.borderWidth; + + if (abs(vertexInputs.normal.x) == 1.0) { + scale.x = scale.y; + scale.y = scale.z; + + if (areaYZ > areaXZ && areaYZ > areaXY) { + scaledBorderWidth *= minOverMiddleScale; + } + } else if (abs(vertexInputs.normal.y) == 1.0) { + scale.x = scale.z; + + if (areaXZ > areaXY && areaXZ > areaYZ) { + scaledBorderWidth *= minOverMiddleScale; + } + } else { + if (areaXY > areaYZ && areaXY > areaXZ) { + scaledBorderWidth *= minOverMiddleScale; + } + } + + let scaleRatio: f32 = min(scale.x, scale.y) / max(scale.x, scale.y); + if (scale.x > scale.y) { + vertexOutputs.scaleInfo = vec2f(1.0 - scaledBorderWidth * scaleRatio, 1.0 - scaledBorderWidth); + } else { + vertexOutputs.scaleInfo = vec2f(1.0 - scaledBorderWidth, 1.0 - scaledBorderWidth * scaleRatio); + } +#endif + + let worldPos: vec4f = uniforms.world * vec4f(vertexInputs.position, 1.0); + +#ifdef HOVERLIGHT + vertexOutputs.worldPosition = worldPos.xyz; +#endif + + vertexOutputs.position = uniforms.viewProjection * worldPos; +} diff --git a/packages/dev/gui/src/3D/materials/fluentBackplate/fluentBackplateMaterial.ts b/packages/dev/gui/src/3D/materials/fluentBackplate/fluentBackplateMaterial.ts index 22483cf77dfc..0b321c474fe2 100644 --- a/packages/dev/gui/src/3D/materials/fluentBackplate/fluentBackplateMaterial.ts +++ b/packages/dev/gui/src/3D/materials/fluentBackplate/fluentBackplateMaterial.ts @@ -18,6 +18,7 @@ import { RegisterClass } from "core/Misc/typeStore"; import { Color4 } from "core/Maths/math.color"; import { EffectFallbacks } from "core/Materials/effectFallbacks"; import { Constants } from "core/Engines/constants"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; import "./shaders/fluentBackplate.fragment"; import "./shaders/fluentBackplate.vertex"; @@ -42,6 +43,8 @@ class FluentBackplateMaterialDefines extends MaterialDefines { * Class used to render square buttons with fluent design */ export class FluentBackplateMaterial extends PushMaterial { + private _shadersLoaded = false; + /** * URL pointing to the texture used to define the coloring for the fluent blob effect. */ @@ -371,6 +374,7 @@ export class FluentBackplateMaterial extends PushMaterial { samplers: samplers, defines: defines, maxSimultaneousLights: 4, + shaderLanguage: this._shaderLanguage, }); subMesh.setEffect( @@ -386,6 +390,16 @@ export class FluentBackplateMaterial extends PushMaterial { onCompiled: this.onCompiled, onError: this.onError, indexParameters: { maxSimultaneousLights: 4 }, + shaderLanguage: this._shaderLanguage, + extraInitializationsAsync: this._shadersLoaded + ? undefined + : async () => { + if (this.shaderLanguage === ShaderLanguage.WGSL) { + await Promise.all([import("./wgsl/fluentBackplate.vertex"), import("./wgsl/fluentBackplate.fragment")]); + } + + this._shadersLoaded = true; + }, }, engine ), @@ -505,6 +519,10 @@ export class FluentBackplateMaterial extends PushMaterial { return []; } + /** + * Disposes the material. + * @param forceDisposeEffect specifies if effects should be forcefully disposed + */ public override dispose(forceDisposeEffect?: boolean): void { super.dispose(forceDisposeEffect); @@ -527,6 +545,13 @@ export class FluentBackplateMaterial extends PushMaterial { } // Statics + /** + * Creates a fluent backplate material from parsed material data. + * @param source defines the JSON representation of the material + * @param scene defines the hosting scene + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @returns a new fluent backplate material + */ public static override Parse(source: any, scene: Scene, rootUrl: string): FluentBackplateMaterial { return SerializationHelper.Parse(() => new FluentBackplateMaterial(source.name, scene), source, scene, rootUrl); } diff --git a/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.fragment.fx b/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.fragment.fx new file mode 100644 index 000000000000..0769883907a0 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.fragment.fx @@ -0,0 +1,101 @@ +uniform cameraPosition: vec3f; + +varying vPosition: vec3f; +varying vNormal: vec3f; +varying vUV: vec2f; +varying vTangent: vec3f; +varying vBinormal: vec3f; +varying vColor: vec4f; +varying vExtra1: vec4f; +varying vExtra2: vec4f; +varying vExtra3: vec4f; + +uniform _Filter_Width_: f32; +uniform _Base_Color_: vec4f; +uniform _Line_Color_: vec4f; +uniform _Rate_: f32; +uniform _Highlight_Color_: vec4f; +uniform _Highlight_Width_: f32; +uniform _Highlight_: f32; +uniform _Iridescence_Intensity_: f32; +uniform _Iridescence_Edge_Intensity_: f32; +uniform _Fade_Out_: f32; +uniform _Reflected_: f32; +uniform _Frequency_: f32; +uniform _Vertical_Offset_: f32; + +var _Blob_Texture_Sampler: sampler; +var _Blob_Texture_: texture_2d; +var _Iridescent_Map_Sampler: sampler; +var _Iridescent_Map_: texture_2d; + +fn roundRectFragment(radius: f32, lineWidth: f32, lineColor: vec4f, filterWidth: f32, uv: vec2f, rectParms: vec4f, fillColor: vec4f) -> vec4f { + let d: f32 = length(max(abs(uv) - rectParms.xy, vec2f(0.0))); + let dx: f32 = max(fwidth(d) * filterWidth, 0.00001); + let g: f32 = min(rectParms.z, rectParms.w); + let dgrad: f32 = max(fwidth(g) * filterWidth, 0.00001); + let insideRect: f32 = clamp(g / dgrad, 0.0, 1.0); + let inner: f32 = clamp((d + dx * 0.5 - max(radius - lineWidth, d - dx * 0.5)) / dx, 0.0, 1.0); + return clamp(mix(fillColor, lineColor, inner), vec4f(0.0), vec4f(1.0)) * insideRect; +} + +fn blobFragment(blobInfo1: vec4f, blobInfo2: vec4f) -> vec4f { + let k1: f32 = dot(blobInfo1.xy, blobInfo1.xy); + let k2: f32 = dot(blobInfo2.xy, blobInfo2.xy); + var closer: vec3f; + if (k1 < k2) { + closer = vec3f(k1, blobInfo1.z, blobInfo1.w); + } else { + closer = vec3f(k2, blobInfo2.z, blobInfo2.w); + } + + let blobUV: vec2f = vec2f(sqrt(closer.x), 1.0 - closer.y); + return closer.z * textureSample(_Blob_Texture_, _Blob_Texture_Sampler, blobUV) * clamp(1.0 - closer.x, 0.0, 1.0); +} + +fn lineFragment(baseColor: vec4f, highlightColor: vec4f, highlightWidth: f32, lineVertex: vec3f, highlight: f32) -> vec4f { + let k2: f32 = 1.0 - clamp(abs(lineVertex.y / highlightWidth), 0.0, 1.0); + return mix(baseColor, highlightColor, highlight * k2); +} + +fn scaleRGB(color: vec4f, scalar: f32) -> vec4f { + return vec4f(scalar, scalar, scalar, 1.0) * color; +} + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { +#ifdef BLOB_ENABLE + let blobColor: vec4f = blobFragment(input.vExtra2, input.vExtra3); +#else + let blobColor: vec4f = vec4f(0.0); +#endif + + let lineColor: vec4f = lineFragment(uniforms._Line_Color_, uniforms._Highlight_Color_, uniforms._Highlight_Width_, input.vTangent, uniforms._Highlight_); + let incident: vec3f = normalize(input.vPosition - uniforms.cameraPosition); + let reflected: vec3f = reflect(incident, input.vBinormal); + var reflectedResult: f32 = dot(input.vNormal, incident); + if (uniforms._Reflected_ > 0.5) { + reflectedResult = dot(incident, reflected); + } + + let iridescentU: f32 = fract((reflectedResult * uniforms._Frequency_ + 1.0) * 0.5 + input.vUV.y * uniforms._Vertical_Offset_); + let iridescentUV: vec2f = vec2f(iridescentU, 0.5); +#ifdef IRIDESCENT_MAP_ENABLE + let iridescentColor: vec4f = textureSample(_Iridescent_Map_, _Iridescent_Map_Sampler, iridescentUV); +#else + let iridescentColor: vec4f = vec4f(0.0); +#endif + + let lineAndIridescent: vec4f = lineColor + vec4f(scaleRGB(iridescentColor, uniforms._Iridescence_Edge_Intensity_).rgb, 0.0); + let fillAndIridescent: vec4f = uniforms._Base_Color_ + vec4f(scaleRGB(iridescentColor, uniforms._Iridescence_Intensity_).rgb, 0.0); + var lineOpaque: vec4f = lineAndIridescent; + lineOpaque.a = 1.0; + let fillWithBlob: vec4f = blobColor + (1.0 - blobColor.a) * fillAndIridescent; + let color: vec4f = uniforms._Fade_Out_ * roundRectFragment(input.vColor.r, input.vColor.g, lineOpaque, uniforms._Filter_Width_, input.vUV, input.vExtra1, fillWithBlob); + + if (color.a < 0.001) { + discard; + } + + fragmentOutputs.color = color; +} diff --git a/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.vertex.fx b/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.vertex.fx new file mode 100644 index 000000000000..c272f2e91ce8 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluentBackplate/wgsl/fluentBackplate.vertex.fx @@ -0,0 +1,223 @@ +uniform world: mat4x4f; +uniform viewProjection: mat4x4f; +uniform cameraPosition: vec3f; + +attribute position: vec3f; +attribute normal: vec3f; +#ifdef TANGENT +attribute tangent: vec3f; +#endif + +uniform _Radius_: f32; +uniform _Line_Width_: f32; +uniform _Absolute_Sizes_: f32; +uniform _Filter_Width_: f32; +uniform _Radius_Top_Left_: f32; +uniform _Radius_Top_Right_: f32; +uniform _Radius_Bottom_Left_: f32; +uniform _Radius_Bottom_Right_: f32; +uniform _Blob_Position_: vec3f; +uniform _Blob_Intensity_: f32; +uniform _Blob_Near_Size_: f32; +uniform _Blob_Far_Size_: f32; +uniform _Blob_Near_Distance_: f32; +uniform _Blob_Far_Distance_: f32; +uniform _Blob_Fade_Length_: f32; +uniform _Blob_Pulse_: f32; +uniform _Blob_Fade_: f32; +uniform _Blob_Position_2_: vec3f; +uniform _Blob_Near_Size_2_: f32; +uniform _Blob_Pulse_2_: f32; +uniform _Blob_Fade_2_: f32; +uniform _Rate_: f32; +uniform _Highlight_Transform_: vec4f; +uniform _Angle_: f32; +uniform _Use_Global_Left_Index_: f32; +uniform _Use_Global_Right_Index_: f32; +uniform Global_Left_Index_Tip_Position: vec4f; +uniform Global_Right_Index_Tip_Position: vec4f; + +varying vPosition: vec3f; +varying vNormal: vec3f; +varying vUV: vec2f; +varying vTangent: vec3f; +varying vBinormal: vec3f; +varying vColor: vec4f; +varying vExtra1: vec4f; +varying vExtra2: vec4f; +varying vExtra3: vec4f; + +fn pickDir(degrees: f32, dirX: vec3f, dirY: vec3f) -> vec3f { + let angle: f32 = degrees * 3.14159 / 180.0; + return cos(angle) * dirX + sin(angle) * dirY; +} + +fn moveVerts(anisotropy: f32, p: vec3f, radius: f32) -> array { + let uv: vec2f = p.xy * 2.0 + vec2f(0.5); + let center: vec2f = clamp(uv, vec2f(0.0), vec2f(1.0)); + let delta: vec2f = uv - center; + let r2: vec2f = 2.0 * vec2f(radius / anisotropy, radius); + let newUV: vec2f = center + r2 * (uv - 2.0 * center + vec2f(0.5)); + let newP: vec3f = vec3f(newUV - vec2f(0.5), p.z); + let radialGradient: f32 = 1.0 - length(delta) * 2.0; + let radialDir: vec3f = vec3f(delta * r2, 0.0); + return array(vec4f(newP, radialGradient), vec4f(newUV, radialDir.xy)); +} + +fn relativeOrAbsoluteDetail(nominalRadius: f32, nominalLineWidth: f32, absoluteMeasurements: f32, height: f32) -> vec2f { + var scale: f32 = 1.0; + if (absoluteMeasurements > 0.5) { + scale = 1.0 / height; + } + + return vec2f(nominalRadius * scale, nominalLineWidth * scale); +} + +fn edgeAAVertex( + positionWorld: vec3f, + positionObject: vec3f, + normalObject: vec3f, + eye: vec3f, + radialGradient: f32, + tangentObject: vec3f +) -> vec2f { + let incident: vec3f = eye - positionWorld; + let tangentWorld: vec3f = (uniforms.world * vec4f(tangentObject, 0.0)).xyz; + var g: f32 = 1.0; + if (dot(tangentWorld, incident) < 0.0) { + g = 0.0; + } + + if (normalObject.z == 0.0) { + if (positionObject.z > 0.0) { + return vec2f(g, 1.0); + } + + return vec2f(1.0, g); + } + + return vec2f(g + (1.0 - g) * radialGradient, 1.0); +} + +fn pickRadius(radius: f32, topLeft: f32, topRight: f32, bottomLeft: f32, bottomRight: f32, positionObject: vec3f) -> f32 { + var result: f32; + if (positionObject.x < 0.0) { + if (positionObject.y > 0.0) { + result = topLeft; + } else { + result = bottomLeft; + } + } else { + if (positionObject.y > 0.0) { + result = topRight; + } else { + result = bottomRight; + } + } + + return result * radius; +} + +fn blobVertex( + positionWorld: vec3f, + normalWorld: vec3f, + tangentWorld: vec3f, + binormalWorld: vec3f, + blobPosition: vec3f, + intensity: f32, + blobNearSize: f32, + blobFarSize: f32, + blobNearDistance: f32, + blobFarDistance: f32, + blobFadeLength: f32, + blobPulse: f32, + blobFade: f32 +) -> vec4f { + let delta: vec3f = blobPosition - positionWorld; + let dist: f32 = dot(normalWorld, delta); + let lerpValue: f32 = clamp((abs(dist) - blobNearDistance) / (blobFarDistance - blobNearDistance), 0.0, 1.0); + let fadeValue: f32 = 1.0 - clamp((abs(dist) - blobFarDistance) / blobFadeLength, 0.0, 1.0); + let size: f32 = blobNearSize + (blobFarSize - blobNearSize) * lerpValue; + let blobXY: vec2f = vec2f(dot(delta, tangentWorld), dot(delta, binormalWorld)) / (0.0001 + size); + let fade: f32 = fadeValue * intensity * blobFade; + let distance: f32 = (lerpValue * 0.5 + 0.5) * (1.0 - blobPulse); + return vec4f(blobXY.x, blobXY.y, distance, fade); +} + +fn roundRectVertex(uv: vec2f, radius: f32, margin: f32, anisotropy: f32, gradient1: f32, gradient2: f32) -> array { + let scaleXY: vec2f = vec2f(anisotropy, 1.0); + let lineUV: vec2f = uv - vec2f(0.5); + let rectUV: vec2f = lineUV * scaleXY; + let rectParms: vec4f = vec4f(scaleXY * 0.5 - vec2f(radius) - vec2f(margin), gradient1, gradient2); + return array(vec4f(rectUV, lineUV), rectParms); +} + +fn lineVertex(uv: vec2f, time: f32, rate: f32, highlightTransform: vec4f) -> vec3f { + let angle2: f32 = (rate * time) * 2.0 * 3.1416; + let sinAngle2: f32 = sin(angle2); + let cosAngle2: f32 = cos(angle2); + let xformUV: vec2f = uv * highlightTransform.xy + highlightTransform.zw; + return vec3f(0.0, cosAngle2 * xformUV.x - sinAngle2 * xformUV.y, 0.0); +} + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + let normalWorld: vec3f = normalize((uniforms.world * vec4f(vertexInputs.normal, 0.0)).xyz); + let tangentWorld: vec3f = (uniforms.world * vec4f(vec3f(1.0, 0.0, 0.0), 0.0)).xyz; + let tangentLength: f32 = length(tangentWorld); + let tangentWorldN: vec3f = tangentWorld / tangentLength; + let binormalWorld: vec3f = (uniforms.world * vec4f(vec3f(0.0, 1.0, 0.0), 0.0)).xyz; + let binormalLength: f32 = length(binormalWorld); + let binormalWorldN: vec3f = binormalWorld / binormalLength; + let anisotropy: f32 = tangentLength / binormalLength; + let blobPositionLeft: vec3f = mix(uniforms._Blob_Position_, uniforms.Global_Left_Index_Tip_Position.xyz, step(0.5, uniforms._Use_Global_Left_Index_)); + let blobPositionRight: vec3f = mix(uniforms._Blob_Position_2_, uniforms.Global_Right_Index_Tip_Position.xyz, step(0.5, uniforms._Use_Global_Right_Index_)); + let radiusMultiplier: f32 = pickRadius(uniforms._Radius_, uniforms._Radius_Top_Left_, uniforms._Radius_Top_Right_, uniforms._Radius_Bottom_Left_, uniforms._Radius_Bottom_Right_, vertexInputs.position); + let normalDir: vec3f = pickDir(uniforms._Angle_, tangentWorldN, binormalWorldN); + let detail: vec2f = relativeOrAbsoluteDetail(radiusMultiplier, uniforms._Line_Width_, uniforms._Absolute_Sizes_, binormalLength); + let radius: f32 = detail.x; + let lineWidth: f32 = detail.y; + let moved: array = moveVerts(anisotropy, vertexInputs.position, radius); + let newP: vec3f = moved[0].xyz; + let radialGradient: f32 = moved[0].w; + let newUV: vec2f = moved[1].xy; + let positionWorld: vec3f = (uniforms.world * vec4f(newP, 1.0)).xyz; + +#ifdef BLOB_ENABLE + let blobInfoLeft: vec4f = blobVertex(positionWorld, normalWorld, tangentWorldN, binormalWorldN, blobPositionLeft, uniforms._Blob_Intensity_, uniforms._Blob_Near_Size_, uniforms._Blob_Far_Size_, uniforms._Blob_Near_Distance_, uniforms._Blob_Far_Distance_, uniforms._Blob_Fade_Length_, uniforms._Blob_Pulse_, uniforms._Blob_Fade_); +#else + let blobInfoLeft: vec4f = vec4f(0.0); +#endif + +#ifdef BLOB_ENABLE_2 + let blobInfoRight: vec4f = blobVertex(positionWorld, normalWorld, tangentWorldN, binormalWorldN, blobPositionRight, uniforms._Blob_Intensity_, uniforms._Blob_Near_Size_2_, uniforms._Blob_Far_Size_, uniforms._Blob_Near_Distance_, uniforms._Blob_Far_Distance_, uniforms._Blob_Fade_Length_, uniforms._Blob_Pulse_2_, uniforms._Blob_Fade_2_); +#else + let blobInfoRight: vec4f = vec4f(0.0); +#endif + +#ifdef TANGENT + let tangentObject: vec3f = vertexInputs.tangent; +#else + let tangentObject: vec3f = vec3f(0.0); +#endif + +#ifdef SMOOTH_EDGES + let edgeGradient: vec2f = edgeAAVertex(positionWorld, vertexInputs.position, vertexInputs.normal, uniforms.cameraPosition, radialGradient, tangentObject); +#else + let edgeGradient: vec2f = vec2f(1.0); +#endif + + let rectData: array = roundRectVertex(newUV, radius, 0.0, anisotropy, edgeGradient.x, edgeGradient.y); + let lineVx: vec3f = lineVertex(rectData[0].zw, 0.0, uniforms._Rate_, uniforms._Highlight_Transform_); + + vertexOutputs.position = uniforms.viewProjection * vec4f(positionWorld, 1.0); + vertexOutputs.vPosition = positionWorld; + vertexOutputs.vNormal = normalDir; + vertexOutputs.vUV = rectData[0].xy; + vertexOutputs.vTangent = lineVx; + vertexOutputs.vBinormal = normalWorld; + vertexOutputs.vColor = vec4f(radius, lineWidth, 0.0, 1.0); + vertexOutputs.vExtra1 = rectData[1]; + vertexOutputs.vExtra2 = blobInfoLeft; + vertexOutputs.vExtra3 = blobInfoRight; +} diff --git a/packages/dev/gui/src/3D/materials/fluentButton/fluentButtonMaterial.ts b/packages/dev/gui/src/3D/materials/fluentButton/fluentButtonMaterial.ts index 5374891c6cea..9098ce458c37 100644 --- a/packages/dev/gui/src/3D/materials/fluentButton/fluentButtonMaterial.ts +++ b/packages/dev/gui/src/3D/materials/fluentButton/fluentButtonMaterial.ts @@ -18,6 +18,7 @@ import { RegisterClass } from "core/Misc/typeStore"; import { Color3, Color4 } from "core/Maths/math.color"; import { EffectFallbacks } from "core/Materials/effectFallbacks"; import { Constants } from "core/Engines/constants"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; import "./shaders/fluentButton.fragment"; import "./shaders/fluentButton.vertex"; @@ -42,6 +43,8 @@ class FluentButtonMaterialDefines extends MaterialDefines { * @since 5.0.0 */ export class FluentButtonMaterial extends PushMaterial { + private _shadersLoaded = false; + /** * URL pointing to the texture used to define the coloring for the fluent blob effect. */ @@ -429,6 +432,7 @@ export class FluentButtonMaterial extends PushMaterial { samplers: samplers, defines: defines, maxSimultaneousLights: 4, + shaderLanguage: this._shaderLanguage, }); subMesh.setEffect( @@ -444,6 +448,16 @@ export class FluentButtonMaterial extends PushMaterial { onCompiled: this.onCompiled, onError: this.onError, indexParameters: { maxSimultaneousLights: 4 }, + shaderLanguage: this._shaderLanguage, + extraInitializationsAsync: this._shadersLoaded + ? undefined + : async () => { + if (this.shaderLanguage === ShaderLanguage.WGSL) { + await Promise.all([import("./wgsl/fluentButton.vertex"), import("./wgsl/fluentButton.fragment")]); + } + + this._shadersLoaded = true; + }, }, engine ), @@ -559,6 +573,10 @@ export class FluentButtonMaterial extends PushMaterial { return []; } + /** + * Disposes the material. + * @param forceDisposeEffect specifies if effects should be forcefully disposed + */ public override dispose(forceDisposeEffect?: boolean): void { super.dispose(forceDisposeEffect); } @@ -578,6 +596,13 @@ export class FluentButtonMaterial extends PushMaterial { } // Statics + /** + * Creates a fluent button material from parsed material data. + * @param source defines the JSON representation of the material + * @param scene defines the hosting scene + * @param rootUrl defines the root URL to use to load textures and relative dependencies + * @returns a new fluent button material + */ public static override Parse(source: any, scene: Scene, rootUrl: string): FluentButtonMaterial { return SerializationHelper.Parse(() => new FluentButtonMaterial(source.name, scene), source, scene, rootUrl); } diff --git a/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.fragment.fx b/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.fragment.fx new file mode 100644 index 000000000000..b0d6ae3f8b04 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.fragment.fx @@ -0,0 +1,60 @@ +varying vPosition: vec3f; +varying vNormal: vec3f; +varying vUV: vec2f; +varying vTangent: vec3f; +varying vBinormal: vec3f; +varying vColor: vec4f; +varying vExtra1: vec4f; + +uniform _Edge_Color_: vec4f; +uniform _Proximity_Max_Intensity_: f32; +uniform _Proximity_Near_Radius_: f32; +uniform _Fade_Width_: f32; +uniform _Blob_Position_: vec3f; +uniform _Blob_Position_2_: vec3f; +uniform _Use_Blob_Texture_: f32; +uniform _Show_Frame_: f32; + +uniform Use_Global_Left_Index: f32; +uniform Use_Global_Right_Index: f32; +uniform Global_Left_Index_Tip_Position: vec4f; +uniform Global_Right_Index_Tip_Position: vec4f; + +var _Blob_Texture_Sampler: sampler; +var _Blob_Texture_: texture_2d; + +fn filterStep(edge: vec2f, x: vec2f) -> vec2f { + let dX: vec2f = max(fwidth(x), vec2f(0.00001)); + return clamp((x + dX - max(edge, x - dX)) / (dX * 2.0), vec2f(0.0), vec2f(1.0)); +} + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { +#ifdef ENABLE_FADE + let edgeC: vec2f = vec2f(min(input.vColor.r, input.vColor.g), min(input.vColor.b, input.vColor.a)); + let edgeDf: vec2f = fwidth(edgeC) * uniforms._Fade_Width_; + let edgeG: vec2f = clamp(edgeC / edgeDf, vec2f(0.0), vec2f(1.0)); + let notEdge: f32 = edgeG.x * edgeG.y; +#else + let notEdge: f32 = 1.0; +#endif + + let k: f32 = dot(input.vUV, input.vUV); + let blobTextureCoord: vec2f = vec2f(sqrt(k), 1.0 - input.vTangent.x); + let proceduralBlob: vec4f = vec4f(1.0) * step(1.0 - input.vTangent.x, clamp(sqrt(k) + 0.1, 0.0, 1.0)); + let sampledBlob: vec4f = textureSample(_Blob_Texture_, _Blob_Texture_Sampler, blobTextureCoord); + let blobColor: vec4f = mix(proceduralBlob, sampledBlob, step(0.5, uniforms._Use_Blob_Texture_)); + let blob: vec4f = input.vTangent.y * blobColor * (1.0 - clamp(k, 0.0, 1.0)); + + let blobPositionLeft: vec3f = mix(uniforms._Blob_Position_, uniforms.Global_Left_Index_Tip_Position.xyz, step(0.5, uniforms.Use_Global_Left_Index)); + let blobPositionRight: vec3f = mix(uniforms._Blob_Position_2_, uniforms.Global_Right_Index_Tip_Position.xyz, step(0.5, uniforms.Use_Global_Right_Index)); + let distanceXY: f32 = sqrt(min(dot(input.vExtra1.xy, input.vExtra1.xy), dot(input.vExtra1.zw, input.vExtra1.zw)) + input.vBinormal.y * input.vBinormal.y); + let proximity: f32 = input.vBinormal.z * uniforms._Proximity_Max_Intensity_ * (1.0 - clamp(distanceXY / uniforms._Proximity_Near_Radius_, 0.0, 1.0)) * (1.0 - input.vBinormal.x) + input.vBinormal.x; + let edgeUV: vec2f = min(input.vUV, vec2f(1.0) - input.vUV); + let wireFilter: vec2f = filterStep(input.vNormal.xy * 0.5, edgeUV); + let wireframe: vec4f = (1.0 - min(wireFilter.x, wireFilter.y)) * proximity * uniforms._Edge_Color_; + + let wireOrBlob: vec4f = mix(wireframe, blob, input.vNormal.z); + let color: vec4f = mix(wireOrBlob, vec4f(0.3, 0.3, 0.3, 0.3), step(0.5, uniforms._Show_Frame_)); + fragmentOutputs.color = notEdge * color; +} diff --git a/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.vertex.fx b/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.vertex.fx new file mode 100644 index 000000000000..bf3d6edc1a42 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/fluentButton/wgsl/fluentButton.vertex.fx @@ -0,0 +1,186 @@ +uniform world: mat4x4f; +uniform viewProjection: mat4x4f; +uniform cameraPosition: vec3f; + +attribute position: vec3f; +attribute normal: vec3f; +attribute uv: vec2f; +attribute tangent: vec3f; +attribute color: vec4f; + +uniform _Edge_Width_: f32; +uniform _Proximity_Far_Distance_: f32; +uniform _Proximity_Near_Radius_: f32; +uniform _Proximity_Anisotropy_: f32; +uniform _Selection_Fuzz_: f32; +uniform _Selected_: f32; +uniform _Selection_Fade_: f32; +uniform _Selection_Fade_Size_: f32; +uniform _Selected_Distance_: f32; +uniform _Selected_Fade_Length_: f32; +uniform _Blob_Enable_: f32; +uniform _Blob_Position_: vec3f; +uniform _Blob_Intensity_: f32; +uniform _Blob_Near_Size_: f32; +uniform _Blob_Far_Size_: f32; +uniform _Blob_Near_Distance_: f32; +uniform _Blob_Far_Distance_: f32; +uniform _Blob_Fade_Length_: f32; +uniform _Blob_Inner_Fade_: f32; +uniform _Blob_Pulse_: f32; +uniform _Blob_Fade_: f32; +uniform _Blob_Enable_2_: f32; +uniform _Blob_Position_2_: vec3f; +uniform _Blob_Near_Size_2_: f32; +uniform _Blob_Inner_Fade_2_: f32; +uniform _Blob_Pulse_2_: f32; +uniform _Blob_Fade_2_: f32; +uniform _Active_Face_Dir_: vec3f; +uniform _Active_Face_Up_: vec3f; +uniform _Smooth_Active_Face_: f32; + +uniform Use_Global_Left_Index: f32; +uniform Use_Global_Right_Index: f32; +uniform Global_Left_Index_Tip_Position: vec4f; +uniform Global_Right_Index_Tip_Position: vec4f; + +varying vPosition: vec3f; +varying vNormal: vec3f; +varying vUV: vec2f; +varying vTangent: vec3f; +varying vBinormal: vec3f; +varying vColor: vec4f; +varying vExtra1: vec4f; + +fn ramp2(start: vec2f, end: vec2f, x: vec2f) -> vec2f { + return clamp((x - start) / (end - start), vec2f(0.0), vec2f(1.0)); +} + +fn computeSelection( + blobPosition: vec3f, + normal: vec3f, + tangentDir: vec3f, + bitangent: vec3f, + faceCenter: vec3f, + faceSize: vec2f, + selectionFuzz: f32, + farDistance: f32, + fadeLength: f32 +) -> f32 { + let delta: vec3f = blobPosition - faceCenter; + let absD: f32 = abs(dot(delta, normal)); + let fadeIn: f32 = 1.0 - clamp((absD - farDistance) / fadeLength, 0.0, 1.0); + let blobCenterXY: vec2f = vec2f(dot(delta, tangentDir), dot(delta, bitangent)); + let innerFace: vec2f = faceSize * (1.0 - selectionFuzz) * 0.5; + let selectPulse: vec2f = ramp2(-faceSize * 0.5, -innerFace, blobCenterXY) - ramp2(innerFace, faceSize * 0.5, blobCenterXY); + return selectPulse.x * selectPulse.y * fadeIn; +} + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + let blobPositionLeft: vec3f = mix(uniforms._Blob_Position_, uniforms.Global_Left_Index_Tip_Position.xyz, step(0.5, uniforms.Use_Global_Left_Index)); + let blobPositionRight: vec3f = mix(uniforms._Blob_Position_2_, uniforms.Global_Right_Index_Tip_Position.xyz, step(0.5, uniforms.Use_Global_Right_Index)); + let activeFaceCenter: vec3f = (uniforms.world * vec4f(uniforms._Active_Face_Dir_ * 0.5, 1.0)).xyz; + let activeFaceDir: vec3f = normalize((uniforms.world * vec4f(uniforms._Active_Face_Dir_, 0.0)).xyz); + +#ifdef RELATIVE_WIDTH + let relativeScale: f32 = length((uniforms.world * vec4f(vec3f(0.0, 1.0, 0.0), 0.0)).xyz); +#else + let relativeScale: f32 = 1.0; +#endif + + let tangentWorld: vec3f = (uniforms.world * vec4f(vertexInputs.tangent, 0.0)).xyz; + let binormalObject: vec3f = cross(vertexInputs.normal, vertexInputs.tangent); + let binormalWorld: vec3f = (uniforms.world * vec4f(binormalObject, 0.0)).xyz; + let normalWorld: vec3f = (uniforms.world * vec4f(vertexInputs.normal, 0.0)).xyz; + let normalWorldN: vec3f = normalize(normalWorld); + let tangentWorldN: vec3f = normalize(tangentWorld); + let binormalWorldN: vec3f = normalize(binormalWorld); + let faceCenter: vec3f = (uniforms.world * vec4f(0.5 * vertexInputs.normal, 1.0)).xyz; + let faceSize: vec2f = vec2f(length(tangentWorld), length(binormalWorld)); + + let chosenBlobPosition: vec3f = mix(blobPositionLeft, blobPositionRight, vertexInputs.color.g); + let chosenBlobEnable: f32 = mix(uniforms._Blob_Enable_, uniforms._Blob_Enable_2_, vertexInputs.color.g); + let chosenBlobPulse: f32 = mix(uniforms._Blob_Pulse_, uniforms._Blob_Pulse_2_, vertexInputs.color.g); + let chosenBlobFade: f32 = mix(uniforms._Blob_Fade_, uniforms._Blob_Fade_2_, vertexInputs.color.g); + let chosenBlobNearSize: f32 = mix(uniforms._Blob_Near_Size_, uniforms._Blob_Near_Size_2_, vertexInputs.color.g); + let chosenBlobInnerFade: f32 = mix(uniforms._Blob_Inner_Fade_, uniforms._Blob_Inner_Fade_2_, vertexInputs.color.g); + + let selectLeft: f32 = computeSelection(blobPositionLeft, normalWorldN, tangentWorldN, binormalWorldN, faceCenter, faceSize, uniforms._Selection_Fuzz_, uniforms._Selected_Distance_, uniforms._Selected_Fade_Length_); + let selectRight: f32 = computeSelection(blobPositionRight, normalWorldN, tangentWorldN, binormalWorldN, faceCenter, faceSize, uniforms._Selection_Fuzz_, uniforms._Selected_Distance_, uniforms._Selected_Fade_Length_); + let activeFaceAmount: f32 = max(0.0, dot(activeFaceDir, normalWorldN)); + let showSelection: f32 = mix(max(selectLeft, selectRight), 1.0, uniforms._Selected_) * activeFaceAmount; + + let upWorld: vec3f = normalize((uniforms.world * vec4f(uniforms._Active_Face_Up_, 0.0)).xyz); + let edgeWidth: f32 = uniforms._Edge_Width_ * relativeScale; + + let boxEdges: vec3f = (uniforms.world * vec4f(vec3f(0.5), 0.0)).xyz; + let boxMaxSize: f32 = length(boxEdges); + let d1: f32 = dot(blobPositionLeft - activeFaceCenter, activeFaceDir); + let d2: f32 = dot(blobPositionRight - activeFaceCenter, activeFaceDir); + let projectedBlobLeft: vec3f = blobPositionLeft - d1 * activeFaceDir; + let projectedBlobRight: vec3f = blobPositionRight - d2 * activeFaceDir; + let nearestProxDist: f32 = sqrt(min(dot(projectedBlobLeft - activeFaceCenter, projectedBlobLeft - activeFaceCenter), dot(projectedBlobRight - activeFaceCenter, projectedBlobRight - activeFaceCenter))); + let visibleWidth: f32 = edgeWidth * (1.0 - step(boxMaxSize + uniforms._Proximity_Near_Radius_, nearestProxDist)) * (1.0 - step(uniforms._Proximity_Far_Distance_, min(d1, d2)) * (1.0 - step(0.0001, showSelection))); + + let widths: vec2f = visibleWidth / faceSize; + let px: f32 = dot(vertexInputs.position, vertexInputs.tangent); + let py: f32 = dot(vertexInputs.position, binormalObject); + let dx: f32 = 0.5 - abs(px); + let dy: f32 = 0.5 - abs(py); + let newX: f32 = (0.5 - dx * widths.x * 2.0) * sign(px); + let newY: f32 = (0.5 - dy * widths.y * 2.0) * sign(py); + let wirePosObject: vec3f = vertexInputs.normal * 0.5 + newX * vertexInputs.tangent + newY * binormalObject; + let wireUV: vec2f = vec2f(dot(wirePosObject, vertexInputs.tangent) + 0.5, dot(wirePosObject, binormalObject) + 0.5); + let wireWorldPos: vec3f = (uniforms.world * vec4f(wirePosObject, 1.0)).xyz; + + let hitDistance: f32 = dot(chosenBlobPosition - faceCenter, normalWorldN); + let hitPosition: vec3f = chosenBlobPosition - hitDistance * normalWorldN; + let absHitDistance: f32 = abs(hitDistance); + let blobLerp: f32 = clamp((absHitDistance - uniforms._Blob_Near_Distance_) / (uniforms._Blob_Far_Distance_ - uniforms._Blob_Near_Distance_), 0.0, 1.0); + let fadeIn: f32 = 1.0 - clamp((absHitDistance - uniforms._Blob_Far_Distance_) / uniforms._Blob_Fade_Length_, 0.0, 1.0); + let innerFade: f32 = 1.0 - clamp(-hitDistance / chosenBlobInnerFade, 0.0, 1.0); + let farClip: f32 = clamp(1.0 - step(uniforms._Blob_Far_Distance_ + uniforms._Blob_Fade_Length_, absHitDistance), 0.0, 1.0); + let blobSize: f32 = mix(mix(chosenBlobNearSize, uniforms._Blob_Far_Size_, blobLerp) * farClip, uniforms._Selection_Fade_Size_, uniforms._Selection_Fade_) * innerFade * chosenBlobEnable; + var blobInfo: vec3f = vec3f(blobLerp * 0.5 + 0.5, fadeIn * uniforms._Blob_Intensity_ * (1.0 - uniforms._Selection_Fade_) * chosenBlobFade, 0.0); + blobInfo.x *= 1.0 - chosenBlobPulse; + + let blobDelta: vec3f = hitPosition - faceCenter; + let blobCenterXY: vec2f = vec2f(dot(blobDelta, tangentWorldN), dot(blobDelta, binormalWorldN)); + let quadUV: vec2f = 2.0 * vertexInputs.uv - vec2f(1.0); + let blobXY: vec2f = blobCenterXY + quadUV * blobSize; + let blobClipped: vec2f = clamp(blobXY, -faceSize * 0.5, faceSize * 0.5); + let blobUV: vec2f = (blobClipped - blobCenterXY) / max(blobSize, 0.0001) * 2.0; + let blobWorldPos: vec3f = faceCenter + blobClipped.x * tangentWorldN + blobClipped.y * binormalWorldN; + let outWorldPos: vec3f = mix(wireWorldPos, blobWorldPos, vertexInputs.color.r); + let outUV: vec2f = mix(wireUV, blobUV, vertexInputs.color.r); + + let incident: vec3f = normalize(wireWorldPos - uniforms.cameraPosition); + let ndotI: f32 = dot(incident, normalWorldN); + let uvFlip: vec2f = vertexInputs.uv - vec2f(0.5); + let uDot: f32 = dot(incident, tangentWorld) * uvFlip.x * ndotI; + let vDot: f32 = -dot(incident, binormalWorld) * uvFlip.y * ndotI; + let smoothAndActive: f32 = step(1.0, step(0.5, uniforms._Smooth_Active_Face_) * step(0.0001, activeFaceAmount)); + let uVal: f32 = mix(1.0 - step(0.0, uDot), max(1.0, 1.0 - step(0.0, uDot)), smoothAndActive); + let vVal: f32 = mix(1.0 - step(0.0, vDot), max(1.0, 1.0 - step(0.0, vDot)), smoothAndActive); + let holoEdges: vec4f = vec4f(1.0) - vec4f(uVal * vertexInputs.uv.x, uVal * (1.0 - vertexInputs.uv.x), vVal * vertexInputs.uv.y, vVal * (1.0 - vertexInputs.uv.y)); + + let xDir: vec3f = normalize(cross(activeFaceDir, upWorld)) * uniforms._Proximity_Anisotropy_; + let yDir: vec3f = cross(activeFaceDir, normalize(cross(activeFaceDir, upWorld))); + let deltaLeft: vec3f = blobPositionLeft - wireWorldPos; + let deltaRight: vec3f = blobPositionRight - wireWorldPos; + let distLeft: f32 = abs(dot(deltaLeft, activeFaceDir)); + let distRight: f32 = abs(dot(deltaRight, activeFaceDir)); + let extra1: vec4f = vec4f(dot(deltaLeft, xDir), dot(deltaLeft, yDir), dot(deltaRight, xDir), dot(deltaRight, yDir)) / relativeScale; + let distanceToFace: f32 = dot(activeFaceDir, wireWorldPos - activeFaceCenter); + let intensity: f32 = 1.0 - clamp(min(distLeft, distRight) / uniforms._Proximity_Far_Distance_, 0.0, 1.0); + + vertexOutputs.position = uniforms.viewProjection * vec4f(outWorldPos, 1.0); + vertexOutputs.vPosition = outWorldPos; + vertexOutputs.vNormal = vec3f(widths.x, widths.y, vertexInputs.color.r); + vertexOutputs.vUV = outUV; + vertexOutputs.vTangent = blobInfo; + vertexOutputs.vBinormal = vec3f(showSelection, distanceToFace, intensity); + vertexOutputs.vColor = holoEdges; + vertexOutputs.vExtra1 = extra1; +} diff --git a/packages/dev/gui/src/3D/materials/handle/handleMaterial.ts b/packages/dev/gui/src/3D/materials/handle/handleMaterial.ts index 42a1e9e90307..a58115150a4d 100644 --- a/packages/dev/gui/src/3D/materials/handle/handleMaterial.ts +++ b/packages/dev/gui/src/3D/materials/handle/handleMaterial.ts @@ -4,6 +4,7 @@ import { type Nullable } from "core/types"; import { type Observer } from "core/Misc/observable"; import { Color3, TmpColors } from "core/Maths/math.color"; import { Vector3 } from "core/Maths/math.vector"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; import "./shaders/handle.vertex"; import "./shaders/handle.fragment"; @@ -88,11 +89,20 @@ export class HandleMaterial extends ShaderMaterial { * @param scene Scene */ constructor(name: string, scene: Scene) { + const shaderLanguage = scene.getEngine().isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL; + super(name, scene, "handle", { attributes: ["position"], uniforms: ["worldViewProjection", "color", "scale", "positionOffset"], needAlphaBlending: false, needAlphaTesting: false, + shaderLanguage, + extraInitializationsAsync: + shaderLanguage === ShaderLanguage.WGSL + ? async () => { + await Promise.all([import("./wgsl/handle.vertex"), import("./wgsl/handle.fragment")]); + } + : undefined, }); this._updateInterpolationTarget(); diff --git a/packages/dev/gui/src/3D/materials/handle/wgsl/handle.fragment.fx b/packages/dev/gui/src/3D/materials/handle/wgsl/handle.fragment.fx new file mode 100644 index 000000000000..00f7a70dc26d --- /dev/null +++ b/packages/dev/gui/src/3D/materials/handle/wgsl/handle.fragment.fx @@ -0,0 +1,6 @@ +uniform color: vec3f; + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + fragmentOutputs.color = vec4f(uniforms.color, 1.0); +} diff --git a/packages/dev/gui/src/3D/materials/handle/wgsl/handle.vertex.fx b/packages/dev/gui/src/3D/materials/handle/wgsl/handle.vertex.fx new file mode 100644 index 000000000000..04b296c310d1 --- /dev/null +++ b/packages/dev/gui/src/3D/materials/handle/wgsl/handle.vertex.fx @@ -0,0 +1,11 @@ +attribute position: vec3f; + +uniform positionOffset: vec3f; +uniform worldViewProjection: mat4x4f; +uniform scale: f32; + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + let vPos: vec4f = vec4f((vertexInputs.position + uniforms.positionOffset) * uniforms.scale, 1.0); + vertexOutputs.position = uniforms.worldViewProjection * vPos; +} diff --git a/packages/dev/gui/test/unit/gui3DWebGPUShaders.test.ts b/packages/dev/gui/test/unit/gui3DWebGPUShaders.test.ts new file mode 100644 index 000000000000..05999c52d632 --- /dev/null +++ b/packages/dev/gui/test/unit/gui3DWebGPUShaders.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { NullEngine } from "core/Engines/nullEngine"; +import { MeshBuilder } from "core/Meshes/meshBuilder"; +import { Scene } from "core/scene"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; +import { FluentMaterial } from "../../src/3D/materials/fluent/fluentMaterial"; +import { FluentBackplateMaterial } from "../../src/3D/materials/fluentBackplate/fluentBackplateMaterial"; +import { HandleMaterial } from "../../src/3D/materials/handle/handleMaterial"; + +function createWebGPUNullEngine(): NullEngine { + const engine = new NullEngine({ + renderHeight: 256, + renderWidth: 256, + textureSize: 256, + }); + + Object.defineProperty(engine, "isWebGPU", { + configurable: true, + value: true, + }); + + return engine; +} + +function createReadyEffect(engine: NullEngine): any { + return { + isReady: () => true, + dispose: () => {}, + getEngine: () => engine, + }; +} + +describe("GUI3D WebGPU shaders", () => { + let engine: NullEngine | undefined; + let scene: Scene | undefined; + + afterEach(() => { + scene?.dispose(); + engine?.dispose(); + scene = undefined; + engine = undefined; + }); + + it("uses WGSL when FluentMaterial creates an effect under WebGPU", () => { + engine = createWebGPUNullEngine(); + scene = new Scene(engine); + const mesh = MeshBuilder.CreateBox("box", {}, scene); + const material = new FluentMaterial("fluent", scene); + const createEffect = vi.spyOn(engine, "createEffect").mockReturnValue(createReadyEffect(engine)); + + expect(material.isReadyForSubMesh(mesh, mesh.subMeshes[0])).toBe(true); + expect(createEffect).toHaveBeenCalled(); + + const options = createEffect.mock.calls[0][1] as any; + expect(options.shaderLanguage).toBe(ShaderLanguage.WGSL); + expect(options.extraInitializationsAsync).toBeTypeOf("function"); + }); + + it("uses WGSL when FluentBackplateMaterial creates an effect under WebGPU", () => { + engine = createWebGPUNullEngine(); + scene = new Scene(engine); + const mesh = MeshBuilder.CreatePlane("plane", {}, scene); + const material = new FluentBackplateMaterial("backplate", scene); + (material as any)._blobTexture.isReady = () => true; + (material as any)._iridescentMap.isReady = () => true; + const createEffect = vi.spyOn(engine, "createEffect").mockReturnValue(createReadyEffect(engine)); + + expect(material.isReadyForSubMesh(mesh, mesh.subMeshes[0])).toBe(true); + expect(createEffect).toHaveBeenCalled(); + + const options = createEffect.mock.calls[0][1] as any; + expect(options.shaderLanguage).toBe(ShaderLanguage.WGSL); + expect(options.extraInitializationsAsync).toBeTypeOf("function"); + }); + + it("uses WGSL for HandleMaterial under WebGPU", () => { + engine = createWebGPUNullEngine(); + scene = new Scene(engine); + const material = new HandleMaterial("handle", scene); + + expect(material.options.shaderLanguage).toBe(ShaderLanguage.WGSL); + expect(material.options.extraInitializationsAsync).toBeTypeOf("function"); + }); +});