forked from mirrors/zig-glsl-view
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
8.8 KiB
Zig
264 lines
8.8 KiB
Zig
const std = @import("std");
|
|
const fs = @import("std").fs;
|
|
const panic = std.debug.panic;
|
|
const c = @import("c.zig");
|
|
const debug_gl = @import("debug_gl.zig");
|
|
const cfg = @import("config.zig");
|
|
const out = @import("output.zig");
|
|
const gl = @import("gl.zig");
|
|
const ctrl = @import("control.zig");
|
|
const c_allocator = @import("std").heap.c_allocator;
|
|
|
|
var window: *c.GLFWwindow = undefined;
|
|
|
|
fn errorCallback(err: c_int, description: [*c]const u8) callconv(.C) void {
|
|
panic("Error {}: {s}\n", .{ err, description });
|
|
}
|
|
|
|
pub fn main() !void {
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer arena.deinit();
|
|
|
|
var config = try cfg.Config.parse(arena.allocator(), "config.yaml");
|
|
|
|
_ = c.glfwSetErrorCallback(errorCallback);
|
|
|
|
if (c.glfwInit() == c.GL_FALSE) {
|
|
panic("GLFW init failure\n", .{});
|
|
}
|
|
defer c.glfwTerminate();
|
|
|
|
var monitor_count: c_int = 0;
|
|
const monitors = c.glfwGetMonitors(&monitor_count);
|
|
for (monitors[0..@intCast(usize, monitor_count)]) |monitor, i| {
|
|
std.debug.print("monitor {}: '{s}'\n", .{ i, @ptrCast([*:0]const u8, c.glfwGetMonitorName(monitor)) });
|
|
}
|
|
|
|
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MAJOR, 4);
|
|
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 2);
|
|
c.glfwWindowHint(c.GLFW_OPENGL_FORWARD_COMPAT, c.GL_TRUE);
|
|
c.glfwWindowHint(c.GLFW_OPENGL_DEBUG_CONTEXT, debug_gl.is_on);
|
|
c.glfwWindowHint(c.GLFW_OPENGL_PROFILE, c.GLFW_OPENGL_CORE_PROFILE);
|
|
c.glfwWindowHint(c.GLFW_DEPTH_BITS, 0);
|
|
c.glfwWindowHint(c.GLFW_STENCIL_BITS, 0);
|
|
c.glfwWindowHint(c.GLFW_VISIBLE, c.GLFW_FALSE);
|
|
|
|
// master window, shared with WindowOutputs used for rendering
|
|
window = c.glfwCreateWindow(config.width, config.height, "glsl-view", null, null) orelse {
|
|
panic("unable to create window\n", .{});
|
|
};
|
|
defer c.glfwDestroyWindow(window);
|
|
|
|
var constants = try gl.Constants.create(window, &config);
|
|
defer constants.destroy();
|
|
|
|
var outputs = try std.ArrayList(*out.Output).initCapacity(arena.allocator(), config.outputs.len);
|
|
defer outputs.deinit();
|
|
for (config.outputs) |output_config| {
|
|
try outputs.append(out.Output.create(arena.allocator(), output_config, &constants));
|
|
}
|
|
defer for (outputs.items) |output| {
|
|
output.destroy(output);
|
|
};
|
|
|
|
c.glfwMakeContextCurrent(window);
|
|
c.glfwSwapInterval(1);
|
|
c.glClearColor(0.0, 0.0, 0.0, 1.0);
|
|
|
|
debug_gl.assertNoError();
|
|
debug_gl.init();
|
|
|
|
constants.normalized_quad.bind(0);
|
|
|
|
// main_program is the user fragment shader, rendered to a FrameBufferObject.
|
|
// From this fbo, the texture is rendered to one or more Outputs.
|
|
var main_program = try gl.ShaderProgram.create(
|
|
\\#version 330 core
|
|
\\layout(location = 0) in vec2 position;
|
|
\\out vec2 uv;
|
|
\\void main() {
|
|
\\ uv = position / 2.0 + 0.5;
|
|
\\ gl_Position = vec4(position, 0, 1);
|
|
\\}
|
|
,
|
|
\\#version 330 core
|
|
\\in vec2 uv;
|
|
\\out vec4 color;
|
|
\\void main() { color = vec4(0,0,0,1); }
|
|
);
|
|
defer main_program.destroy();
|
|
|
|
var fbo = try gl.FramebufferObject.create(config.width, config.height);
|
|
defer fbo.destroy();
|
|
|
|
const shader_file = try fs.cwd().openFile(config.fragment, .{});
|
|
defer shader_file.close();
|
|
var last_stat = try shader_file.stat();
|
|
try reloadShader(&main_program, config.fragment, last_stat.size);
|
|
|
|
var cache = gl.UniformCache.init(std.heap.c_allocator, &main_program);
|
|
defer cache.deinit();
|
|
|
|
const control = try ctrl.ControlServer.init(arena.allocator(), config.osc, &cache);
|
|
defer control.destroy();
|
|
|
|
const shadervars = &ShadertoyContext.init(constants, &cache);
|
|
|
|
while (c.glfwWindowShouldClose(window) == c.GL_FALSE) {
|
|
c.glfwMakeContextCurrent(window);
|
|
c.glClear(c.GL_COLOR_BUFFER_BIT);
|
|
|
|
// TODO: instead of statting, we could use std.fs.Watch() (once it's more stable)
|
|
const stat = try shader_file.stat();
|
|
if (stat.mtime > last_stat.mtime) {
|
|
try reloadShader(&main_program, config.fragment, stat.size);
|
|
try cache.refresh();
|
|
last_stat = stat;
|
|
}
|
|
|
|
control.update();
|
|
|
|
fbo.bind();
|
|
c.glClear(c.GL_COLOR_BUFFER_BIT);
|
|
|
|
main_program.bind();
|
|
shadervars.update(main_program); // apply default uniforms
|
|
constants.normalized_quad.draw();
|
|
fbo.unbind();
|
|
|
|
for (outputs.items) |output, i| {
|
|
const close = output.update(output, fbo.texture_id);
|
|
if (close) {
|
|
const removed = outputs.swapRemove(i);
|
|
removed.destroy(removed);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (outputs.items.len == 0)
|
|
break;
|
|
|
|
c.glfwPollEvents();
|
|
}
|
|
}
|
|
|
|
// given a file and the directory it is in, resolve references
|
|
fn loadFile(writer: anytype, dir: []const u8, filename: []const u8) !void {
|
|
const file_dir = try std.fs.path.resolve(c_allocator, &[_][]const u8{
|
|
dir,
|
|
std.fs.path.dirname(filename) orelse "",
|
|
});
|
|
defer c_allocator.free(file_dir);
|
|
|
|
const file_path = try std.fs.path.resolve(c_allocator, &[_][]const u8{ dir, filename });
|
|
defer c_allocator.free(file_path);
|
|
|
|
var file = try fs.openFileAbsolute(file_path, .{});
|
|
defer file.close();
|
|
var reader = file.reader();
|
|
|
|
var buf: [1024]u8 = undefined;
|
|
while (try reader.readUntilDelimiterOrEof(&buf, '\n')) |line| {
|
|
if (std.mem.startsWith(u8, line, "#pragma ")) {
|
|
var parts = std.mem.split(u8, line, " ");
|
|
_ = parts.next();
|
|
const pragma = parts.next().?;
|
|
|
|
if (std.mem.eql(u8, pragma, "include")) {
|
|
var include_path = parts.next().?;
|
|
if (include_path[0] != '"' or include_path[include_path.len - 1] != '"') {
|
|
std.debug.print("Invalid #pragma directive '{s}'\n", .{line});
|
|
continue;
|
|
}
|
|
include_path = include_path[1 .. include_path.len - 1];
|
|
|
|
loadFile(writer, file_dir, include_path) catch unreachable;
|
|
|
|
continue;
|
|
} else {
|
|
std.debug.print("Unknown #pragma directive '{s}'\n", .{pragma});
|
|
}
|
|
}
|
|
|
|
_ = try writer.write(line);
|
|
_ = try writer.write("\n");
|
|
}
|
|
}
|
|
|
|
fn reloadShader(current: *gl.ShaderProgram, frag_filename: []const u8, size: u64) !void {
|
|
var buffer = try std.ArrayList(u8).initCapacity(c_allocator, @intCast(usize, size));
|
|
errdefer buffer.deinit();
|
|
|
|
try loadFile(buffer.writer(), "", frag_filename);
|
|
|
|
const frag_source = buffer.toOwnedSlice();
|
|
defer c_allocator.free(frag_source);
|
|
|
|
const vert_source =
|
|
\\#version 330 core
|
|
\\
|
|
\\layout(location = 0) in vec2 position;
|
|
\\out vec2 uv;
|
|
\\
|
|
\\void main() {
|
|
\\ uv = position / 2.0 + 0.5;
|
|
\\ gl_Position = vec4(position, 0, 1);
|
|
\\}
|
|
;
|
|
|
|
if (gl.ShaderProgram.create(vert_source, frag_source)) |new_program| {
|
|
current.destroy();
|
|
current.* = new_program;
|
|
} else |err| {
|
|
std.debug.print("Error while reloading shader: {}\n", .{err});
|
|
}
|
|
}
|
|
|
|
// utility to set default uniforms on a shader (a subset of what shadertoy.com provides)
|
|
pub const ShadertoyContext = struct {
|
|
cache: *gl.UniformCache,
|
|
iResolution: [2]f32,
|
|
prev_time: ?f32 = null,
|
|
|
|
pub fn init(constants: gl.Constants, cache: *gl.UniformCache) ShadertoyContext {
|
|
return ShadertoyContext{
|
|
.cache = cache,
|
|
.iResolution = .{
|
|
@intToFloat(f32, constants.config.width),
|
|
@intToFloat(f32, constants.config.height),
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn update(self: *ShadertoyContext, shader: gl.ShaderProgram) void {
|
|
// using glfw timer, as std.time.Timer bugs around..?!
|
|
const iTime = @floatCast(f32, c.glfwGetTime());
|
|
if (self.prev_time) |t| {
|
|
const iTimeDelta = iTime - t;
|
|
if (self.cache.get("iTimeDelta") catch null) |uniform| {
|
|
switch (uniform.value) {
|
|
.FLOAT => |*v| v.* = iTimeDelta,
|
|
else => std.debug.print("uniform iTimeDelta should be FLOAT\n", .{}),
|
|
}
|
|
uniform.setShaderValue(shader);
|
|
}
|
|
}
|
|
self.prev_time = iTime;
|
|
if (self.cache.get("iTime") catch null) |uniform| {
|
|
switch (uniform.value) {
|
|
.FLOAT => |*v| v.* = iTime,
|
|
else => std.debug.print("uniform iTime should be FLOAT\n", .{}),
|
|
}
|
|
uniform.setShaderValue(shader);
|
|
}
|
|
|
|
if (self.cache.get("iResolution") catch null) |uniform| {
|
|
switch (uniform.value) {
|
|
.FLOAT_VEC2 => |*v| v.* = self.iResolution,
|
|
else => std.debug.print("uniform iResolution should be FLOAT_VEC2\n", .{}),
|
|
}
|
|
uniform.setShaderValue(shader);
|
|
}
|
|
}
|
|
};
|