Author:ptrace
Comitter:ptrace
Date:2026-08-28 19:46:42 UTC
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ebd21e3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
.build/
bin/
/shaderview
/shaderview_release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d3e0560
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
# Usage
```
shaderview <path to fragment shader> [options]
```
Options:
```
-st Pass it, when you're using a shader from Shadertoy.com
```
## Example
```
./shaderview shaders/shader3.frag -st
```
## Hot Reloading
It automagically reloads the shaders as soon you modified it.
Or you can just press `R`.
# Build
You need:
```
$ jai -version
Version: beta 0.2.030, built on 2 July 2026.
```
and a directory named `./modules` which contains a symlink or a copy of the
[Raylib library](https://git.ptrace.dev/public/raylib-jai.git/)
Build via:
```
chmod +x shaderview.jai && ./shaderview.jai
```
or
```
jai shaderview.jai -optimized
```
# Credits
- shader2.frag from [kishimisu](https://www.shadertoy.com/view/mtyGWy)
- shader3.frag from [Gaijin Entertainment](https://www.shadertoy.com/view/sctXDn)
# License
Do whatever you want with this program.
I'm not liable for any damages this program might have caused.
You're using this program at your own risk.
This license does not include the fragment shaders in `./shaders/*.frag`.
diff --git a/desc.gt b/desc.gt
new file mode 100644
index 0000000..62b4685
--- /dev/null
+++ b/desc.gt
@@ -0,0 +1 @@
Low effort shader viewer
diff --git a/modules/raylib b/modules/raylib
new file mode 120000
index 0000000..9a2d4c3
--- /dev/null
+++ b/modules/raylib
@@ -0,0 +1 @@
/home/ptrace/dev/private/raylib-jai/Raylib
\ No newline at end of file
diff --git a/shaders/shader.frag b/shaders/shader.frag
new file mode 100644
index 0000000..224ef2d
--- /dev/null
+++ b/shaders/shader.frag
@@ -0,0 +1,46 @@
#define DO_RED
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
void main() {
vec2 uv = (fragTexCoord * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);
vec2 m = (iMouse / iResolution - 0.5) * 2.0 * vec2(iResolution.x / iResolution.y, 1.0);
m.y = -m.y; // Raylib
float t = iTime * 0.8;
/* rotating box */
float angle = t;
vec2 rot = vec2(cos(angle) * uv.x - sin(angle) * uv.y,
sin(angle) * uv.x + cos(angle) * uv.y);
float box = sdBox(rot, vec2(0.3));
/* pulsing circle that follows mouse */
float pulse = 0.05 + 0.02 * sin(iTime * 3.0);
float circle = sdCircle(uv - m, pulse);
/* combine */
float d = min(box, circle);
/* coloring */
#ifdef DO_RED
vec3 col = mix(vec3(0.05), vec3(1.0, 0.2, 0.1), 1.0 - smoothstep(0.0, 0.01, d));
col += vec3(0.1, 0.4, 1.0) * (1.0 - smoothstep(0.0, 0.01, abs(d) - 0.005));
col += min(0.03 / abs(d), 1.0) * vec3(1.0, 0.3, 0.2);
#else
vec3 col = mix(vec3(0.05), vec3(0.2, 0.6, 1.0), 1.0 - smoothstep(0.0, 0.01, d));
col += vec3(1.0, 0.4, 0.1) * (1.0 - smoothstep(0.0, 0.01, abs(d) - 0.005));
col += min(0.03 / abs(d), 1.0) * vec3(0.3, 0.6, 1.0);
#endif
finalColor = vec4(col, 1.0);
}
diff --git a/shaders/shader2.frag b/shaders/shader2.frag
new file mode 100644
index 0000000..897d7c4
--- /dev/null
+++ b/shaders/shader2.frag
@@ -0,0 +1,41 @@
/* This animation is the material of my first youtube tutorial about creative
coding, which is a video in which I try to introduce programmers to GLSL
and to the wonderful world of shaders, while also trying to share my recent
passion for this community.
Video URL: https://youtu.be/f4s1h2YETNY
*/
//https://iquilezles.org/articles/palettes/
vec3 palette( float t ) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.263,0.416,0.557);
return a + b*cos( 6.28318*(c*t+d) );
}
//https://www.shadertoy.com/view/mtyGWy
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
vec2 uv0 = uv;
vec3 finalColor = vec3(0.0);
for (float i = 0.0; i < 4.0; i++) {
uv = fract(uv * 1.5) - 0.5;
float d = length(uv) * exp(-length(uv0));
vec3 col = palette(length(uv0) + i*.4 + iTime*.4);
d = sin(d*8. + iTime)/8.;
d = abs(d);
d = pow(0.01 / d, 1.2);
finalColor += col * d;
}
fragColor = vec4(finalColor, 1.0);
}
diff --git a/shaders/shader3.frag b/shaders/shader3.frag
new file mode 100644
index 0000000..6846af7
--- /dev/null
+++ b/shaders/shader3.frag
@@ -0,0 +1,110 @@
// Copyright © 2026 Gaijin Entertainment
//
// Free to use, copy, modify and share, including for personal use and monetized streams, videos and other online content.
//
// The shader itself (including modified versions) may not be sold, sublicensed or commercially distributed as a standalone product.
//
// No rights to Gaijin trademarks, game titles, artwork or other IP are granted. Provided "as is", without warranty.
const float SEED = 0.0;
const float PI = 3.14159265358979;
vec4 kal_march(vec3 rayDir, mat2 worldRot, float marchTime)
{
vec4 accumulated = vec4(0.0, 0.0, 0.0, 0.0);
vec3 rayPos = vec3(0.0, 0.0, 0.0);
float rayDist = 0.0, stepSize = 0.0;
for (float i = 1.0; i <= 30.0; i += 1.0)
{
rayPos = rayDir * rayDist;
rayPos.z -= 10.0;
vec2 rotatedXZ = worldRot * vec2(rayPos.x, rayPos.z);
rayPos.x = rotatedXZ.x; rayPos.z = rotatedXZ.y;
for (stepSize = 0.04; stepSize < 3.0; stepSize += stepSize)
{
rayPos += cos(2.0 * marchTime + rayPos.yzx / 10.0) * 0.6;
rayPos -= abs(dot(sin(0.03 * rayPos.z + marchTime + rayPos / stepSize / 3.2), vec3(stepSize, stepSize, stepSize)));
}
rayPos.xy /= 4.0;
stepSize = 0.08 + 0.5 * abs(length(rayPos) - 30.0);
rayDist += stepSize;
accumulated += vec4(3.9, 2.0, 1.0, 0.0) / stepSize * rayDist + 10.0 * (1.0 + cos(i * 0.4 + vec4(2.0, 1.0, 0.0, 0.0))) / stepSize;
}
return accumulated;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 pos = vec2(fragCoord.x, iResolution.y - fragCoord.y);
vec2 resolution = iResolution.xy;
float time = iTime;
vec2 uv = (pos.xy * 2.0 - resolution) / resolution.y;
float stepDuration = 4.0;
float firstStepDuration = 15.0;
float animDuration = 0.5;
float rotPerStep = PI / 3.0;
bool inFirstStep = time < firstStepDuration;
float stepIndex = inFirstStep ? 0.0 : (1.0 + floor((time - firstStepDuration) / stepDuration));
float localTime = inFirstStep ? time : fract((time - firstStepDuration) / stepDuration) * stepDuration;
float currentStepDur = inFirstStep ? firstStepDuration : stepDuration;
float animProgress = clamp((localTime - (currentStepDur - animDuration)) / animDuration, 0.0, 1.0);
float easedProgress = animProgress * animProgress * animProgress;
float rotationPhase = sin(animProgress * PI);
float mirrorAngle = mix(stepIndex * rotPerStep, (stepIndex + 1.0) * rotPerStep, easedProgress);
float segments = 3.0;
float radius = length(uv);
float period = 2.0 * PI / segments;
float foldAngle = atan(uv.y, uv.x) + PI * 0.5 + mirrorAngle;
foldAngle = mod(foldAngle, period);
foldAngle = abs(foldAngle - period * 0.5);
uv = radius * vec2(cos(foldAngle - mirrorAngle * 2.0), sin(foldAngle - mirrorAngle * 2.0));
uv *= mix(1.0, 1.25, rotationPhase);
float slowTime = time / 20.0 + SEED;
vec4 rotCos = cos(slowTime + vec4(0.0, 33.0, 11.0, 0.0));
mat2 worldRot = mat2(rotCos.x, rotCos.y, rotCos.z, rotCos.w);
float brightness = 1.5;
float flashDuration = 0.4;
float flashFade = 1.0 - smoothstep(0.0, flashDuration, localTime);
float aberration = mix(0.003, 0.02, flashFade);
float marchTime = time / 4.0 + SEED;
float cloudScale = 0.45;
float fov = 0.8;
vec2 uvScaled = uv * cloudScale;
vec4 sampleR = kal_march(normalize(vec3(uvScaled + vec2(0.0, aberration), fov)), worldRot, marchTime);
vec4 sampleGB = kal_march(normalize(vec3(uvScaled, fov)), worldRot, marchTime);
vec4 color = vec4(sampleR.r, sampleGB.g, sampleGB.b, 1.0);
vec3 edgeHue = vec3(0.0, 1.0, 0.65);
float edgeStrength = 0.7;
float edgeBlend = clamp(smoothstep(0.0, 0.6, length(uv) / 2.0) * edgeStrength, 0.0, 1.0);
vec4 edgeColor = vec4(color.b * edgeHue.r, color.g * edgeHue.g, color.r * edgeHue.b, 1.0);
color = mix(color, edgeColor, edgeBlend);
color = tanh(color * color / 9e8);
vec3 shadowLift = vec3(0.02, 0.01, 0.0);
color.rgb = color.rgb + shadowLift * (1.0 - color.rgb);
float startFade = smoothstep(0.0, 3.0, time);
float vigInnerRadius = mix(0.4, 0.1, rotationPhase);
float vigOuterRadius = mix(1.8, 1.3, rotationPhase);
float vignetteMask = 1.0 - smoothstep(vigInnerRadius, vigOuterRadius, radius);
color *= vignetteMask;
color *= brightness;
color *= 1.0 + flashFade;
color *= startFade;
vec3 radiance = color.rgb;
vec4 load = vec4(radiance, 1.0);
fragColor = load;
}
diff --git a/shaderview.jai b/shaderview.jai
new file mode 100755
index 0000000..08a1aa3
--- /dev/null
+++ b/shaderview.jai
@@ -0,0 +1,252 @@
#!/usr/bin/env -S jai -quiet -optimized shaderview.jai -
shader_thing: Shader_Thing;
shader_path: string;
last_mtime: Apollo_Time;
last_size: s64;
elapsed: float;
frame_time: float;
revision: int;
args_is_shadertoy: bool;
SCREEN_X :s32: 1280;
SCREEN_Y :s32: 720;
TITLE :: "shader viewer";
SHADERTOY_COMPAT :: #string STR_END
#version 330
in vec2 fragTexCoord;
out vec4 finalColor;
uniform vec2 iResolution;
uniform float iTime;
uniform vec2 iMouse;
STR_END;
SHADERTOY_MAIN :: #string STR_END
void main() {
mainImage(finalColor, fragTexCoord * iResolution);
}
STR_END;
ARGS_HELP :: #string STR_END
Usage: shaderview <path to fragment shader> [options]
Options:
-st Pass it, when you're using a shader from Shadertoy.com
STR_END;
Shader_Load_Kind :: enum_flags {
NONE;
PATH;
CODE;
SHADERTOY;
}
Shader_Thing :: struct {
using shader: Shader;
resolution: s32;
time: s32;
mouse: s32;
}
main :: () {
args := get_command_line_arguments();
if args.count == 1 {
log_error("No arguments passed. Need file path to fragment shader.");
log_error("`./shaderview <path to shader>`");
exit(1);
}
for string.[ "help", "h", "-help", "-h", "--help" ] {
if array_find(args, it) {
log(ARGS_HELP);
exit(0);
}
}
shader_path = args[1];
if !file_exists(shader_path) {
log_error("Cringe bro. Path does not exist: '%'", shader_path);
exit(1);
}
args_is_shadertoy = array_find(args, "-st");
if args_is_shadertoy then log("Loaded Shadertoy flavor");
SetConfigFlags(.WINDOW_RESIZABLE | .VSYNC_HINT);
SetTraceLogLevel(.WARNING);
InitWindow(SCREEN_X, SCREEN_Y, TITLE);
SetTargetFPS(60);
shader_ok := load_shader(*shader_thing, shader_path, .PATH);
target := LoadRenderTexture(SCREEN_X, SCREEN_Y);
while !WindowShouldClose() {
push_allocator(temp);
reset_temporary_storage();
frame_time = GetFrameTime();
elapsed += frame_time;
if IsKeyPressed(.R) || has_file_changed(shader_path) {
kind := Shader_Load_Kind.PATH;
if args_is_shadertoy then kind |= .SHADERTOY;
shader_ok = load_shader(*shader_thing, shader_path, kind);
elapsed = 0.0;
if shader_ok then revision += 1;
}
if shader_ok then update_uniforms(*shader_thing);
if IsWindowResized() {
UnloadRenderTexture(target);
target = LoadRenderTexture(SCREEN_X, SCREEN_Y);
}
{
BeginDrawing();
defer EndDrawing();
ClearBackground(BLACK);
if shader_ok {
BeginShaderMode(shader_thing.shader);
DrawTextureRec(
target.texture,
{ 0, 0, cast(float, GetRenderWidth()), cast(float, -GetRenderHeight()) },
{ 0, 0 },
WHITE
);
EndShaderMode();
}
else {
ClearBackground({ 20, 20, 20, 255 });
draw_text_in_center("Shader compile error", 30, RED);
draw_text_in_center("check console logs", 20, GRAY, { 0, 40 });
}
DrawFPS(8, 8);
path_c := temp_c_string(shader_path);
tw := MeasureText(path_c, 14);
DrawText(
path_c,
GetScreenWidth() - tw - 8, 8, 14,
{ 200, 200, 200, 180 }
);
rev := tprint("Revision Count: %", revision);
crev := temp_c_string(rev);
DrawText(crev, 8, GetScreenHeight() - 22, 13, { 160, 160, 160, 180 });
}
}
UnloadShader(shader_thing.shader);
UnloadRenderTexture(target);
CloseWindow();
}
draw_text_in_center :: (
text: string,
size: s32,
color: Color,
offset: struct { x, y: s32; } = {}
) {
mid_x := GetScreenWidth() / 2;
mid_y := GetScreenHeight() / 2;
mid_x += offset.x;
mid_y += offset.y;
ctext := temp_c_string(text);
mt := MeasureText(ctext, size);
DrawText(ctext, mid_x - (mt / 2), mid_y, size, color);
}
has_file_changed :: (path: string) -> bool {
mtime, size := file_modtime_and_size(path);
if mtime != last_mtime || size != last_size {
last_mtime = mtime;
last_size = size;
return true;
}
return false;
}
load_shader :: (
using sh: *Shader_Thing,
path_or_code: string,
kind: Shader_Load_Kind
)
-> ok: bool
{
assert(!(!kind || kind & .NONE), "cannot be `NONE`");
buf: String_Builder;
append(*buf, SHADERTOY_COMPAT);
if kind & .PATH {
if !file_exists(path_or_code) return false;
raw, ok := read_entire_file(path_or_code);
if !ok {
log_error("Could not read '%'", path_or_code);
return false;
}
append(*buf, raw);
}
else if kind & .CODE {
append(*buf, path_or_code);
}
if kind & .SHADERTOY {
append(*buf, SHADERTOY_MAIN);
}
if id > 0 then UnloadShader(shader);
code := builder_to_string(*buf);
poc := to_c_string(code);
shader = LoadShaderFromMemory(null, poc);
resolution = GetShaderLocation(shader, "iResolution");
time = GetShaderLocation(shader, "iTime");
mouse = GetShaderLocation(shader, "iMouse");
return id > 0;
}
update_uniforms :: (using sh: *Shader_Thing) {
res: [2]float;
res[0] = cast(float) GetScreenWidth();
res[1] = cast(float) GetScreenHeight();
t := elapsed;
mp := GetMousePosition();
mouse_pos: [2]float;
mouse_pos[0] = mp.x;
mouse_pos[1] = mp.y;
SetShaderValue(shader, resolution, res.data, .VEC2);
SetShaderValue(shader, time, *t, .FLOAT);
SetShaderValue(shader, mouse, mouse_pos.data, .VEC2);
}
#import "Basic";
#import "Print_Vars";
#import "File";
#import "File_Utilities";
#import "raylib";