diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-10 17:47:15 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-10 17:47:15 +0100 |
| commit | ae810e1a9c68d05bee254ef570fbb0e783e25931 (patch) | |
| tree | 407f3d7d8e03da07d8f7995df78d37be7d1f32c1 /tools/shadertoy | |
| parent | f3c7ef8cd612f5ac908f39310c4c11566879313f (diff) | |
feat: Add ShaderToy conversion tools
Add automated conversion pipeline for ShaderToy shaders to demo effects:
- convert_shadertoy.py: Automated code generation script
- Manual templates: Header, implementation, and WGSL boilerplate
- Example shader: Test case for conversion workflow
- README: Complete conversion guide with examples
Handles basic GLSL→WGSL conversion (types, uniforms, mainImage extraction).
Manual fixes needed for fragColor returns and complex type inference.
Organized under tools/shadertoy/ for maintainability.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/shadertoy')
| -rw-r--r-- | tools/shadertoy/README.md | 191 | ||||
| -rwxr-xr-x | tools/shadertoy/convert_shadertoy.py | 408 | ||||
| -rw-r--r-- | tools/shadertoy/example.txt | 25 | ||||
| -rw-r--r-- | tools/shadertoy/template.cc | 120 | ||||
| -rw-r--r-- | tools/shadertoy/template.h | 41 | ||||
| -rw-r--r-- | tools/shadertoy/template.wgsl | 90 |
6 files changed, 875 insertions, 0 deletions
diff --git a/tools/shadertoy/README.md b/tools/shadertoy/README.md new file mode 100644 index 0000000..e734684 --- /dev/null +++ b/tools/shadertoy/README.md @@ -0,0 +1,191 @@ +# ShaderToy Conversion Guide + +Quick guide to convert ShaderToy shaders to demo effects. + +## Quick Start (Automated) + +```bash +# Save ShaderToy code to a file +cat > tunnel.txt << 'EOF' +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4)); + fragColor = vec4(col, 1.0); +} +EOF + +# Generate effect files +./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel + +# Follow printed instructions to integrate +``` + +## Files + +**Automated Script:** +- `convert_shadertoy.py` - Generates all files from ShaderToy code +- `example.txt` - Example ShaderToy shader for testing + +**Manual Templates:** +- `template.h` - Header boilerplate +- `template.cc` - Implementation boilerplate +- `template.wgsl` - Shader boilerplate with conversion notes + +## Manual Steps + +### 1. Copy Templates + +```bash +# Choose effect name (e.g., "tunnel", "plasma", "warp") +EFFECT_NAME="myeffect" + +cp tools/shadertoy/template.h src/gpu/effects/${EFFECT_NAME}_effect.h +cp tools/shadertoy/template.cc src/gpu/effects/${EFFECT_NAME}_effect.cc +cp tools/shadertoy/template.wgsl workspaces/main/shaders/${EFFECT_NAME}.wgsl +``` + +### 2. Rename Class + +In both `.h` and `.cc`: +- `ShaderToyEffect` → `MyEffectEffect` +- `SHADERTOY_EFFECT_H_` → `MYEFFECT_EFFECT_H_` +- `shadertoy_effect.h` → `myeffect_effect.h` + +### 3. Convert Shader + +In `.wgsl`, paste ShaderToy `mainImage()` into `fs_main()`: + +**ShaderToy:** +```glsl +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + fragColor = vec4(uv, 0.5, 1.0); +} +``` + +**WGSL:** +```wgsl +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { + let uv = p.xy / uniforms.resolution; + return vec4<f32>(uv, 0.5, 1.0); +} +``` + +### 4. Update Asset Name + +In `.cc`, update `AssetId::ASSET_SHADERTOY_SHADER` to match your shader filename: +```cpp +AssetId::ASSET_MYEFFECT_SHADER +``` + +### 5. Add to Assets + +In `workspaces/main/assets.txt`: +``` +shaders/myeffect.wgsl +``` + +### 6. Register Effect + +In `src/gpu/demo_effects.h`: +```cpp +#include "gpu/effects/myeffect_effect.h" +``` + +In `workspaces/main/timeline.seq`: +``` +SEQUENCE 0.0 0 + EFFECT MyEffectEffect 0.0 10.0 0 +``` + +### 7. Update Tests + +In `tests/test_demo_effects.cc`: +- Add `"MyEffectEffect"` to test list +- Increment `EXPECTED_*_COUNT` + +### 8. Build & Test + +```bash +cmake --build build -j4 +./build/demo64k + +# Run tests +cmake -S . -B build -DDEMO_BUILD_TESTS=ON +cmake --build build -j4 +cd build && ctest +``` + +## Example Conversion + +**Input ShaderToy:** +```glsl +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4)); + fragColor = vec4(col, 1.0); +} +``` + +**Generated WGSL (after script + manual fixes):** +```wgsl +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { + let uv = p.xy / uniforms.resolution; + let col = vec3<f32>(0.5) + 0.5 * cos(uniforms.time + uv.xyx + vec3<f32>(0.0, 2.0, 4.0)); + return vec4<f32>(col, 1.0); +} +``` + +## Common Conversions + +| ShaderToy | WGSL | +|-----------|------| +| `iResolution.xy` | `uniforms.resolution` | +| `iTime` | `uniforms.time` | +| `fragCoord` | `p.xy` | +| `float` | `f32` | +| `vec2` | `vec2<f32>` | +| `mod(x, y)` | `x % y` | +| `texture(iChannel0, uv)` | `textureSample(txt, smplr, uv)` | +| `fragColor = ...` | `return ...` | +| `vec2 p = ...` | `let p = vec2<f32>(...)` or `var p: vec2<f32> = ...` | + +## Custom Parameters + +For tunable values: + +**C++ (`.h`):** +```cpp +struct MyEffectParams { + float speed; + float scale; + float _pad[2]; +}; +static_assert(sizeof(MyEffectParams) == 16, "..."); +``` + +**WGSL:** +```wgsl +struct MyEffectParams { + speed: f32, + scale: f32, + _pad0: f32, + _pad1: f32, +} +@group(0) @binding(3) var<uniform> params: MyEffectParams; +``` + +## Available Uniforms + +Always available in `uniforms: CommonUniforms`: +- `resolution: vec2<f32>` - Screen resolution +- `aspect_ratio: f32` - Width/height +- `time: f32` - Demo time (seconds) +- `beat: f32` - Music beat sync (0-1) +- `audio_intensity: f32` - Audio reactivity + +## Next Steps + +- See `doc/CONTRIBUTING.md` for commit policy +- See `doc/SEQUENCE.md` for timeline syntax +- See existing effects in `src/gpu/effects/` for examples diff --git a/tools/shadertoy/convert_shadertoy.py b/tools/shadertoy/convert_shadertoy.py new file mode 100755 index 0000000..4956cfd --- /dev/null +++ b/tools/shadertoy/convert_shadertoy.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +# This file is part of the 64k demo project. +# Converts ShaderToy shader to demo effect boilerplate. +# +# Usage: +# ./tools/shadertoy/convert_shadertoy.py <shader.txt> <EffectName> +# +# Example: +# ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel +# ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow +# +# Generates: +# - src/gpu/effects/<effect>_effect.h +# - src/gpu/effects/<effect>_effect.cc +# - workspaces/main/shaders/<effect>.wgsl +# +# The script performs basic ShaderToy→WGSL conversion: +# - Converts types (float→f32, vec2→vec2<f32>, etc.) +# - Converts uniforms (iTime→uniforms.time, etc.) +# - Extracts mainImage() body into fs_main() +# - Generates boilerplate C++ effect class +# +# Manual fixes usually needed: +# - fragColor assignments → return statements +# - Variable name conflicts (e.g., shadowing 'p') +# - Complex type inference +# - Texture channel mappings +# - Helper function signatures + +import sys +import os +import re +from pathlib import Path + +def to_snake_case(name): + """Convert CamelCase to snake_case.""" + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +def to_upper_snake_case(name): + """Convert CamelCase to UPPER_SNAKE_CASE.""" + return to_snake_case(name).upper() + +def to_camel_case(name): + """Convert snake_case to CamelCase.""" + return ''.join(word.capitalize() for word in name.split('_')) + +def convert_shadertoy_to_wgsl(shader_code): + """Basic ShaderToy to WGSL conversion.""" + # Replace common ShaderToy uniforms + conversions = [ + (r'\biResolution\b', 'uniforms.resolution'), + (r'\biTime\b', 'uniforms.time'), + (r'\bfragCoord\b', 'p.xy'), + + # Type conversions + (r'\bfloat\b', 'f32'), + (r'\bvec2\b', 'vec2<f32>'), + (r'\bvec3\b', 'vec3<f32>'), + (r'\bvec4\b', 'vec4<f32>'), + (r'\bmat2\b', 'mat2x2<f32>'), + (r'\bmat3\b', 'mat3x3<f32>'), + (r'\bmat4\b', 'mat4x4<f32>'), + + # Function declarations + (r'\bfloat\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec2\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec3\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec4\s+(\w+)\s*\(', r'fn \1('), + + # Function parameters (simple cases) + (r'\(\s*vec2\s+(\w+)\s*\)', r'(\1: vec2<f32>)'), + (r'\(\s*vec3\s+(\w+)\s*\)', r'(\1: vec3<f32>)'), + (r'\(\s*float\s+(\w+)\s*\)', r'(\1: f32)'), + + # Return types + (r'\)\s*{', r') -> f32 {'), # Assume f32 return, may need manual fix + + # Texture sampling + (r'\btexture\s*\(\s*iChannel0\s*,', 'textureSample(txt, smplr,'), + (r'\btexture\s*\(\s*iChannel1\s*,', 'textureSample(txt, smplr,'), + ] + + converted = shader_code + for pattern, replacement in conversions: + converted = re.sub(pattern, replacement, converted) + + # Convert variable declarations + converted = re.sub(r'\bfloat\s+(\w+)\s*=', r'var \1: f32 =', converted) + converted = re.sub(r'\bvec2\s+(\w+)\s*=', r'var \1: vec2<f32> =', converted) + converted = re.sub(r'\bvec3\s+(\w+)\s*=', r'var \1: vec3<f32> =', converted) + converted = re.sub(r'\bvec4\s+(\w+)\s*=', r'var \1: vec4<f32> =', converted) + + return converted + +def extract_main_image(shader_code): + """Extract mainImage function body from ShaderToy code.""" + # Try to find mainImage function + match = re.search(r'void\s+mainImage\s*\([^)]+\)\s*\{(.*)\}', shader_code, re.DOTALL) + if match: + return match.group(1).strip() + + # If no mainImage found, return whole shader + return shader_code + +def generate_header(effect_name): + """Generate .h file content.""" + class_name = f"{effect_name}Effect" + snake_name = to_snake_case(effect_name) + upper_name = to_upper_snake_case(effect_name) + + return f"""// This file is part of the 64k demo project. +// {effect_name} effect - ShaderToy conversion +// Generated by convert_shadertoy.py + +#ifndef {upper_name}_EFFECT_H_ +#define {upper_name}_EFFECT_H_ + +#include "gpu/effect.h" +#include "gpu/effects/post_process_helper.h" +#include "gpu/uniform_helper.h" + +class {class_name} : public Effect {{ + public: + {class_name}(const GpuContext& ctx); + ~{class_name}() override; + + void init(MainSequence* demo) override; + void render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) override; + + private: + // Effect-specific parameters - adjust as needed + struct {effect_name}Params {{ + float param1; + float param2; + float _pad[2]; + }}; + static_assert(sizeof({effect_name}Params) == 16, + "{effect_name}Params must be 16 bytes for WGSL alignment"); + + MainSequence* demo_ = nullptr; + WGPURenderPipeline pipeline_ = nullptr; + WGPUBindGroup bind_group_ = nullptr; + WGPUSampler sampler_ = nullptr; + UniformBuffer<{effect_name}Params> params_; +}}; + +#endif /* {upper_name}_EFFECT_H_ */ +""" + +def generate_implementation(effect_name): + """Generate .cc file content.""" + class_name = f"{effect_name}Effect" + snake_name = to_snake_case(effect_name) + upper_name = to_upper_snake_case(effect_name) + + return f"""// This file is part of the 64k demo project. +// {effect_name} effect implementation - ShaderToy conversion +// Generated by convert_shadertoy.py + +#include "gpu/effects/{snake_name}_effect.h" +#include "gpu/effects/shader_composer.h" +#include "generated/assets.h" + +{class_name}::{class_name}(const GpuContext& ctx) : Effect(ctx) {{ +}} + +{class_name}::~{class_name}() {{ + if (sampler_) + wgpuSamplerRelease(sampler_); + if (bind_group_) + wgpuBindGroupRelease(bind_group_); + if (pipeline_) + wgpuRenderPipelineRelease(pipeline_); +}} + +void {class_name}::init(MainSequence* demo) {{ + demo_ = demo; + params_.init(ctx_.device); + + WGPUSamplerDescriptor sampler_desc = {{}}; + sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Linear; + sampler_desc.maxAnisotropy = 1; + sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc); + + size_t shader_size; + const char* shader_code = (const char*)GetAsset( + AssetId::ASSET_{upper_name}_SHADER, &shader_size); + + std::string composed = ShaderComposer::Get().Compose({{}}, shader_code); + + WGPUShaderSourceWGSL wgsl = {{}}; + wgsl.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl.code = str_view(composed.c_str()); + + WGPUShaderModuleDescriptor desc = {{}}; + desc.nextInChain = &wgsl.chain; + WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx_.device, &desc); + + const WGPUColorTargetState target = {{ + .format = ctx_.format, + .writeMask = WGPUColorWriteMask_All, + }}; + WGPUFragmentState frag = {{}}; + frag.module = module; + frag.entryPoint = str_view("fs_main"); + frag.targetCount = 1; + frag.targets = ⌖ + + const WGPUDepthStencilState depth_stencil = {{ + .format = WGPUTextureFormat_Depth24Plus, + .depthWriteEnabled = WGPUOptionalBool_False, + .depthCompare = WGPUCompareFunction_Always, + }}; + + WGPURenderPipelineDescriptor pipeline_desc = {{}}; + pipeline_desc.label = label_view("{class_name}"); + pipeline_desc.vertex.module = module; + pipeline_desc.vertex.entryPoint = str_view("vs_main"); + pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pipeline_desc.primitive.cullMode = WGPUCullMode_None; + pipeline_desc.depthStencil = &depth_stencil; + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = 0xFFFFFFFF; + pipeline_desc.fragment = &frag; + + pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc); + wgpuShaderModuleRelease(module); + + WGPUTextureView prev_view = demo_->get_prev_texture_view(); + const WGPUBindGroupEntry entries[] = {{ + {{.binding = 0, .sampler = sampler_}}, + {{.binding = 1, .textureView = prev_view}}, + {{.binding = 2, + .buffer = uniforms_.get().buffer, + .size = sizeof(CommonPostProcessUniforms)}}, + {{.binding = 3, + .buffer = params_.get().buffer, + .size = sizeof({effect_name}Params)}}, + }}; + const WGPUBindGroupDescriptor bg_desc = {{ + .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0), + .entryCount = 4, + .entries = entries, + }}; + bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc); +}} + +void {class_name}::render(WGPURenderPassEncoder pass, float time, + float beat, float intensity, float aspect_ratio) {{ + const CommonPostProcessUniforms uniforms = {{ + .resolution = {{static_cast<float>(width_), static_cast<float>(height_)}}, + .aspect_ratio = aspect_ratio, + .time = time, + .beat = beat, + .audio_intensity = intensity, + }}; + uniforms_.update(ctx_.queue, uniforms); + + const {effect_name}Params params = {{ + .param1 = 1.0f, + .param2 = beat, + }}; + params_.update(ctx_.queue, params); + + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); +}} +""" + +def generate_shader(effect_name, shadertoy_code): + """Generate .wgsl file content.""" + # Extract mainImage body + main_body = extract_main_image(shadertoy_code) + + # Convert to WGSL + converted = convert_shadertoy_to_wgsl(main_body) + + # Indent the converted code + indented = '\n'.join(' ' + line if line.strip() else '' + for line in converted.split('\n')) + + return f"""// {effect_name} effect shader - ShaderToy conversion +// Generated by convert_shadertoy.py +// TODO: Review and fix conversion issues + +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d<f32>; + +#include "common_uniforms" + +@group(0) @binding(2) var<uniform> uniforms: CommonUniforms; + +struct {effect_name}Params {{ + param1: f32, + param2: f32, + _pad0: f32, + _pad1: f32, +}} + +@group(0) @binding(3) var<uniform> params: {effect_name}Params; + +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {{ + var pos = array<vec2<f32>, 3>( + vec2<f32>(-1.0, -1.0), + vec2<f32>(3.0, -1.0), + vec2<f32>(-1.0, 3.0) + ); + return vec4<f32>(pos[i], 0.0, 1.0); +}} + +// ============================================================================ +// CONVERTED SHADERTOY CODE (may need manual fixes) +// ============================================================================ +// Original helper functions (if any) appear above mainImage conversion + +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {{ + // Converted from mainImage() +{indented} + + // TODO: Fix the return statement if needed + // ShaderToy used: fragColor = ... + // WGSL needs: return vec4<f32>(...); +}} +""" + +def main(): + if len(sys.argv) < 3: + print("Usage: convert_shadertoy.py <shader.txt> <EffectName>") + print() + print("Examples:") + print(" ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel") + print(" ./tools/shadertoy/convert_shadertoy.py plasma.txt Plasma") + print(" ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow") + print() + print("This will generate:") + print(" src/gpu/effects/<effect>_effect.h") + print(" src/gpu/effects/<effect>_effect.cc") + print(" workspaces/main/shaders/<effect>.wgsl") + sys.exit(1) + + shader_file = sys.argv[1] + effect_name = sys.argv[2] + + # Ensure effect name is CamelCase + if '_' in effect_name: + effect_name = to_camel_case(effect_name) + + # Read shader code + if not os.path.exists(shader_file): + print(f"Error: {shader_file} not found") + sys.exit(1) + + with open(shader_file, 'r') as f: + shadertoy_code = f.read() + + # Generate file names + snake_name = to_snake_case(effect_name) + + # Script is in tools/shadertoy/, so go up two levels to repo root + repo_root = Path(__file__).parent.parent.parent + header_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.h" + impl_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.cc" + shader_path = repo_root / "workspaces" / "main" / "shaders" / f"{snake_name}.wgsl" + + # Generate files + print(f"Generating effect: {effect_name}") + print(f" Header: {header_path}") + print(f" Impl: {impl_path}") + print(f" Shader: {shader_path}") + print() + + # Write files + header_path.write_text(generate_header(effect_name)) + impl_path.write_text(generate_implementation(effect_name)) + shader_path.write_text(generate_shader(effect_name, shadertoy_code)) + + print("✓ Files generated") + print() + print("Next steps:") + print() + print("1. Add shader to workspaces/main/assets.txt:") + print(f" shaders/{snake_name}.wgsl") + print() + print("2. Include header in src/gpu/demo_effects.h:") + print(f' #include "gpu/effects/{snake_name}_effect.h"') + print() + print("3. Add to timeline in workspaces/main/timeline.seq:") + print(f" EFFECT {effect_name}Effect 0.0 10.0 0") + print() + print("4. Update tests/test_demo_effects.cc:") + print(f' - Add "{effect_name}Effect" to test list') + print(" - Increment EXPECTED_*_COUNT") + print() + print("5. Build and test:") + print(" cmake --build build -j4") + print(" ./build/demo64k") + print() + print("Note: WGSL conversion is basic - review and fix shader manually!") + +if __name__ == '__main__': + main() diff --git a/tools/shadertoy/example.txt b/tools/shadertoy/example.txt new file mode 100644 index 0000000..e0287de --- /dev/null +++ b/tools/shadertoy/example.txt @@ -0,0 +1,25 @@ +// Example ShaderToy shader for testing convert_shadertoy.py +// Simple animated gradient effect +// +// Test with: +// ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + // Normalized pixel coordinates (from 0 to 1) + vec2 uv = fragCoord / iResolution.xy; + + // Center coordinates + vec2 center = uv - 0.5; + + // Distance from center + float dist = length(center); + + // Animated rainbow colors + vec3 col = 0.5 + 0.5 * cos(iTime + dist * 10.0 + vec3(0.0, 2.0, 4.0)); + + // Pulsing effect + col *= 1.0 + 0.2 * sin(iTime * 2.0); + + // Output to screen + fragColor = vec4(col, 1.0); +} diff --git a/tools/shadertoy/template.cc b/tools/shadertoy/template.cc new file mode 100644 index 0000000..288283d --- /dev/null +++ b/tools/shadertoy/template.cc @@ -0,0 +1,120 @@ +// This file is part of the 64k demo project. +// ShaderToy effect implementation - REPLACE THIS LINE +// TODO: Update description, rename class + +#include "gpu/effects/shadertoy_effect.h" +#include "gpu/effects/shader_composer.h" +#include "generated/assets.h" + +// TODO: Rename class and adjust constructor parameters +ShaderToyEffect::ShaderToyEffect(const GpuContext& ctx) : Effect(ctx) { +} + +ShaderToyEffect::~ShaderToyEffect() { + if (sampler_) + wgpuSamplerRelease(sampler_); + if (bind_group_) + wgpuBindGroupRelease(bind_group_); + if (pipeline_) + wgpuRenderPipelineRelease(pipeline_); +} + +void ShaderToyEffect::init(MainSequence* demo) { + demo_ = demo; + params_.init(ctx_.device); + + WGPUSamplerDescriptor sampler_desc = {}; + sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge; + sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge; + sampler_desc.magFilter = WGPUFilterMode_Linear; + sampler_desc.minFilter = WGPUFilterMode_Linear; + sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Linear; + sampler_desc.maxAnisotropy = 1; + sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc); + + // TODO: Update asset name to match your shader file + size_t shader_size; + const char* shader_code = (const char*)GetAsset( + AssetId::ASSET_SHADERTOY_SHADER, &shader_size); + + std::string composed = ShaderComposer::Get().Compose({}, shader_code); + + WGPUShaderSourceWGSL wgsl = {}; + wgsl.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl.code = str_view(composed.c_str()); + + WGPUShaderModuleDescriptor desc = {}; + desc.nextInChain = &wgsl.chain; + WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx_.device, &desc); + + const WGPUColorTargetState target = { + .format = ctx_.format, + .writeMask = WGPUColorWriteMask_All, + }; + WGPUFragmentState frag = {}; + frag.module = module; + frag.entryPoint = str_view("fs_main"); + frag.targetCount = 1; + frag.targets = ⌖ + + const WGPUDepthStencilState depth_stencil = { + .format = WGPUTextureFormat_Depth24Plus, + .depthWriteEnabled = WGPUOptionalBool_False, + .depthCompare = WGPUCompareFunction_Always, + }; + + WGPURenderPipelineDescriptor pipeline_desc = {}; + pipeline_desc.label = label_view("ShaderToyEffect"); + pipeline_desc.vertex.module = module; + pipeline_desc.vertex.entryPoint = str_view("vs_main"); + pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pipeline_desc.primitive.cullMode = WGPUCullMode_None; + pipeline_desc.depthStencil = &depth_stencil; + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = 0xFFFFFFFF; + pipeline_desc.fragment = &frag; + + pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc); + wgpuShaderModuleRelease(module); + + WGPUTextureView prev_view = demo_->get_prev_texture_view(); + const WGPUBindGroupEntry entries[] = { + {.binding = 0, .sampler = sampler_}, + {.binding = 1, .textureView = prev_view}, + {.binding = 2, + .buffer = uniforms_.get().buffer, + .size = sizeof(CommonPostProcessUniforms)}, + {.binding = 3, + .buffer = params_.get().buffer, + .size = sizeof(ShaderToyParams)}, + }; + const WGPUBindGroupDescriptor bg_desc = { + .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0), + .entryCount = 4, + .entries = entries, + }; + bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc); +} + +void ShaderToyEffect::render(WGPURenderPassEncoder pass, float time, + float beat, float intensity, float aspect_ratio) { + const CommonPostProcessUniforms uniforms = { + .resolution = {static_cast<float>(width_), static_cast<float>(height_)}, + .aspect_ratio = aspect_ratio, + .time = time, + .beat = beat, + .audio_intensity = intensity, + }; + uniforms_.update(ctx_.queue, uniforms); + + // TODO: Update parameters based on your effect + const ShaderToyParams params = { + .param1 = 1.0f, + .param2 = beat, + }; + params_.update(ctx_.queue, params); + + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); +} diff --git a/tools/shadertoy/template.h b/tools/shadertoy/template.h new file mode 100644 index 0000000..2e4af5f --- /dev/null +++ b/tools/shadertoy/template.h @@ -0,0 +1,41 @@ +// This file is part of the 64k demo project. +// ShaderToy effect boilerplate - REPLACE THIS LINE WITH DESCRIPTION +// TODO: Update description, rename class, adjust parameters + +#ifndef SHADERTOY_EFFECT_H_ +#define SHADERTOY_EFFECT_H_ + +#include "gpu/effect.h" +#include "gpu/effects/post_process_helper.h" +#include "gpu/uniform_helper.h" + +// TODO: Rename class to match your effect (e.g., TunnelEffect, PlasmaEffect) +class ShaderToyEffect : public Effect { + public: + // TODO: Add constructor parameters for tunable values + ShaderToyEffect(const GpuContext& ctx); + ~ShaderToyEffect() override; + + void init(MainSequence* demo) override; + void render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) override; + + private: + // TODO: Add effect-specific parameters here + // Must match WGSL struct exactly - use padding for 16-byte alignment + struct ShaderToyParams { + float param1; + float param2; + float _pad[2]; // Padding to 16 bytes + }; + static_assert(sizeof(ShaderToyParams) == 16, + "ShaderToyParams must be 16 bytes for WGSL alignment"); + + MainSequence* demo_ = nullptr; + WGPURenderPipeline pipeline_ = nullptr; + WGPUBindGroup bind_group_ = nullptr; + WGPUSampler sampler_ = nullptr; + UniformBuffer<ShaderToyParams> params_; +}; + +#endif /* SHADERTOY_EFFECT_H_ */ diff --git a/tools/shadertoy/template.wgsl b/tools/shadertoy/template.wgsl new file mode 100644 index 0000000..37e7def --- /dev/null +++ b/tools/shadertoy/template.wgsl @@ -0,0 +1,90 @@ +// ShaderToy conversion template for 64k demo project +// TODO: Paste ShaderToy mainImage() function below and adapt + +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d<f32>; + +#include "common_uniforms" + +@group(0) @binding(2) var<uniform> uniforms: CommonUniforms; + +// TODO: Define your effect parameters (must match C++ struct) +struct ShaderToyParams { + param1: f32, + param2: f32, + _pad0: f32, + _pad1: f32, +} + +@group(0) @binding(3) var<uniform> params: ShaderToyParams; + +// Standard fullscreen triangle vertex shader +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> { + var pos = array<vec2<f32>, 3>( + vec2<f32>(-1.0, -1.0), + vec2<f32>(3.0, -1.0), + vec2<f32>(-1.0, 3.0) + ); + return vec4<f32>(pos[i], 0.0, 1.0); +} + +// ============================================================================ +// PASTE SHADERTOY CODE HERE +// ============================================================================ +// ShaderToy → WGSL conversion notes: +// +// 1. Replace ShaderToy uniforms: +// iResolution.xy → uniforms.resolution +// iTime → uniforms.time +// fragCoord → p.xy (from @builtin(position)) +// fragColor → return value +// +// 2. Coordinate conversion: +// vec2 uv = fragCoord / iResolution.xy; +// becomes: +// let uv = p.xy / uniforms.resolution; +// +// 3. Type syntax changes: +// float → f32 +// vec2/vec3/vec4 → vec2<f32>, vec3<f32>, vec4<f32> +// mat2/mat3/mat4 → mat2x2<f32>, mat3x3<f32>, mat4x4<f32> +// +// 4. Function syntax: +// float foo(vec2 p) → fn foo(p: vec2<f32>) -> f32 +// +// 5. Common functions (mostly same): +// mix, sin, cos, length, normalize, dot, cross, etc. +// fract() → fract() +// mod(x, y) → x % y OR x - y * floor(x / y) +// +// 6. Texture sampling: +// texture(iChannel0, uv) → textureSample(txt, smplr, uv) +// +// 7. Variable declarations: +// float x = 1.0; → var x: f32 = 1.0; OR let x = 1.0; +// const float x = 1.0; → const x: f32 = 1.0; +// +// 8. Swizzling is the same: col.rgb, uv.xy, etc. +// +// ============================================================================ + +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { + // TODO: Paste and adapt ShaderToy mainImage() body here + + // Example coordinate setup (typical ShaderToy pattern): + let uv = p.xy / uniforms.resolution; + + // TODO: Your effect code here + var col = vec3<f32>(uv.x, uv.y, 0.5); + + // Optional: Sample previous frame + // var prev_col = textureSample(txt, smplr, uv); + + // Optional: Audio reactivity + // col *= 1.0 + uniforms.audio_intensity * 0.2; + + // Optional: Beat sync + // col *= 1.0 + uniforms.beat * 0.1; + + return vec4<f32>(col, 1.0); +} |
