3 Commits

Author SHA1 Message Date
b290cee689 corregit shader water. afegides metadades en els shaders 2025-11-16 16:04:29 +01:00
194726f823 Add self-feedback system and water shader
Features:
- Self-feedback rendering system for shaders with feedback loops
- Automatic FBO/texture management for feedback
- Metadata parser detects iChannel feedback configuration
- Adaptive render loop (with/without feedback)
- Water shader from Shadertoy (adapted and working)
- Fixed variable initialization issues in shader code

Technical details:
- FBO creation/destruction on shader switch
- Texture binding to iChannel0-3 based on metadata
- Auto-resize feedback buffers on window resize
- Cleanup on exit and shader switch

Files:
- src/main.cpp: Feedback system implementation
- shaders/water.glsl: Water shader with fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 15:45:18 +01:00
44de2c7013 Add FPS counter, VSync toggle, shader metadata system, and multi-pass infrastructure
- FPS counter in window title (updates every 500ms)
- F4 key toggles VSync on/off
- Shader metadata: Name and Author from comments
- iChannel metadata parsing for multi-pass support
- Base structures: ShaderBuffer, ShaderPass
- FBO/texture management functions
- Updated all 11 shaders with Name/Author metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 15:22:06 +01:00
35 changed files with 7865 additions and 17 deletions

236
CLAUDE.md Normal file
View File

@@ -0,0 +1,236 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a cross-platform Shadertoy-like fragment shader viewer built with SDL3 + OpenGL 3.3. The application loads and displays GLSL shaders from the `shaders/` directory with runtime switching capabilities.
## Build and Development Commands
### Building (CMake - Development)
```bash
mkdir build && cd build
cmake ..
cmake --build . --config Release
```
### Building (Makefile - Release Packages)
```bash
make windows_release # Creates .zip with DLLs
make macos_release # Creates .dmg with app bundle
make linux_release # Creates .tar.gz
make show_version # Display build version (YYYY-MM-DD format)
```
Platform-specific debug builds:
```bash
make windows_debug
make macos_debug
make linux_debug
```
### Running the Application
```bash
./shadertoy [SHADER_PATH] [-F|--fullscreen]
# Examples:
./shadertoy shaders/test.frag.glsl
./shadertoy -F shaders/fractal_pyramid.frag.glsl
```
**Runtime Controls:**
- `ESC` - Exit
- `F3` - Toggle fullscreen
- `LEFT/RIGHT ARROW` - Cycle through shaders in directory
## Architecture
### Core Design
All application logic resides in `src/main.cpp` (~469 lines) - a monolithic design that's straightforward to understand. Key components:
1. **Shader Loading System** - Automatic directory scanning of `.glsl` files, sorted alphabetically
2. **OpenGL Rendering** - Single fullscreen quad with fragment shader using GL_TRIANGLE_STRIP
3. **Event Loop** - SDL3-based with vsync (SDL_GL_SwapWindow + 1ms delay)
4. **Resource Path Resolution** - Multi-path fallback system for executable, relative, and macOS bundle paths
### Global State (main.cpp)
```cpp
shader_list_ // Vector of discovered .glsl shader paths
current_shader_index_ // Active shader in rotation
current_program_ // OpenGL shader program handle
shader_start_ticks_ // Base time for iTime uniform calculation
window_ // SDL3 window pointer
shaders_directory_ // Shader directory path (resolved at startup)
```
### Dependencies
- **SDL3** - Window/input management, OpenGL context
- **GLAD** - OpenGL 3.3 loader (statically linked via `third_party/glad/`)
- **C++17 stdlib** - filesystem, fstream, vector, algorithms
### Platform-Specific Code
Uses preprocessor defines (`WINDOWS_BUILD`, `MACOS_BUILD`, `LINUX_BUILD`) for:
- `getExecutableDirectory()` - Windows API, mach-o dyld, or /proc/self/exe
- `getResourcesDirectory()` - Special macOS app bundle handling (Contents/Resources)
- Linking flags - Windows uses static linking, macOS links OpenGL framework
## Shader System
### Shader Format (GLSL 3.3 Core)
All shaders must follow this structure:
```glsl
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV; // Normalized [0,1] coordinates from vertex shader
uniform vec2 iResolution; // Window resolution in pixels
uniform float iTime; // Time since shader loaded (seconds)
// Shadertoy-style entry point
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// fragCoord is in pixel coordinates
// Your shader code here
}
// Wrapper converts vUV to pixel coordinates
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}
```
### Adding New Shaders
1. Create `.glsl` file in `shaders/` directory (use `.frag.glsl` convention)
2. Follow required format above
3. File automatically appears in runtime shader rotation (arrow keys to navigate)
4. **No code changes required** - directory is scanned at startup
### Shader Loading Pipeline
1. Directory scan on startup (`scanShaderDirectory()`)
2. Sort alphabetically
3. Load fragment shader source from disk
4. Compile vertex shader (fixed, embedded in main.cpp)
5. Compile fragment shader with error logging
6. Link program with error handling
7. Update uniforms each frame (iResolution, iTime)
### Supported Shadertoy Features
-`iTime` - Time uniform
-`iResolution` - Window resolution (vec2, not vec3)
-`mainImage()` function signature
-`iMouse` - Not implemented
-`iChannel0-3` - No texture channels (multi-pass not supported)
-`iFrame`, `iTimeDelta`, `iDate` - Not implemented
### Converting from Shadertoy
When porting Shadertoy shaders:
1. Copy the `mainImage()` function as-is
2. Add standard header (see format above)
3. Add wrapper `main()` function
4. Remove texture channel references (iChannel0-3)
5. Change `iResolution.xy` to `iResolution` (this project uses vec2)
### Common Conversion Issues and Solutions
Based on experience converting complex shaders like ddla_light_tunnel:
#### iResolution vec3 vs vec2
- **Shadertoy:** `iResolution` is `vec3(width, height, width/height)`
- **This project:** `iResolution` is `vec2(width, height)`
- **Solution:** Create vec3 manually: `vec3 r = vec3(iResolution.xy, iResolution.x/iResolution.y);`
- **Then use:** `r.xy` for resolution, `r.y` for height (as in original)
#### Uninitialized Variables
- **Problem:** Shadertoy code may have `vec3 rgb;` without initialization
- **Shadertoy behavior:** Likely initializes to `vec3(0.0)` (black)
- **This project:** Uninitialized variables contain undefined values (often causes black screen or wrong colors)
- **Solution:** Always initialize: `vec3 rgb = vec3(0.0);`
- **Wrong approach:** Don't use random noise unless shader explicitly uses iChannel texture for noise
#### Low mix() Factors Are Intentional
- **Example:** `rgb = mix(rgb, calculated_color, 0.01);` means 1% new color, 99% existing
- **Don't change these factors** - they create subtle effects intentionally
- **If output is black:** Problem is likely the base value (rgb), not the mix factor
#### iChannel Textures
- **Shadertoy shows iChannel0-3** in UI, but shader may not use them
- **If iChannel3 has RGB noise but code doesn't reference it:** The noise is not actually used
- **Check code for `texture(iChannelN, ...)` calls** - if none exist, ignore the iChannel setup
- **Procedural noise replacement:** Only if shader explicitly samples the texture
#### Color Swapping Issues
- **If colors are completely wrong:** Don't randomly swap color variables
- **First verify:** Code matches original Shadertoy exactly (except required GLSL changes)
- **Common mistake:** Changing color applications that were correct in original
- **Debug approach:** Revert to exact original code, only modify for GLSL 3.3 compatibility
#### Division by Small Values - Color Overflow Artifacts
- **Problem:** Division by values near zero causes extreme values → color overflow artifacts (green points, strange colors)
- **Example:** `.01*vec4(6,2,1,0)/length(u*sin(iTime))` can divide by ~0.0 when near center and sin≈0
- **Symptoms:** Bright white center that pulses to green, or random color artifacts in specific areas
- **Root cause:** Even with tonemap (tanh/clamp), extreme intermediate values cause precision issues or saturation
- **Solution - Double Protection:**
1. **Add epsilon to denominator:** `max(denominator, 0.001)` prevents division by exact zero
2. **Clamp the result:** `min(result, vec4(50.0))` prevents extreme accumulation
3. **Only clamp problematic terms:** Don't clamp everything or scene becomes dim
- **Example fix:**
```glsl
// Original (causes overflow):
o += .01*vec4(6,2,1,0)/length(u*sin(t+t+t)) + 1./s * length(u);
// Fixed (prevents overflow while maintaining brightness):
vec4 brightTerm = min(.01*vec4(6,2,1,0)/max(length(u*sin(t+t+t)), 0.001), vec4(50.0));
o += brightTerm + 1./s * length(u);
```
- **Key values:** epsilon ~0.001 (not too small, not too large), clamp ~50.0 (allows brightness without explosion)
- **Why this works in Shadertoy:** WebGL/browsers may handle float overflow differently than native OpenGL drivers
#### Debugging Black/Wrong Output
1. **Check compilation errors first** - shader must compile without errors
2. **Initialize all variables** - especially vec3 colors
3. **Verify vec3 iResolution handling** - create vec3 from vec2 if needed
4. **Don't modify mix factors** - keep original values
5. **Compare with original code** - ensure logic is identical
6. **Test progressive changes** - add header, test; add wrapper, test; etc.
7. **Check for division by small values** - if you see color artifacts (especially green), look for divisions that can approach zero
## Build System Details
### Version Numbering
Releases use build date format: `shadertoy-YYYY-MM-DD-{platform}` (auto-generated by Makefile)
### Release Artifacts
- **Windows:** `.zip` with `shadertoy.exe` + `SDL3.dll` + shaders
- **macOS:** `.dmg` with app bundle containing embedded SDL3.framework (arm64 only)
- **Linux:** `.tar.gz` with binary + shaders
### Resource Bundling
- Shaders copied to release packages
- LICENSE and README.md included
- Platform-specific dependencies bundled (DLLs on Windows, frameworks on macOS)
## Important Notes
### Vertex Shader
The vertex shader is hardcoded in `main.cpp` and creates a fullscreen quad. It:
- Takes `vec2 aPos` (location 0) in NDC space [-1, 1]
- Outputs `vec2 vUV` normalized to [0, 1]
- No transformation matrices needed
### Shader Hot-Reloading
Currently **not implemented**. Shader changes require application restart. The architecture would support adding this via file watching.
### Multi-Pass Rendering
Single-pass only. To add multi-pass (BufferA/B/C like Shadertoy):
- Create FBOs and textures per buffer
- Render buffers in dependency order
- Pass textures as `iChannel0-3` uniforms
- Use ping-pong for feedback loops
### OpenGL Context
Created via SDL3 with core profile (no deprecated functions). Context version: 3.3 core.

View File

@@ -22,6 +22,7 @@ set(APP_SOURCES
# Fuentes de librerías de terceros # Fuentes de librerías de terceros
set(EXTERNAL_SOURCES set(EXTERNAL_SOURCES
third_party/glad/src/glad.c third_party/glad/src/glad.c
third_party/jail_audio.cpp
) )
# Configuración de SDL3 # Configuración de SDL3
@@ -35,6 +36,7 @@ add_executable(${PROJECT_NAME} ${APP_SOURCES} ${EXTERNAL_SOURCES})
target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} PUBLIC
"${CMAKE_SOURCE_DIR}/src" "${CMAKE_SOURCE_DIR}/src"
"${CMAKE_SOURCE_DIR}/third_party/glad/include" "${CMAKE_SOURCE_DIR}/third_party/glad/include"
"${CMAKE_SOURCE_DIR}/third_party"
) )
# Enlazar la librería SDL3 # Enlazar la librería SDL3

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,41 @@
Pack downloaded from Freesound
----------------------------------------
"Ambient pads"
This Pack of sounds contains sounds by the following user:
- Mat397 ( https://freesound.org/people/Mat397/ )
You can find this pack online at: https://freesound.org/people/Mat397/packs/27416/
Licenses in this Pack (see below for individual sound licenses)
---------------------------------------------------------------
Creative Commons 0: http://creativecommons.org/publicdomain/zero/1.0/
Attribution 3.0: http://creativecommons.org/licenses/by/3.0/
Sounds in this Pack
-------------------
* 618042__mat397__bad-electric-dark-pad.wav.wav
* url: https://freesound.org/s/618042/
* license: Creative Commons 0
* 618041__mat397__mangle-dark-pad.wav.wav
* url: https://freesound.org/s/618041/
* license: Creative Commons 0
* 486083__mat397__world-of-ants-pad.wav.wav
* url: https://freesound.org/s/486083/
* license: Attribution 3.0
* 485079__mat397__melancholic-flutes-pad.wav.wav
* url: https://freesound.org/s/485079/
* license: Attribution 3.0
* 485078__mat397__polyflute-pad.wav.wav
* url: https://freesound.org/s/485078/
* license: Attribution 3.0
* 485077__mat397__confused-voices.wav.wav
* url: https://freesound.org/s/485077/
* license: Attribution 3.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 364 KiB

BIN
release/icon/icon.afdesign Normal file

Binary file not shown.

BIN
release/icon/icon.pxd Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,83 @@
// Name: Cineshader Lava
// Author: edankwan
// URL: https://www.shadertoy.com/view/3sySRK
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
float opSmoothUnion( float d1, float d2, float k )
{
float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 );
return mix( d2, d1, h ) - k*h*(1.0-h);
}
float sdSphere( vec3 p, float s )
{
return length(p)-s;
}
float map(vec3 p)
{
float d = 2.0;
for (int i = 0; i < 16; i++) {
float fi = float(i);
float time = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
d = opSmoothUnion(
sdSphere(p + sin(time + fi * vec3(52.5126, 64.62744, 632.25)) * vec3(2.0, 2.0, 0.8), mix(0.5, 1.0, fract(fi * 412.531 + 0.5124))),
d,
0.4
);
}
return d;
}
vec3 calcNormal( in vec3 p )
{
const float h = 1e-5; // or some other value
const vec2 k = vec2(1,-1);
return normalize( k.xyy*map( p + k.xyy*h ) +
k.yyx*map( p + k.yyx*h ) +
k.yxy*map( p + k.yxy*h ) +
k.xxx*map( p + k.xxx*h ) );
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
// screen size is 6m x 6m
vec3 rayOri = vec3((uv - 0.5) * vec2(iResolution.x/iResolution.y, 1.0) * 6.0, 3.0);
vec3 rayDir = vec3(0.0, 0.0, -1.0);
float depth = 0.0;
vec3 p;
for(int i = 0; i < 64; i++) {
p = rayOri + rayDir * depth;
float dist = map(p);
depth += dist;
if (dist < 1e-6) {
break;
}
}
depth = min(6.0, depth);
vec3 n = calcNormal(p);
float b = max(0.0, dot(n, vec3(0.577)));
vec3 col = (0.5 + 0.5 * cos((b + iTime * 3.0) + uv.xyx * 2.0 + vec3(0,2,4))) * (0.85 + b * 0.35);
col *= exp( -depth * 0.15 );
// maximum thickness is 2m in alpha channel
fragColor = vec4(col, 1.0 - (depth - 0.5) / 2.0);
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

View File

@@ -1,3 +1,6 @@
// Name: Creation by Silexars
// Author: Danguafer
// URL: https://www.shadertoy.com/view/XsXXDn
#version 330 core #version 330 core
precision highp float; precision highp float;

190
shaders/dbz.frag.glsl Normal file
View File

@@ -0,0 +1,190 @@
// Name: New Leaked 3I/Atlas NASA Footage
// Author: msm01
// URL: https://www.shadertoy.com/view/3ftcRr
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
// A small improv/fanart from yesterday.
#define s(a,b,c) smoothstep(a,b,c)
#define PI 3.14159
#define NBCaps 3.
mat2 r2d( float a ){ float c = cos(a), s = sin(a); return mat2( c, s, -s, c ); }
float metaDiamond(vec2 p, vec2 pixel, float r){ vec2 d = abs(p-pixel); return r / (d.x + d.y); }
// Dave Hoskins's hash ! Noone can hash hashes like he hashes !
float hash12(vec2 p)
{
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// Smoothed 1D-noise. Just like Zoltraak : stupid simple. Powerful.
float fbm(in vec2 v_p)
{
float pvpx = v_p.x;
vec2 V1 = vec2(floor(pvpx ));
vec2 V2 = vec2(floor(pvpx + 1.0));
return mix(hash12(V1),hash12(V2),smoothstep(0.0,1.0,fract(pvpx)));
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Get Base Coordinates
vec2 p = vec2( (1.0/iResolution.y)*(fragCoord.x - iResolution.x/2.0),fragCoord.y / iResolution.y - 0.5);
// reversed, for accurate Martian perspective... :)
p.x=-p.x;
// Zoom out
p*=150.0;
// Init the Accumulator
vec4 col = vec4(0.05,0.05,0.15,1.0);
// Make up other boxes and save base in them.
vec2 save1 = p;
vec2 save2 = p;
// Faint Nebula Background
// Tilt the camera
p*= r2d(-0.05);
// Space Background Gradient
col = mix(col,vec4(0.2,0.3,0.5,1.0),smoothstep(75.0,0.0,abs(p.y - 5.0*fbm(vec2(0.01*(p.x - 33.333*iTime))) + 3.5)));
// Untilt the camera
p*= r2d( 0.05);
// BG Starfield
// Rotate
p*= r2d(-0.05);
// Zoom In
p*= 0.35;
// Scroll Left
p+= vec2(-5.0*iTime,0.0);
// Hack the coords...
vec2 b = fract(5.0*p);
p = floor(5.0*p);
// Draw the stars
if( fbm(vec2(p.x*p.y)) > 0.996)col += clamp(1.0-pow(3.0*length(b+vec2(-0.5)),0.5),0.0,1.0);
// Reload because the coords are all f.. up now !
p = save1;
// Another Box...
vec2 save3;
// We're going to draw max 4 capsules max.
// Yes we could draw more but Earth must survive, man. Have mercy !
float Nb_Capsules = clamp(NBCaps,0.0,4.0);
for( float i = 0.0;i<Nb_Capsules; i++ )
{
// Reloooooaaaaad !
p = save1;
// Tilt as much as we tilted the stars...
p*= r2d(-0.05);
// Zoom out a bit
p*=2.5;
// Then zoom in a bit closer every loop.
p*=1.0-0.25*i;
// Compute Random Coordinates For Each Capsule Position
// Move To That Position.
p += vec2(150.0*fbm(vec2(0.15*iTime + i*54.321)) - 75.0, // X varies randomly
50.0*sin( 0.25*iTime + i*54.321) - 25.0); // Y varies periodically
// Save Position
save3 = p;
// Mega Zoom
p*=0.04;
// (Ox)-axis Symetry for jets
p.y = abs(p.y);
if( p.x > 0.0 )
{
// Green Jet
col += vec4(0.0,1.0,0.5,1.0)*smoothstep(0.2,0.0,abs(p.y - 0.05*fbm(vec2(1.5*p.x - 40.0*iTime)))-0.05)*smoothstep(29.0,0.0,abs(p.x));
// White Jet
col += vec4(1.0,1.0,1.0,1.0)*smoothstep(0.1,0.0,abs(p.y - 0.05*fbm(vec2(1.5*p.x - 40.0*iTime)))-0.05)*smoothstep(29.0,0.0,abs(p.x));
};
// Reload !
p = save3;
// (Ox)-axis Symetry for the flames
p.y = abs(p.y);
// Fine-tuning Flames position
p+= vec2(-10.0,0.0);
// Fine-tuning Flames Shape
p *= vec2(0.75,1.0);
// Green Flames
col += 0.8*vec4(0.0,1.0,0.5,1.0)*s(20.0,0.0,length(p)-25.0+7.0*sin(0.30*length(p)*atan(p.y,p.x) + 55.0*iTime));
// White flames
col += 0.8*vec4(1.0,1.0,1.0,1.0)*s(20.0,0.0,length(p)-20.0+7.0*sin(0.30*length(p)*atan(p.y,p.x) + 55.0*iTime));
p = save3;
// Fat Aura
col = mix(col,vec4(1.0),0.5*s(10.0,0.0,length(p + vec2(5.0,0.0))-20.0)*abs(sin(50.0*iTime)));
// Less-Fat Aura
col = mix(col,vec4(1.0),0.5*s(20.0,0.0,length(p + vec2(5.0,0.0))-20.0));
// Frieren : "Aura ? Shader yourself !"
// The Pod
// White Disk
col = mix(col,vec4(1.0),s(0.01,0.0,length(p)-20.0));
if( length(p) - 20.0 < 0.0 ) // Basic Masking
{
// 2D Shading : bluish large shadow
col = mix(col,vec4(0.65,0.68,0.68 + 0.1*(3.0-i),1.0),s(0.5,0.0,length(p - vec2(2.0,0.0))-17.0));
// 2D Shading : dark small shadow
// If Outside Porthole Zone
if(s(0.0,1.0,length(vec2(3.0,2.0)*p + vec2(33.5,0.0))-23.0)>0.0)
col = mix(col,vec4(0.45,0.55,0.55 + 0.1*(3.0-i),1.0),0.75*s(0.5,0.0,length(p - vec2(2.0,0.0)+ 0.5*fbm(vec2(4.5*atan(p.y,p.x))))-9.0));
// Small 2D Indentation Details On The Spheres Using A Procedural Texture
// NOTE: Original used texture(iChannel0, ...) which is not supported
// Texture detail removed - not essential for the effect
// vec4 colorCapsule = vec4(hash12(0.0003*p*dot(p,p) + 0.789*i));
// if(colorCapsule.x>0.75)if(s(0.0,1.0,length(vec2(3.0,2.0)*p + vec2(33.5,0.0))-23.0)>0.0)col *= vec4(0.25,0.25,0.25,1.0);
// Bigger Dark Line All Around The Pod
col = mix(col,vec4(0.0),s(0.2,0.0,abs(length(p)-19.9)-0.20));
// Draw The Porthole :
col = mix(col,vec4(0.5,0.2,0.3,1.0) // Base Color
-s(5.0,0.0,length(p + vec2(-6.0,15.0))-20.0) // Main Shadow
-s(0.25,0.0,abs(length(p + vec2(0.0,3.0))-15.0)-0.4)// Vertical Shadow
-s(0.0,1.5,p.y-8.5) // top Shadow
+0.25*vec4(1.0,0.5,0.0,1.0)*s(10.0,0.0,abs(p.y)) // Fake Glass Gradient
,
s(0.5,0.0,length(vec2(3.0,2.0)*p + vec2(35.0,0.0))-19.9));
// Porthole Black Rings
// Internal
col = mix(col,vec4(0.0,0.0,0.0,1.0),s(1.0,0.0,abs(length(vec2(3.0,2.0)*p + vec2(35.0,0.0))-19.9)-0.1));
// External
col = mix(col,vec4(0.0,0.0,0.0,1.0),s(1.0,0.0,abs(length(vec2(3.0,2.0)*p + vec2(33.5,0.0))-23.0)-0.1));
// Pod Tennis-Ball Door Line...
if(p.y>0.0)col = mix(col,vec4(0.0,0.0,0.0,1.0),s(1.0,0.0,abs(length(vec2(3.0,2.0)*p + vec2( 29.0,0.0))-30.0)-0.1));
if(p.y<0.0)col = mix(col,vec4(0.0,0.0,0.0,1.0),s(1.0,0.0,abs(length(vec2(3.0,2.0)*p + vec2(-31.0,0.0))-30.0)-0.1));
};
};
// WAKE UP SHEEPLE !
fragColor = clamp(col,0.0,1.0);
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

View File

@@ -1,3 +1,6 @@
// Name: Fractal Pyramid
// Author: bradjamesgrant
// URL: https://www.shadertoy.com/view/tsXBzS
#version 330 core #version 330 core
precision highp float; precision highp float;

View File

@@ -0,0 +1,159 @@
// Name: Just Another Cube
// Author: mrange
// URL: https://www.shadertoy.com/view/3XdXRr
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
// CC0: Just another cube
// Glowtracers are great for compact coding, but I wanted to see how much
// I could squeeze a more normal raymarcher in terms of characters used.
// Twigl: https://twigl.app?ol=true&ss=-OW-y9xgRgWubwKcn0Nd
// == Globals ==
// Single-letter variable names are used to save characters (code golfing).
mat2 R; // A 2D rotation matrix, calculated once per frame in mainImage and used by D.
float d=1. // Stores the most recent distance to the scene from the ray's position.
, z=0. // Stores the total distance traveled along the ray (initialized to avoid undefined behavior)
, G=9. // "Glow" variable. Tracks the closest the ray comes to the object (for volumetric glow effect).
, M=1e-3
;
// == Distance Function (SDF - Signed Distance Field) ==
// This function calculates the shortest distance from a given point 'p' to the scene geometry.
// A positive result means the point is outside an object, negative is inside, and zero is on the surface.
// This is the core of "raymarching", as it tells us the largest safe step we can take along a ray.
float D(vec3 p) {
// Apply two rotations to the point's coordinates. This twists the space the object
// exists in, making the simple cube shape appear more complex and animated.
p.xy *= R;
p.xz *= R;
// Create a higher-frequency version of the coordinate for detailed surface patterns.
vec3 S = sin(123.*p);
// This creates a volumetric glow effect by tracking the minimum distance
// to either the existing glow value or a glowing shell around the object.
G = min(
G
// The glowing shell
, max(
abs(length(p)-.6)
// The main object distance calculation:
// 1. A superquadric (rounded cube shape) is created using an L8-norm.
// The expression `pow(dot(p=p*p*p*p,p),.125)` is a golfed version of
// `pow(pow(p.x,8)+pow(p.y,8)+pow(p.z,8), 1./8.)`.
// The `- .5` defines the object's size.
, d = pow(dot(p*=p*p*p,p),.125) - .5
// 2. Surface detail subtraction. This creates small surface variations
// using high-frequency sine waves for more appealing reflections.
- pow(1.+S.x*S.y*S.z,8.)/1e5
)
);
return d;
}
// == Main Render Function ==
// This function is called for every pixel on the screen to determine its color.
// 'o' is the final output color (rgba). 'C' is the input pixel coordinate (xy).
void mainImage(out vec4 o, vec2 C) {
// Single-letter variable names are used to save characters (code golfing).
vec3 p // The current point in 3D space along the ray.
, O // Multi-purpose vector: color accumulator, then normal vector, then final color.
, r=vec3(iResolution.xy, iResolution.y) // 'r' holds screen resolution, later re-used for the epsilon vector and reflection.
// 'I' is the Ray Direction vector. It's calculated once per pixel.
// This converts the 2D screen coordinate 'C' into a 3D direction, creating the camera perspective.
, I=normalize(vec3(C-.5*r.xy, r.y))
// Base glow color (dark bluish tint).
, B=vec3(1,2,9)*M
;
// == Raymarching Loop ==
// This loop "marches" a ray from the camera out into the scene to find what it hits.
// It uses a golfed structure where the body of the loop updates the ray position 'p',
// and the "advancement" step moves the ray forward.
for(
// -- Initializer (runs once before the loop) --
// Calculate the rotation matrix for this frame based on time.
R = mat2(cos(.3*iTime+vec4(0,11,33,0)))
// -- Condition --
// Loop while total distance 'z' is less than 9 and we are not yet touching a surface (d > 1e-3).
; z<9. && d > M
// -- Advancement --
// The ray advances by the safe distance 'd' returned by D(p).
// The result of D(p) is also assigned to the global 'd' inside the function.
; z += D(p)
)
// -- Loop Body --
// Calculate the current position 'p' in world space.
// The camera starts at (0,0,-2) and points forward.
p = z*I
, p.z -= 2.
;
// -- Hit Condition --
// If the loop finished because z exceeded the max distance, we hit nothing. Otherwise, we hit the surface.
if (z < 9.) {
// -- Calculate Surface Normal --
// Estimate the gradient ∇D at the hit point 'p' via central differences on the SDF D.
// We use ε = 1e-3 and loop over each axis (x, y, z):
// • Zero r, then set r[i] = ε.
// • Compute O[i] = D(p + r) D(p r).
// After the loop, O holds the unnormalized normal vector.
for (
int i=0 // axis index: 0→x, 1→y, 2→z (initialized to avoid warnings)
; i < 3
; O[i++] = D(p+r) - D(p-r)
)
r -= r // clear r to vec3(0)
, r[i] = M // set only the i-th component
;
// -- Lighting and Shading --
// 'z' is re-purposed to store a fresnel factor (1 - cos(angle)) for edge brightness.
// `dot(O, I)` calculates how much the surface faces away from the camera.
// O is also normalized here to become a proper normal vector.
z = 1.+dot(O = normalize(O),I);
// 'r' is re-purposed to store the reflection vector.
r = reflect(I,O);
// Calculate a point 'C' along the reflection vector 'r' to sample a background color.
// For upward reflections (r.y > 0), this finds the intersection with the plane y=5.
C = (p+r*(5.-p.y)/abs(r.y)).xz;
// Calculate the final color 'O' of the hit point.
O =
// Multiply by the fresnel factor squared for stronger edge reflections.
z*z *
// Use a ternary operator to decide the color based on where the reflection ray goes.
(
// If the reflection vector points upward...
r.y>0.
// ...sample a procedural "sky" with a radial gradient and blue tint.
? 5e2*smoothstep(5., 4., d = sqrt(length(C*C))+1.)*d*B
// ...otherwise, sample a "floor" with a deep blue exponential falloff.
: exp(-2.*length(C))*(B/M-1.)
)
// Add rim lighting (brighter on upward-facing surfaces).
+ pow(1.+O.y,5.)*B
;
}
// == Tonemapping & Output ==
// Apply final effects and map the High Dynamic Range (HDR) color to a displayable range.
// Add glow contribution: smaller G values (closer ray passes) create a brighter blue glow.
o = sqrt(O+B/G).xyzx;
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

View File

@@ -1,3 +1,6 @@
// Name: Octograms
// Author: whisky_shusuky
// URL: https://www.shadertoy.com/view/tlVGDt
#version 330 core #version 330 core
precision highp float; precision highp float;

View File

@@ -0,0 +1,73 @@
// Name: Remember
// Author: diatribes
// URL: https://www.shadertoy.com/view/tXSBDK
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
// fuzzy brain
// Hash function to replace iChannel0 texture noise
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void mainImage(out vec4 o, vec2 u) {
vec3 q,p = vec3(iResolution.xy, iResolution.x / iResolution.y);
float i = 0.0, s,
// start the ray at a small random distance,
// this will reduce banding
// Replaced texelFetch(iChannel0, ...) with hash function
d = .125 * hash12(u),
t = iTime * .1;
// scale coords
u = (u+u-p.xy)/p.y;
if (abs(u.y) > .8) { o = vec4(0); return; }
// Initialize output color (out parameter must be initialized before use)
o = vec4(0.0);
for(; i<64.; i++) {
// shorthand for standard raymarch sample, then move forward:
// p = ro + rd * d, p.z + t
q = p = vec3(u * d, d + t*5.);
p.xy *= mat2(cos(.1*p.z+.1*t+vec4(0,33,11,0)));
q.xz = cos(q.xz);
p.z = cos(p.z) ;
// turbulence
for (s = 1.; s++ <6.;
q += sin(.6*t+p.zxy*.6),
p += sin(t+t+p.yzx*s)*.6);
// distance to spheres
d += s = .02 + abs(min(length(p+3.*sin(p.z*.5))-4., length(q-2.*sin(p.z*.4))-6.))*.2;
// color: 1.+cos so we don't go negative, cos(d+vec4(6,4,2,0)) samples from the palette
// divide by s for form and distance
// Clamp only the first term to prevent extreme overflow, leave second term free
vec4 brightTerm = min(.01*vec4(6,2,1,0)/max(length(u*sin(t+t+t)), 0.001), vec4(50.0));
o += brightTerm + 1. / s * length(u);
}
// tonemap and divide brightness
o = tanh(max(o /6e2 + dot(u,u)*.35, 0.));
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

223
shaders/seascape.frag.glsl Normal file
View File

@@ -0,0 +1,223 @@
// Name: Seascape
// Author: Alexander Alekseev
// URL: https://www.shadertoy.com/view/Ms2SD1
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
/*
* "Seascape" by Alexander Alekseev aka TDM - 2014
* License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
* Contact: tdmaav@gmail.com
*/
const int NUM_STEPS = 32;
const float PI = 3.141592;
const float EPSILON = 1e-3;
#define EPSILON_NRM (0.1 / iResolution.x)
//#define AA
// sea
const int ITER_GEOMETRY = 3;
const int ITER_FRAGMENT = 5;
const float SEA_HEIGHT = 0.6;
const float SEA_CHOPPY = 4.0;
const float SEA_SPEED = 0.8;
const float SEA_FREQ = 0.16;
const vec3 SEA_BASE = vec3(0.0,0.09,0.18);
const vec3 SEA_WATER_COLOR = vec3(0.8,0.9,0.6)*0.6;
#define SEA_TIME (1.0 + iTime * SEA_SPEED)
const mat2 octave_m = mat2(1.6,1.2,-1.2,1.6);
// math
mat3 fromEuler(vec3 ang) {
vec2 a1 = vec2(sin(ang.x),cos(ang.x));
vec2 a2 = vec2(sin(ang.y),cos(ang.y));
vec2 a3 = vec2(sin(ang.z),cos(ang.z));
mat3 m;
m[0] = vec3(a1.y*a3.y+a1.x*a2.x*a3.x,a1.y*a2.x*a3.x+a3.y*a1.x,-a2.y*a3.x);
m[1] = vec3(-a2.y*a1.x,a1.y*a2.y,a2.x);
m[2] = vec3(a3.y*a1.x*a2.x+a1.y*a3.x,a1.x*a3.x-a1.y*a3.y*a2.x,a2.y*a3.y);
return m;
}
float hash( vec2 p ) {
float h = dot(p,vec2(127.1,311.7));
return fract(sin(h)*43758.5453123);
}
float noise( in vec2 p ) {
vec2 i = floor( p );
vec2 f = fract( p );
vec2 u = f*f*(3.0-2.0*f);
return -1.0+2.0*mix( mix( hash( i + vec2(0.0,0.0) ),
hash( i + vec2(1.0,0.0) ), u.x),
mix( hash( i + vec2(0.0,1.0) ),
hash( i + vec2(1.0,1.0) ), u.x), u.y);
}
// lighting
float diffuse(vec3 n,vec3 l,float p) {
return pow(dot(n,l) * 0.4 + 0.6,p);
}
float specular(vec3 n,vec3 l,vec3 e,float s) {
float nrm = (s + 8.0) / (PI * 8.0);
return pow(max(dot(reflect(e,n),l),0.0),s) * nrm;
}
// sky
vec3 getSkyColor(vec3 e) {
e.y = (max(e.y,0.0)*0.8+0.2)*0.8;
return vec3(pow(1.0-e.y,2.0), 1.0-e.y, 0.6+(1.0-e.y)*0.4) * 1.1;
}
// sea
float sea_octave(vec2 uv, float choppy) {
uv += noise(uv);
vec2 wv = 1.0-abs(sin(uv));
vec2 swv = abs(cos(uv));
wv = mix(wv,swv,wv);
return pow(1.0-pow(wv.x * wv.y,0.65),choppy);
}
float map(vec3 p) {
float freq = SEA_FREQ;
float amp = SEA_HEIGHT;
float choppy = SEA_CHOPPY;
vec2 uv = p.xz; uv.x *= 0.75;
float d, h = 0.0;
for(int i = 0; i < ITER_GEOMETRY; i++) {
d = sea_octave((uv+SEA_TIME)*freq,choppy);
d += sea_octave((uv-SEA_TIME)*freq,choppy);
h += d * amp;
uv *= octave_m; freq *= 1.9; amp *= 0.22;
choppy = mix(choppy,1.0,0.2);
}
return p.y - h;
}
float map_detailed(vec3 p) {
float freq = SEA_FREQ;
float amp = SEA_HEIGHT;
float choppy = SEA_CHOPPY;
vec2 uv = p.xz; uv.x *= 0.75;
float d, h = 0.0;
for(int i = 0; i < ITER_FRAGMENT; i++) {
d = sea_octave((uv+SEA_TIME)*freq,choppy);
d += sea_octave((uv-SEA_TIME)*freq,choppy);
h += d * amp;
uv *= octave_m; freq *= 1.9; amp *= 0.22;
choppy = mix(choppy,1.0,0.2);
}
return p.y - h;
}
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {
float fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
fresnel = min(fresnel * fresnel * fresnel, 0.5);
vec3 reflected = getSkyColor(reflect(eye, n));
vec3 refracted = SEA_BASE + diffuse(n, l, 80.0) * SEA_WATER_COLOR * 0.12;
vec3 color = mix(refracted, reflected, fresnel);
float atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * atten;
color += specular(n, l, eye, 600.0 * inversesqrt(dot(dist,dist)));
return color;
}
// tracing
vec3 getNormal(vec3 p, float eps) {
vec3 n;
n.y = map_detailed(p);
n.x = map_detailed(vec3(p.x+eps,p.y,p.z)) - n.y;
n.z = map_detailed(vec3(p.x,p.y,p.z+eps)) - n.y;
n.y = eps;
return normalize(n);
}
float heightMapTracing(vec3 ori, vec3 dir, out vec3 p) {
float tm = 0.0;
float tx = 1000.0;
float hx = map(ori + dir * tx);
if(hx > 0.0) {
p = ori + dir * tx;
return tx;
}
float hm = map(ori);
for(int i = 0; i < NUM_STEPS; i++) {
float tmid = mix(tm, tx, hm / (hm - hx));
p = ori + dir * tmid;
float hmid = map(p);
if(hmid < 0.0) {
tx = tmid;
hx = hmid;
} else {
tm = tmid;
hm = hmid;
}
if(abs(hmid) < EPSILON) break;
}
return mix(tm, tx, hm / (hm - hx));
}
vec3 getPixel(in vec2 coord, float time) {
vec2 uv = coord / iResolution.xy;
uv = uv * 2.0 - 1.0;
uv.x *= iResolution.x / iResolution.y;
// ray
vec3 ang = vec3(sin(time*3.0)*0.1,sin(time)*0.2+0.3,time);
vec3 ori = vec3(0.0,3.5,time*5.0);
vec3 dir = normalize(vec3(uv.xy,-2.0)); dir.z += length(uv) * 0.14;
dir = normalize(dir) * fromEuler(ang);
// tracing
vec3 p;
heightMapTracing(ori,dir,p);
vec3 dist = p - ori;
vec3 n = getNormal(p, dot(dist,dist) * EPSILON_NRM);
vec3 light = normalize(vec3(0.0,1.0,0.8));
// color
return mix(
getSkyColor(dir),
getSeaColor(p,n,light,dir,dist),
pow(smoothstep(0.0,-0.02,dir.y),0.2));
}
// main
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
// Removed mouse interaction (iMouse not available)
float time = iTime * 0.3;
#ifdef AA
vec3 color = vec3(0.0);
for(int i = -1; i <= 1; i++) {
for(int j = -1; j <= 1; j++) {
vec2 uv = fragCoord+vec2(i,j)/3.0;
color += getPixel(uv, time);
}
}
color /= 9.0;
#else
vec3 color = getPixel(fragCoord, time);
#endif
// post
fragColor = vec4(pow(color,vec3(0.65)), 1.0);
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

View File

@@ -1,3 +1,6 @@
// Name: Shader Art Coding Introduction
// Author: kishimisu
// URL: https://www.shadertoy.com/view/mtyGWy
#version 330 core #version 330 core
precision highp float; precision highp float;

View File

@@ -1,3 +1,5 @@
// Name: Test
// Author: JailDesigner
#version 330 core #version 330 core
out vec4 FragColor; out vec4 FragColor;
in vec2 vUV; in vec2 vUV;

View File

@@ -0,0 +1,52 @@
// Name: Voxel Descent
// Author: Jaenman
// URL: https://www.shadertoy.com/view/Wc3cRr
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
/*================================
= Voxel Descent =
= Author: Jaenam =
================================*/
// Date: 2025-11-14
// License: Creative Commons (CC BY-NC-SA 4.0)
// Thanks to @diatribes for the plasma :D
void mainImage( out vec4 O, vec2 I )
{
float i=0.,d=0.,s=0.,t = iTime*.8;
vec3 p,q,z,k;
vec4 c = vec4(1,2,3,1);
mat2 R = mat2(cos(t/3.+vec4(0,33,11,0)));
// Initialize O before loop (was O*=i where i=0, reading uninitialized out parameter)
O = vec4(0.0);
for(; i++<1e2;O+=sin(.5*c+i*.2)/s)
z = normalize(vec3(I+I, 0) - vec3(iResolution.xy, iResolution.y)),
z.xy*=R,
p = d*z+sqrt(i)*i,
k=z/=length(z.xy),
z.z-=t, p.z+=t,
d+=s=.012+.07*abs(mix(cos(dot(sin(floor(p/8.)).yzx,cos(ceil(z/.1)))),length(k),.2)-i/1e2);
O*= exp(-.02*d*d);
// Add epsilon protection to prevent division overflow
O=tanh(O*O*mix(length(abs(c/max(dot(cos(t+p),vec3(6)), 0.001))),length(k),.5)/6e4);
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

44
shaders/water.glsl Normal file
View File

@@ -0,0 +1,44 @@
// Name: Water
// Author: diatribes
// URL: https://www.shadertoy.com/view/tXjXDy
#version 330 core
precision highp float;
out vec4 FragColor;
in vec2 vUV;
uniform vec2 iResolution;
uniform float iTime;
/*
-2 by @FabriceNeyret2
thanks!! :D
If it doesn't display correctly, change line 17 "r/r" to "vec3(1)"
*/
void mainImage( out vec4 o, vec2 u ) {
float s=.002, i=0., n; // FIXED: Initialize i=0
vec3 r = vec3(iResolution.xy, iResolution.x/iResolution.y);
vec3 p = vec3(0);
u = (u-r.xy/2.)/r.y-.3;
o = vec4(0); // FIXED: Initialize output to black
for(; i < 32. && s > .001; i++) {
// Clamp only extreme overflow values, let normal brightness through
vec4 term = vec4(5,2,1,0)/max(length(u-.1), 0.001);
o += min(term, vec4(100.0));
for (p += vec3(u*s,s), s = 1. + p.y, n =.01; n < 1.; n+=n) {
s += abs(dot(sin(p.z+iTime+p / n), vec3(1))) * n*.1;
}
}
o = tanh(o/5e2);
}
void main() {
vec2 fragCoordPixels = vUV * iResolution;
vec4 outColor;
mainImage(outColor, fragCoordPixels);
FragColor = outColor;
}

View File

@@ -7,8 +7,10 @@
#include <algorithm> #include <algorithm>
#include <type_traits> #include <type_traits>
#include <filesystem> #include <filesystem>
#include <ctime>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <glad/glad.h> #include <glad/glad.h>
#include "jail_audio.h"
#include "defines.hpp" #include "defines.hpp"
@@ -21,6 +23,7 @@ struct Logger {
// Opciones mínimas parecidas a las tuyas // Opciones mínimas parecidas a las tuyas
struct VideoOptions { struct VideoOptions {
bool fullscreen = false; bool fullscreen = false;
bool vsync = true;
} Options_video; } Options_video;
// Estructura para guardar info del display // Estructura para guardar info del display
@@ -31,17 +34,66 @@ struct DisplayMonitor {
int refresh_rate = 0; int refresh_rate = 0;
}; };
// Forward declarations of structs
struct ShaderMetadata {
std::string name;
std::string author;
std::string iChannel0; // "BufferA", "BufferB", "none", etc.
std::string iChannel1;
std::string iChannel2;
std::string iChannel3;
};
struct ShaderBuffer {
GLuint program = 0; // Shader program for this buffer
GLuint fbo = 0; // Framebuffer object
GLuint texture = 0; // Output texture
std::string name; // "BufferA", "BufferB", etc.
};
struct ShaderPass {
std::string shaderName; // Base name (e.g., "water")
std::string displayName; // Custom name from metadata
std::string author; // Author from metadata
GLuint imageProgram = 0; // Main image shader program
std::vector<ShaderBuffer> buffers; // BufferA, BufferB, etc.
ShaderMetadata metadata; // iChannel configuration
};
// Globales simplificados (tu proyecto puede integrarlo en clases) // Globales simplificados (tu proyecto puede integrarlo en clases)
static DisplayMonitor display_monitor_; static DisplayMonitor display_monitor_;
static SDL_Window* window_ = nullptr; static SDL_Window* window_ = nullptr;
// Sistema de shaders // Sistema de shaders (legacy - kept for backward compatibility with single-pass shaders)
static std::vector<std::filesystem::path> shader_list_; static std::vector<std::filesystem::path> shader_list_;
static std::vector<std::string> shader_names_; // Custom names from "// Name: XXX" comments
static std::vector<std::string> shader_authors_; // Custom authors from "// Author: XXX" comments
static size_t current_shader_index_ = 0; static size_t current_shader_index_ = 0;
static std::filesystem::path shaders_directory_; static std::filesystem::path shaders_directory_;
static GLuint current_program_ = 0; static GLuint current_program_ = 0;
static Uint32 shader_start_ticks_ = 0; static Uint32 shader_start_ticks_ = 0;
// Multi-pass shader system
static std::vector<ShaderPass> shader_passes_;
static int current_window_width_ = 0;
static int current_window_height_ = 0;
// Self-feedback system (for shaders that use their own output as input)
static GLuint feedback_fbo_ = 0;
static GLuint feedback_texture_ = 0;
static bool current_shader_uses_feedback_ = false;
static int feedback_channel_ = -1; // Which iChannel (0-3) is used for feedback
// FPS tracking
static Uint32 fps_frame_count_ = 0;
static Uint32 fps_last_update_ticks_ = 0;
static float current_fps_ = 0.0f;
// Sistema de música
static std::vector<std::filesystem::path> music_list_;
static size_t current_music_index_ = 0;
static JA_Music_t* current_music_ = nullptr;
// Vertex shader embebido // Vertex shader embebido
static const char* vertexShaderSrc = R"glsl( static const char* vertexShaderSrc = R"glsl(
#version 330 core #version 330 core
@@ -63,6 +115,64 @@ static bool loadFileToString(const std::filesystem::path& path, std::string& out
return true; return true;
} }
static std::string trimString(const std::string& str) {
size_t start = str.find_first_not_of(" \t\r\n");
size_t end = str.find_last_not_of(" \t\r\n");
if (start != std::string::npos && end != std::string::npos) {
return str.substr(start, end - start + 1);
}
return "";
}
static ShaderMetadata extractShaderMetadata(const std::string& shaderSource) {
ShaderMetadata metadata;
metadata.iChannel0 = "none";
metadata.iChannel1 = "none";
metadata.iChannel2 = "none";
metadata.iChannel3 = "none";
std::istringstream stream(shaderSource);
std::string line;
int lineCount = 0;
const int maxLinesToCheck = 30;
while (std::getline(stream, line) && lineCount < maxLinesToCheck) {
lineCount++;
// Look for "// XXX: YYY" patterns (case-insensitive)
size_t pos = line.find("//");
if (pos != std::string::npos) {
std::string comment = line.substr(pos + 2);
std::string commentLower = comment;
std::transform(commentLower.begin(), commentLower.end(), commentLower.begin(), ::tolower);
// Check for Name:
if (commentLower.find("name:") != std::string::npos) {
metadata.name = trimString(comment.substr(comment.find(":") + 1));
}
// Check for Author:
else if (commentLower.find("author:") != std::string::npos) {
metadata.author = trimString(comment.substr(comment.find(":") + 1));
}
// Check for iChannel0-3:
else if (commentLower.find("ichannel0:") != std::string::npos) {
metadata.iChannel0 = trimString(comment.substr(comment.find(":") + 1));
}
else if (commentLower.find("ichannel1:") != std::string::npos) {
metadata.iChannel1 = trimString(comment.substr(comment.find(":") + 1));
}
else if (commentLower.find("ichannel2:") != std::string::npos) {
metadata.iChannel2 = trimString(comment.substr(comment.find(":") + 1));
}
else if (commentLower.find("ichannel3:") != std::string::npos) {
metadata.iChannel3 = trimString(comment.substr(comment.find(":") + 1));
}
}
}
return metadata;
}
static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesystem::path& directory) { static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesystem::path& directory) {
std::vector<std::filesystem::path> shaders; std::vector<std::filesystem::path> shaders;
@@ -84,14 +194,333 @@ static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesys
std::sort(shaders.begin(), shaders.end()); std::sort(shaders.begin(), shaders.end());
Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string()); Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string());
// Initialize shader metadata vectors with empty strings (will be filled when shaders are loaded)
shader_names_.resize(shaders.size(), "");
shader_authors_.resize(shaders.size(), "");
return shaders; return shaders;
} }
static std::vector<std::filesystem::path> scanMusicDirectory(const std::filesystem::path& directory) {
std::vector<std::filesystem::path> music_files;
if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
Logger::info("Music directory does not exist: " + directory.string());
return music_files;
}
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
if (entry.is_regular_file()) {
auto ext = entry.path().extension().string();
if (ext == ".ogg") {
music_files.push_back(entry.path());
}
}
}
// Ordenar alfabéticamente
std::sort(music_files.begin(), music_files.end());
Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string());
return music_files;
}
static void playRandomMusic() {
if (music_list_.empty()) return;
// Liberar música anterior si existe
if (current_music_) {
JA_DeleteMusic(current_music_);
current_music_ = nullptr;
}
// Elegir índice aleatorio
current_music_index_ = rand() % music_list_.size();
// Cargar y reproducir música (sin loop, loop=0)
const auto& music_path = music_list_[current_music_index_];
current_music_ = JA_LoadMusic(music_path.string().c_str());
if (current_music_) {
JA_PlayMusic(current_music_, 0); // 0 = no loop, se reproduce una vez
Logger::info("Now playing: " + music_path.filename().string());
} else {
Logger::error("Failed to load music: " + music_path.string());
}
}
// ===== Multi-pass FBO/Texture Management =====
static bool createBufferFBO(ShaderBuffer& buffer, int width, int height) {
// Create texture
glGenTextures(1, &buffer.texture);
glBindTexture(GL_TEXTURE_2D, buffer.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
// Create FBO
glGenFramebuffers(1, &buffer.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, buffer.fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buffer.texture, 0);
// Check FBO completeness
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
Logger::error("FBO creation failed for " + buffer.name + ": " + std::to_string(status));
return false;
}
Logger::info("Created FBO for " + buffer.name + " (" + std::to_string(width) + "x" + std::to_string(height) + ")");
return true;
}
static void destroyBuffer(ShaderBuffer& buffer) {
if (buffer.fbo != 0) {
glDeleteFramebuffers(1, &buffer.fbo);
buffer.fbo = 0;
}
if (buffer.texture != 0) {
glDeleteTextures(1, &buffer.texture);
buffer.texture = 0;
}
if (buffer.program != 0) {
glDeleteProgram(buffer.program);
buffer.program = 0;
}
}
static void destroyShaderPass(ShaderPass& pass) {
if (pass.imageProgram != 0) {
glDeleteProgram(pass.imageProgram);
pass.imageProgram = 0;
}
for (auto& buffer : pass.buffers) {
destroyBuffer(buffer);
}
pass.buffers.clear();
}
static bool resizeBuffersIfNeeded(ShaderPass& pass, int width, int height) {
if (current_window_width_ == width && current_window_height_ == height) {
return false; // No resize needed
}
Logger::info("Resizing buffers: " + std::to_string(width) + "x" + std::to_string(height));
// Destroy and recreate all buffers with new size
for (auto& buffer : pass.buffers) {
// Keep program, destroy FBO/texture only
if (buffer.fbo != 0) glDeleteFramebuffers(1, &buffer.fbo);
if (buffer.texture != 0) glDeleteTextures(1, &buffer.texture);
buffer.fbo = 0;
buffer.texture = 0;
if (!createBufferFBO(buffer, width, height)) {
return false;
}
}
current_window_width_ = width;
current_window_height_ = height;
return true;
}
// ===== Self-Feedback System =====
static bool createFeedbackFBO(int width, int height) {
// Delete existing if any
if (feedback_fbo_ != 0) glDeleteFramebuffers(1, &feedback_fbo_);
if (feedback_texture_ != 0) glDeleteTextures(1, &feedback_texture_);
// Create texture
glGenTextures(1, &feedback_texture_);
glBindTexture(GL_TEXTURE_2D, feedback_texture_);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Clear to black initially
std::vector<float> black(width * height * 4, 0.0f);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, black.data());
glBindTexture(GL_TEXTURE_2D, 0);
// Create FBO
glGenFramebuffers(1, &feedback_fbo_);
glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, feedback_texture_, 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
Logger::error("Feedback FBO creation failed: " + std::to_string(status));
return false;
}
Logger::info("Created feedback FBO (" + std::to_string(width) + "x" + std::to_string(height) + ")");
return true;
}
static void destroyFeedbackFBO() {
if (feedback_fbo_ != 0) {
glDeleteFramebuffers(1, &feedback_fbo_);
feedback_fbo_ = 0;
}
if (feedback_texture_ != 0) {
glDeleteTextures(1, &feedback_texture_);
feedback_texture_ = 0;
}
current_shader_uses_feedback_ = false;
feedback_channel_ = -1;
}
static int detectFeedbackChannel(const ShaderMetadata& metadata) {
// Check which iChannel uses "self" feedback
if (metadata.iChannel0 == "self") return 0;
if (metadata.iChannel1 == "self") return 1;
if (metadata.iChannel2 == "self") return 2;
if (metadata.iChannel3 == "self") return 3;
return -1; // No feedback
}
// ===== Multi-pass Shader Loading =====
// Forward declarations
static GLuint compileShader(GLenum type, const char* src);
static GLuint linkProgram(GLuint vs, GLuint fs);
static std::vector<std::string> findBufferFiles(const std::filesystem::path& imagePath) {
std::vector<std::string> buffers;
std::filesystem::path dir = imagePath.parent_path();
std::string basename = imagePath.stem().stem().string(); // Remove .image.glsl -> get base name
// Check for BufferA, BufferB, BufferC, BufferD
std::vector<std::string> bufferNames = {"bufferA", "bufferB", "bufferC", "bufferD"};
for (const auto& bufName : bufferNames) {
std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl");
if (std::filesystem::exists(bufferPath)) {
buffers.push_back(bufName);
Logger::info("Found buffer: " + bufferPath.string());
}
}
return buffers;
}
static GLuint loadMultiPassShader(const std::filesystem::path& imagePath, ShaderPass& outPass, int width, int height) {
std::string basename = imagePath.stem().stem().string();
outPass.shaderName = basename;
// Load and compile Image shader
std::string imageSrc;
if (!loadFileToString(imagePath, imageSrc)) {
Logger::error("Failed to load image shader: " + imagePath.string());
return 0;
}
// Extract metadata from Image shader
outPass.metadata = extractShaderMetadata(imageSrc);
outPass.displayName = outPass.metadata.name.empty() ? basename : outPass.metadata.name;
outPass.author = outPass.metadata.author;
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
GLuint fs = compileShader(GL_FRAGMENT_SHADER, imageSrc.c_str());
if (!vs || !fs) {
if (vs) glDeleteShader(vs);
if (fs) glDeleteShader(fs);
return 0;
}
outPass.imageProgram = linkProgram(vs, fs);
glDeleteShader(vs);
glDeleteShader(fs);
if (!outPass.imageProgram) {
return 0;
}
// Find and load buffer shaders
std::vector<std::string> bufferNames = findBufferFiles(imagePath);
std::filesystem::path dir = imagePath.parent_path();
for (const auto& bufName : bufferNames) {
ShaderBuffer buffer;
buffer.name = bufName;
// Load buffer shader source
std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl");
std::string bufferSrc;
if (!loadFileToString(bufferPath, bufferSrc)) {
Logger::error("Failed to load buffer: " + bufferPath.string());
continue;
}
// Compile buffer shader
GLuint bufVs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
GLuint bufFs = compileShader(GL_FRAGMENT_SHADER, bufferSrc.c_str());
if (!bufVs || !bufFs) {
if (bufVs) glDeleteShader(bufVs);
if (bufFs) glDeleteShader(bufFs);
continue;
}
buffer.program = linkProgram(bufVs, bufFs);
glDeleteShader(bufVs);
glDeleteShader(bufFs);
if (!buffer.program) {
continue;
}
// Create FBO and texture for this buffer
if (!createBufferFBO(buffer, width, height)) {
glDeleteProgram(buffer.program);
continue;
}
outPass.buffers.push_back(buffer);
Logger::info("Loaded buffer: " + bufName);
}
Logger::info("Multi-pass shader loaded: " + outPass.displayName + " (" + std::to_string(outPass.buffers.size()) + " buffers)");
return outPass.imageProgram;
}
static void updateWindowTitle() { static void updateWindowTitle() {
if (!window_ || shader_list_.empty()) return; if (!window_ || shader_list_.empty()) return;
std::string filename = shader_list_[current_shader_index_].filename().string(); // Use custom shader name if available, otherwise fallback to filename
std::string title = std::string(APP_NAME) + " (" + filename + ")"; std::string shaderName;
if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) {
shaderName = shader_names_[current_shader_index_];
} else {
shaderName = shader_list_[current_shader_index_].filename().string();
}
// Add author if available
if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) {
shaderName += " by " + shader_authors_[current_shader_index_];
}
std::string title = std::string(APP_NAME) + " (" + shaderName + ")";
if (current_fps_ > 0.0f) {
title += " - " + std::to_string(static_cast<int>(current_fps_ + 0.5f)) + " FPS";
}
title += Options_video.vsync ? " [VSync ON]" : " [VSync OFF]";
SDL_SetWindowTitle(window_, title.c_str()); SDL_SetWindowTitle(window_, title.c_str());
} }
@@ -147,6 +576,25 @@ static GLuint loadAndCompileShader(size_t index) {
return 0; return 0;
} }
// Extract custom shader metadata (name, author, iChannels) from source code
ShaderMetadata metadata = extractShaderMetadata(fragSrc);
if (!metadata.name.empty()) {
shader_names_[index] = metadata.name;
Logger::info("Shader name: " + metadata.name);
}
if (!metadata.author.empty()) {
shader_authors_[index] = metadata.author;
Logger::info("Shader author: " + metadata.author);
}
// Detect self-feedback
feedback_channel_ = detectFeedbackChannel(metadata);
current_shader_uses_feedback_ = (feedback_channel_ >= 0);
if (current_shader_uses_feedback_) {
Logger::info("Shader uses self-feedback on iChannel" + std::to_string(feedback_channel_));
}
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc); GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str()); GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str());
@@ -208,11 +656,15 @@ void setFullscreenMode() {
SDL_SetWindowFullscreen(window_, false); SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
Options_video.fullscreen = false; Options_video.fullscreen = false;
SDL_ShowCursor(); // Show cursor on fallback to windowed
} else {
SDL_HideCursor(); // Hide cursor in fullscreen
} }
} else { } else {
// Volver a modo ventana 800x800 // Volver a modo ventana 800x800
SDL_SetWindowFullscreen(window_, false); SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
SDL_ShowCursor(); // Show cursor in windowed mode
} }
} }
@@ -221,6 +673,17 @@ void toggleFullscreen() {
setFullscreenMode(); setFullscreenMode();
} }
void toggleVSync() {
Options_video.vsync = !Options_video.vsync;
int result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);
if (result == 0) {
Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
} else {
Logger::error(std::string("Failed to set VSync: ") + SDL_GetError());
}
}
void switchShader(int direction) { void switchShader(int direction) {
if (shader_list_.empty()) return; if (shader_list_.empty()) return;
@@ -244,6 +707,9 @@ void switchShader(int direction) {
glDeleteProgram(current_program_); glDeleteProgram(current_program_);
} }
// Destroy feedback FBO from previous shader
destroyFeedbackFBO();
current_program_ = new_program; current_program_ = new_program;
current_shader_index_ = new_index; current_shader_index_ = new_index;
shader_start_ticks_ = SDL_GetTicks(); shader_start_ticks_ = SDL_GetTicks();
@@ -260,6 +726,10 @@ void handleDebugEvents(const SDL_Event& event) {
toggleFullscreen(); toggleFullscreen();
break; break;
} }
case SDLK_F4: {
toggleVSync();
break;
}
case SDLK_LEFT: { case SDLK_LEFT: {
switchShader(-1); switchShader(-1);
break; break;
@@ -287,7 +757,7 @@ int main(int argc, char** argv) {
Options_video.fullscreen = fullscreenFlag; Options_video.fullscreen = fullscreenFlag;
// Inicializar SDL3 // Inicializar SDL3
auto initResult = SDL_Init(SDL_INIT_VIDEO); auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
if constexpr (std::is_same_v<decltype(initResult), bool>) { if constexpr (std::is_same_v<decltype(initResult), bool>) {
if (!initResult) { Logger::error(SDL_GetError()); return -1; } if (!initResult) { Logger::error(SDL_GetError()); return -1; }
} else { } else {
@@ -326,9 +796,34 @@ int main(int argc, char** argv) {
return -1; return -1;
} }
// Set initial vsync state
int vsync_result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);
if (vsync_result == 0) {
Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
} else {
Logger::error(std::string("Failed to set initial VSync: ") + SDL_GetError());
}
// Inicializar jail_audio
JA_Init(48000, SDL_AUDIO_S16LE, 2);
// Obtener directorio de recursos // Obtener directorio de recursos
std::string resources_dir = getResourcesDirectory(); std::string resources_dir = getResourcesDirectory();
// Inicializar generador de números aleatorios
srand(static_cast<unsigned int>(time(nullptr)));
// Escanear directorio de música
std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music";
music_list_ = scanMusicDirectory(music_directory);
// Reproducir primera canción aleatoria
if (!music_list_.empty()) {
playRandomMusic();
} else {
Logger::info("No music files found in " + music_directory.string());
}
// Determinar carpeta de shaders // Determinar carpeta de shaders
std::filesystem::path shaderFile(shaderPath); std::filesystem::path shaderFile(shaderPath);
if (shaderFile.has_parent_path()) { if (shaderFile.has_parent_path()) {
@@ -391,6 +886,7 @@ int main(int argc, char** argv) {
} }
shader_start_ticks_ = SDL_GetTicks(); shader_start_ticks_ = SDL_GetTicks();
fps_last_update_ticks_ = SDL_GetTicks();
updateWindowTitle(); updateWindowTitle();
// Quad setup // Quad setup
@@ -415,6 +911,27 @@ int main(int argc, char** argv) {
bool running = true; bool running = true;
while (running) { while (running) {
// Update FPS counter
fps_frame_count_++;
Uint32 current_ticks = SDL_GetTicks();
// Update FPS display every 500ms
if (current_ticks - fps_last_update_ticks_ >= 500) {
float elapsed_seconds = (current_ticks - fps_last_update_ticks_) / 1000.0f;
current_fps_ = fps_frame_count_ / elapsed_seconds;
fps_frame_count_ = 0;
fps_last_update_ticks_ = current_ticks;
updateWindowTitle();
}
// Actualizar audio (necesario para streaming y loops)
JA_Update();
// Verificar si la música actual terminó y reproducir siguiente aleatoria
if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) {
playRandomMusic();
}
SDL_Event e; SDL_Event e;
while (SDL_PollEvent(&e)) { while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) running = false; if (e.type == SDL_EVENT_QUIT) running = false;
@@ -431,28 +948,81 @@ int main(int argc, char** argv) {
int w, h; int w, h;
SDL_GetWindowSize(window_, &w, &h); SDL_GetWindowSize(window_, &w, &h);
glViewport(0, 0, w, h);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // Create/resize feedback FBO if needed
glClear(GL_COLOR_BUFFER_BIT); if (current_shader_uses_feedback_) {
if (feedback_fbo_ == 0 || current_window_width_ != w || current_window_height_ != h) {
createFeedbackFBO(w, h);
current_window_width_ = w;
current_window_height_ = h;
}
}
glUseProgram(current_program_); glUseProgram(current_program_);
// Obtener uniform locations (se recalculan porque el shader puede cambiar) // Obtener uniform locations
GLint locRes = glGetUniformLocation(current_program_, "iResolution"); GLint locRes = glGetUniformLocation(current_program_, "iResolution");
GLint locTime = glGetUniformLocation(current_program_, "iTime"); GLint locTime = glGetUniformLocation(current_program_, "iTime");
if (locRes >= 0) glUniform2f(locRes, float(w), float(h)); float t = (SDL_GetTicks() - shader_start_ticks_) / 1000.0f;
if (locTime >= 0) {
float t = (SDL_GetTicks() - shader_start_ticks_) / 1000.0f; // === FEEDBACK RENDERING ===
glUniform1f(locTime, t); if (current_shader_uses_feedback_) {
// Step 1: Bind feedback texture to iChannel
std::string channelName = "iChannel" + std::to_string(feedback_channel_);
GLint locChannel = glGetUniformLocation(current_program_, channelName.c_str());
if (locChannel >= 0) {
glActiveTexture(GL_TEXTURE0 + feedback_channel_);
glBindTexture(GL_TEXTURE_2D, feedback_texture_);
glUniform1i(locChannel, feedback_channel_);
}
// Step 2: Render to feedback FBO
glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
glViewport(0, 0, w, h);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
if (locTime >= 0) glUniform1f(locTime, t);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
// Step 3: Render to screen (using the same FBO texture)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, w, h);
if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
if (locTime >= 0) glUniform1f(locTime, t);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
// Unbind texture
glActiveTexture(GL_TEXTURE0 + feedback_channel_);
glBindTexture(GL_TEXTURE_2D, 0);
} else {
// === NORMAL RENDERING (no feedback) ===
glViewport(0, 0, w, h);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (locRes >= 0) glUniform2f(locRes, float(w), float(h));
if (locTime >= 0) glUniform1f(locTime, t);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
} }
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
SDL_GL_SwapWindow(window_); SDL_GL_SwapWindow(window_);
SDL_Delay(1); if (!Options_video.vsync) {
SDL_Delay(1); // Prevent CPU spinning when vsync is off
}
} }
// Cleanup // Cleanup
@@ -462,6 +1032,16 @@ int main(int argc, char** argv) {
glDeleteProgram(current_program_); glDeleteProgram(current_program_);
} }
// Cleanup feedback FBO
destroyFeedbackFBO();
// Cleanup audio
if (current_music_) {
JA_DeleteMusic(current_music_);
current_music_ = nullptr;
}
JA_Quit();
SDL_GL_DestroyContext(glContext); SDL_GL_DestroyContext(glContext);
SDL_DestroyWindow(window_); SDL_DestroyWindow(window_);
SDL_Quit(); SDL_Quit();

477
third_party/jail_audio.cpp vendored Normal file
View File

@@ -0,0 +1,477 @@
#ifndef JA_USESDLMIXER
#include "jail_audio.h"
#include <SDL3/SDL.h> // Para SDL_AudioFormat, SDL_BindAudioStream, SDL_SetAudioStreamGain, SDL_PutAudioStreamData, SDL_DestroyAudioStream, SDL_GetAudioStreamAvailable, Uint8, SDL_CreateAudioStream, SDL_UnbindAudioStream, Uint32, SDL_CloseAudioDevice, SDL_GetTicks, SDL_Log, SDL_free, SDL_AudioSpec, SDL_AudioStream, SDL_IOFromMem, SDL_LoadWAV, SDL_LoadWAV_IO, SDL_OpenAudioDevice, SDL_clamp, SDL_malloc, SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, SDL_AudioDeviceID, SDL_memcpy
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#include "stb_vorbis.h" // Para stb_vorbis_decode_memory
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
#define JA_MAX_GROUPS 2
struct JA_Sound_t
{
SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 };
Uint32 length { 0 };
Uint8 *buffer { NULL };
};
struct JA_Channel_t
{
JA_Sound_t *sound { nullptr };
int pos { 0 };
int times { 0 };
int group { 0 };
SDL_AudioStream *stream { nullptr };
JA_Channel_state state { JA_CHANNEL_FREE };
};
struct JA_Music_t
{
SDL_AudioSpec spec { SDL_AUDIO_S16, 2, 48000 };
Uint32 length { 0 };
Uint8 *buffer { nullptr };
char *filename { nullptr };
int pos { 0 };
int times { 0 };
SDL_AudioStream *stream { nullptr };
JA_Music_state state { JA_MUSIC_INVALID };
};
JA_Music_t *current_music { nullptr };
JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
SDL_AudioSpec JA_audioSpec { SDL_AUDIO_S16, 2, 48000 };
float JA_musicVolume { 1.0f };
float JA_soundVolume[JA_MAX_GROUPS];
bool JA_musicEnabled { true };
bool JA_soundEnabled { true };
SDL_AudioDeviceID sdlAudioDevice { 0 };
//SDL_TimerID JA_timerID { 0 };
bool fading = false;
int fade_start_time;
int fade_duration;
int fade_initial_volume;
void JA_Update()
{
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING)
{
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time+fade_duration)) {
fading = false;
JA_StopMusic();
return;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume*(1.0 - percent));
}
}
if (current_music->times != 0)
{
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length/2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times>0) current_music->times--;
}
else
{
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
}
}
if (JA_soundEnabled)
{
for (int i=0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING)
{
if (channels[i].times != 0)
{
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length/2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
if (channels[i].times>0) channels[i].times--;
}
}
else
{
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
return;
}
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels)
{
#ifdef DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq };
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice==0) SDL_Log("Failed to initialize SDL audio!");
for (int i=0; i<JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i=0; i<JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
void JA_Quit()
{
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
JA_Music_t *JA_LoadMusic(Uint8* buffer, Uint32 length)
{
JA_Music_t *music = new JA_Music_t();
int chan, samplerate;
short *output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
music->spec.channels = chan;
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16;
music->buffer = (Uint8*)SDL_malloc(music->length);
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
return music;
}
JA_Music_t *JA_LoadMusic(const char* filename)
{
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
FILE *f = fopen(filename, "rb");
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
Uint8 *buffer = (Uint8*)malloc(fsize + 1);
if (fread(buffer, fsize, 1, f)!=1) return NULL;
fclose(f);
JA_Music_t *music = JA_LoadMusic(buffer, fsize);
music->filename = (char*)malloc(strlen(filename)+1);
strcpy(music->filename, filename);
free(buffer);
return music;
}
void JA_PlayMusic(JA_Music_t *music, const int loop)
{
if (!JA_musicEnabled) return;
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
//SDL_ResumeAudioStreamDevice(current_music->stream);
}
char *JA_GetMusicFilename(JA_Music_t *music)
{
if (!music) music = current_music;
return music->filename;
}
void JA_PauseMusic()
{
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PAUSED;
//SDL_PauseAudioStreamDevice(current_music->stream);
SDL_UnbindAudioStream(current_music->stream);
}
void JA_ResumeMusic()
{
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PLAYING;
//SDL_ResumeAudioStreamDevice(current_music->stream);
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
void JA_StopMusic()
{
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
//SDL_PauseAudioStreamDevice(current_music->stream);
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
free(current_music->filename);
current_music->filename = nullptr;
}
void JA_FadeOutMusic(const int milliseconds)
{
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
JA_Music_state JA_GetMusicState()
{
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
void JA_DeleteMusic(JA_Music_t *music)
{
if (current_music == music) current_music = nullptr;
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
delete music;
}
float JA_SetMusicVolume(float volume)
{
JA_musicVolume = SDL_clamp( volume, 0.0f, 1.0f );
if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
return JA_musicVolume;
}
void JA_SetMusicPosition(float value)
{
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
}
float JA_GetMusicPosition()
{
if (!current_music) return 0;
return float(current_music->pos)/float(current_music->spec.freq);
}
void JA_EnableMusic(const bool value)
{
if ( !value && current_music && (current_music->state==JA_MUSIC_PLAYING) ) JA_StopMusic();
JA_musicEnabled = value;
}
JA_Sound_t *JA_NewSound(Uint8* buffer, Uint32 length)
{
JA_Sound_t *sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
return sound;
}
JA_Sound_t *JA_LoadSound(uint8_t* buffer, uint32_t size)
{
JA_Sound_t *sound = new JA_Sound_t();
SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size),1, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
JA_Sound_t *JA_LoadSound(const char* filename)
{
JA_Sound_t *sound = new JA_Sound_t();
SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
int JA_PlaySound(JA_Sound_t *sound, const int loop, const int group)
{
if (!JA_soundEnabled) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
int JA_PlaySoundOnChannel(JA_Sound_t *sound, const int channel, const int loop, const int group)
{
if (!JA_soundEnabled) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
void JA_DeleteSound(JA_Sound_t *sound)
{
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
void JA_PauseChannel(const int channel)
{
if (!JA_soundEnabled) return;
if (channel == -1)
{
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING)
{
channels[i].state = JA_CHANNEL_PAUSED;
//SDL_PauseAudioStreamDevice(channels[i].stream);
SDL_UnbindAudioStream(channels[i].stream);
}
}
else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS)
{
if (channels[channel].state == JA_CHANNEL_PLAYING)
{
channels[channel].state = JA_CHANNEL_PAUSED;
//SDL_PauseAudioStreamDevice(channels[channel].stream);
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
void JA_ResumeChannel(const int channel)
{
if (!JA_soundEnabled) return;
if (channel == -1)
{
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED)
{
channels[i].state = JA_CHANNEL_PLAYING;
//SDL_ResumeAudioStreamDevice(channels[i].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
}
else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS)
{
if (channels[channel].state == JA_CHANNEL_PAUSED)
{
channels[channel].state = JA_CHANNEL_PLAYING;
//SDL_ResumeAudioStreamDevice(channels[channel].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
void JA_StopChannel(const int channel)
{
if (!JA_soundEnabled) return;
if (channel == -1)
{
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
}
else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS)
{
if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
JA_Channel_state JA_GetChannelState(const int channel)
{
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
float JA_SetSoundVolume(float volume, const int group)
{
const float v = SDL_clamp( volume, 0.0f, 1.0f );
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
if (group==-1 || group==i) JA_soundVolume[i]=v;
}
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if ( ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) &&
((group==-1) || (channels[i].group==group)) )
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[i]);
return v;
}
void JA_EnableSound(const bool value)
{
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
{
if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i);
}
JA_soundEnabled = value;
}
float JA_SetVolume(float volume)
{
JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f);
return JA_musicVolume;
}
#endif

43
third_party/jail_audio.h vendored Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <SDL3/SDL.h>
enum JA_Channel_state { JA_CHANNEL_INVALID, JA_CHANNEL_FREE, JA_CHANNEL_PLAYING, JA_CHANNEL_PAUSED, JA_SOUND_DISABLED };
enum JA_Music_state { JA_MUSIC_INVALID, JA_MUSIC_PLAYING, JA_MUSIC_PAUSED, JA_MUSIC_STOPPED, JA_MUSIC_DISABLED };
struct JA_Sound_t;
struct JA_Music_t;
void JA_Update();
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels);
void JA_Quit();
JA_Music_t *JA_LoadMusic(const char* filename);
JA_Music_t *JA_LoadMusic(Uint8* buffer, Uint32 length);
void JA_PlayMusic(JA_Music_t *music, const int loop = -1);
char *JA_GetMusicFilename(JA_Music_t *music = nullptr);
void JA_PauseMusic();
void JA_ResumeMusic();
void JA_StopMusic();
void JA_FadeOutMusic(const int milliseconds);
JA_Music_state JA_GetMusicState();
void JA_DeleteMusic(JA_Music_t *music);
float JA_SetMusicVolume(float volume);
void JA_SetMusicPosition(float value);
float JA_GetMusicPosition();
void JA_EnableMusic(const bool value);
JA_Sound_t *JA_NewSound(Uint8* buffer, Uint32 length);
JA_Sound_t *JA_LoadSound(Uint8* buffer, Uint32 length);
JA_Sound_t *JA_LoadSound(const char* filename);
int JA_PlaySound(JA_Sound_t *sound, const int loop = 0, const int group=0);
int JA_PlaySoundOnChannel(JA_Sound_t *sound, const int channel, const int loop = 0, const int group=0);
void JA_PauseChannel(const int channel);
void JA_ResumeChannel(const int channel);
void JA_StopChannel(const int channel);
JA_Channel_state JA_GetChannelState(const int channel);
void JA_DeleteSound(JA_Sound_t *sound);
float JA_SetSoundVolume(float volume, const int group=0);
void JA_EnableSound(const bool value);
float JA_SetVolume(float volume);

5631
third_party/stb_vorbis.h vendored Normal file

File diff suppressed because it is too large Load Diff