Cubyz/src/graphics.zig
2025-05-22 20:52:34 +02:00

2647 lines
89 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// A collection of things that should make dealing with opengl easier.
/// Also contains some basic 2d drawing stuff.
const std = @import("std");
pub const hbft = @cImport({
@cInclude("freetype/ftadvanc.h");
@cInclude("freetype/ftbbox.h");
@cInclude("freetype/ftbitmap.h");
@cInclude("freetype/ftcolor.h");
@cInclude("freetype/ftlcdfil.h");
@cInclude("freetype/ftsizes.h");
@cInclude("freetype/ftstroke.h");
@cInclude("freetype/fttrigon.h");
@cInclude("freetype/ftsynth.h");
@cInclude("hb.h");
@cInclude("hb-ft.h");
});
const vec = @import("vec.zig");
const Mat4f = vec.Mat4f;
const Vec4i = vec.Vec4i;
const Vec4f = vec.Vec4f;
const Vec2f = vec.Vec2f;
const Vec2i = vec.Vec2i;
const Vec3f = vec.Vec3f;
const main = @import("main");
const Window = main.Window;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
pub const c = @cImport({
@cInclude("glad/glad.h");
@cInclude("vulkan/vulkan.h");
});
pub const stb_image = @cImport({
@cInclude("stb/stb_image.h");
@cInclude("stb/stb_image_write.h");
});
const glslang = @cImport({
@cInclude("glslang/Include/glslang_c_interface.h");
@cInclude("glslang/Public/resource_limits_c.h");
});
pub const draw = struct { // MARK: draw
var color: u32 = 0;
var clip: ?Vec4i = null;
var translation: Vec2f = Vec2f{0, 0};
var scale: f32 = 1;
pub fn setColor(newColor: u32) void {
color = newColor;
}
/// Returns the previous translation.
pub fn setTranslation(newTranslation: Vec2f) Vec2f {
const oldTranslation = translation;
translation += newTranslation*@as(Vec2f, @splat(scale));
return oldTranslation;
}
pub fn restoreTranslation(previousTranslation: Vec2f) void {
translation = previousTranslation;
}
/// Returns the previous scale.
pub fn setScale(newScale: f32) f32 {
std.debug.assert(newScale >= 0);
const oldScale = scale;
scale *= newScale;
return oldScale;
}
pub fn restoreScale(previousScale: f32) void {
scale = previousScale;
}
/// Returns the previous clip.
pub fn setClip(clipRect: Vec2f) ?Vec4i {
std.debug.assert(@reduce(.And, clipRect >= Vec2f{0, 0}));
var newClip = Vec4i{
std.math.lossyCast(i32, translation[0]),
main.Window.height - std.math.lossyCast(i32, translation[1] + clipRect[1]*scale),
std.math.lossyCast(i32, clipRect[0]*scale),
std.math.lossyCast(i32, clipRect[1]*scale),
};
if(clip) |oldClip| {
if(newClip[0] < oldClip[0]) {
newClip[2] -= oldClip[0] - newClip[0];
newClip[0] += oldClip[0] - newClip[0];
}
if(newClip[1] < oldClip[1]) {
newClip[3] -= oldClip[1] - newClip[1];
newClip[1] += oldClip[1] - newClip[1];
}
if(newClip[0] + newClip[2] > oldClip[0] + oldClip[2]) {
newClip[2] -= (newClip[0] + newClip[2]) - (oldClip[0] + oldClip[2]);
}
if(newClip[1] + newClip[3] > oldClip[1] + oldClip[3]) {
newClip[3] -= (newClip[1] + newClip[3]) - (oldClip[1] + oldClip[3]);
}
newClip[2] = @max(newClip[2], 0);
newClip[3] = @max(newClip[3], 0);
}
const oldClip = clip;
clip = newClip;
return oldClip;
}
pub fn getScissor() ?c.VkRect2D {
const clipRect = clip orelse return null;
return .{
.offset = .{
.x = clipRect[0],
.y = clipRect[1],
},
.extent = .{
.width = @intCast(clipRect[2]),
.height = @intCast(clipRect[3]),
},
};
}
/// Should be used to restore the old clip when leaving the render function.
pub fn restoreClip(previousClip: ?Vec4i) void {
clip = previousClip;
}
// ----------------------------------------------------------------------------
// MARK: fillRect()
var rectUniforms: struct {
screen: c_int,
start: c_int,
size: c_int,
rectColor: c_int,
} = undefined;
var rectPipeline: Pipeline = undefined;
pub var rectVAO: c_uint = undefined;
var rectVBO: c_uint = undefined;
fn initRect() void {
rectPipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/Rect.vert",
"assets/cubyz/shaders/graphics/Rect.frag",
"",
&rectUniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
const rawData = [_]f32{
0, 0,
0, 1,
1, 0,
1, 1,
};
c.glGenVertexArrays(1, &rectVAO);
c.glBindVertexArray(rectVAO);
c.glGenBuffers(1, &rectVBO);
c.glBindBuffer(c.GL_ARRAY_BUFFER, rectVBO);
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null);
c.glEnableVertexAttribArray(0);
}
fn deinitRect() void {
rectPipeline.deinit();
c.glDeleteVertexArrays(1, &rectVAO);
c.glDeleteBuffers(1, &rectVBO);
}
pub fn rect(_pos: Vec2f, _dim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
rectPipeline.bind(getScissor());
c.glUniform2f(rectUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(rectUniforms.start, pos[0], pos[1]);
c.glUniform2f(rectUniforms.size, dim[0], dim[1]);
c.glUniform1i(rectUniforms.rectColor, @bitCast(color));
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
// ----------------------------------------------------------------------------
// MARK: fillRectBorder()
var rectBorderUniforms: struct {
screen: c_int,
start: c_int,
size: c_int,
rectColor: c_int,
lineWidth: c_int,
} = undefined;
var rectBorderPipeline: Pipeline = undefined;
var rectBorderVAO: c_uint = undefined;
var rectBorderVBO: c_uint = undefined;
fn initRectBorder() void {
rectBorderPipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/RectBorder.vert",
"assets/cubyz/shaders/graphics/RectBorder.frag",
"",
&rectBorderUniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
const rawData = [_]f32{
0, 0, 0, 0,
0, 0, 1, 1,
0, 1, 0, 0,
0, 1, 1, -1,
1, 1, 0, 0,
1, 1, -1, -1,
1, 0, 0, 0,
1, 0, -1, 1,
0, 0, 0, 0,
0, 0, 1, 1,
};
c.glGenVertexArrays(1, &rectBorderVAO);
c.glBindVertexArray(rectBorderVAO);
c.glGenBuffers(1, &rectBorderVBO);
c.glBindBuffer(c.GL_ARRAY_BUFFER, rectBorderVBO);
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 4, c.GL_FLOAT, c.GL_FALSE, 4*@sizeOf(f32), null);
c.glEnableVertexAttribArray(0);
}
fn deinitRectBorder() void {
rectBorderPipeline.deinit();
c.glDeleteVertexArrays(1, &rectBorderVAO);
c.glDeleteBuffers(1, &rectBorderVBO);
}
pub fn rectBorder(_pos: Vec2f, _dim: Vec2f, _width: f32) void {
var pos = _pos;
var dim = _dim;
var width = _width;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
width *= scale;
rectBorderPipeline.bind(getScissor());
c.glUniform2f(rectBorderUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(rectBorderUniforms.start, pos[0], pos[1]);
c.glUniform2f(rectBorderUniforms.size, dim[0], dim[1]);
c.glUniform1i(rectBorderUniforms.rectColor, @bitCast(color));
c.glUniform1f(rectBorderUniforms.lineWidth, width);
c.glBindVertexArray(rectBorderVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 10);
}
// ----------------------------------------------------------------------------
// MARK: drawLine()
var lineUniforms: struct {
screen: c_int,
start: c_int,
direction: c_int,
lineColor: c_int,
} = undefined;
var linePipeline: Pipeline = undefined;
var lineVAO: c_uint = undefined;
var lineVBO: c_uint = undefined;
fn initLine() void {
linePipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/Line.vert",
"assets/cubyz/shaders/graphics/Line.frag",
"",
&lineUniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
const rawData = [_]f32{
0, 0,
1, 1,
};
c.glGenVertexArrays(1, &lineVAO);
c.glBindVertexArray(lineVAO);
c.glGenBuffers(1, &lineVBO);
c.glBindBuffer(c.GL_ARRAY_BUFFER, lineVBO);
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null);
c.glEnableVertexAttribArray(0);
}
fn deinitLine() void {
linePipeline.deinit();
c.glDeleteVertexArrays(1, &lineVAO);
c.glDeleteBuffers(1, &lineVBO);
}
pub fn line(_pos1: Vec2f, _pos2: Vec2f) void {
var pos1 = _pos1;
var pos2 = _pos2;
pos1 *= @splat(scale);
pos1 += translation;
pos2 *= @splat(scale);
pos2 += translation;
linePipeline.bind(getScissor());
c.glUniform2f(lineUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(lineUniforms.start, pos1[0], pos1[1]);
c.glUniform2f(lineUniforms.direction, pos2[0] - pos1[0], pos2[1] - pos1[1]);
c.glUniform1i(lineUniforms.lineColor, @bitCast(color));
c.glBindVertexArray(lineVAO);
c.glDrawArrays(c.GL_LINE_STRIP, 0, 2);
}
// ----------------------------------------------------------------------------
// MARK: drawRect()
// Draw rect can use the same shader as drawline, because it essentially draws lines.
var drawRectVAO: c_uint = undefined;
var drawRectVBO: c_uint = undefined;
fn initDrawRect() void {
const rawData = [_]f32{
0, 0,
0, 1,
1, 1,
1, 0,
};
c.glGenVertexArrays(1, &drawRectVAO);
c.glBindVertexArray(drawRectVAO);
c.glGenBuffers(1, &drawRectVBO);
c.glBindBuffer(c.GL_ARRAY_BUFFER, drawRectVBO);
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null);
c.glEnableVertexAttribArray(0);
}
fn deinitDrawRect() void {
c.glDeleteVertexArrays(1, &drawRectVAO);
c.glDeleteBuffers(1, &drawRectVBO);
}
pub fn rectOutline(_pos: Vec2f, _dim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
linePipeline.bind(getScissor());
c.glUniform2f(lineUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(lineUniforms.start, pos[0], pos[1]); // Move the coordinates, so they are in the center of a pixel.
c.glUniform2f(lineUniforms.direction, dim[0] - 1, dim[1] - 1); // The height is a lot smaller because the inner edge of the rect is drawn.
c.glUniform1i(lineUniforms.lineColor, @bitCast(color));
c.glBindVertexArray(lineVAO);
c.glDrawArrays(c.GL_LINE_LOOP, 0, 5);
}
// ----------------------------------------------------------------------------
// MARK: fillCircle()
var circleUniforms: struct {
screen: c_int,
center: c_int,
radius: c_int,
circleColor: c_int,
} = undefined;
var circlePipeline: Pipeline = undefined;
var circleVAO: c_uint = undefined;
var circleVBO: c_uint = undefined;
fn initCircle() void {
circlePipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/Circle.vert",
"assets/cubyz/shaders/graphics/Circle.frag",
"",
&circleUniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
const rawData = [_]f32{
-1, -1,
-1, 1,
1, -1,
1, 1,
};
c.glGenVertexArrays(1, &circleVAO);
c.glBindVertexArray(circleVAO);
c.glGenBuffers(1, &circleVBO);
c.glBindBuffer(c.GL_ARRAY_BUFFER, circleVBO);
c.glBufferData(c.GL_ARRAY_BUFFER, rawData.len*@sizeOf(f32), &rawData, c.GL_STATIC_DRAW);
c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, 2*@sizeOf(f32), null);
c.glEnableVertexAttribArray(0);
}
fn deinitCircle() void {
circlePipeline.deinit();
c.glDeleteVertexArrays(1, &circleVAO);
c.glDeleteBuffers(1, &circleVBO);
}
pub fn circle(_center: Vec2f, _radius: f32) void {
var center = _center;
var radius = _radius;
center *= @splat(scale);
center += translation;
radius *= scale;
circlePipeline.bind(getScissor());
c.glUniform2f(circleUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(circleUniforms.center, center[0], center[1]); // Move the coordinates, so they are in the center of a pixel.
c.glUniform1f(circleUniforms.radius, radius); // The height is a lot smaller because the inner edge of the rect is drawn.
c.glUniform1i(circleUniforms.circleColor, @bitCast(color));
c.glBindVertexArray(circleVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
// ----------------------------------------------------------------------------
// MARK: drawImage()
// Luckily the vao of the regular rect can used.
var imageUniforms: struct {
screen: c_int,
start: c_int,
size: c_int,
color: c_int,
uvOffset: c_int,
uvDim: c_int,
} = undefined;
var imagePipeline: Pipeline = undefined;
fn initImage() void {
imagePipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/Image.vert",
"assets/cubyz/shaders/graphics/Image.frag",
"",
&imageUniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
}
fn deinitImage() void {
imagePipeline.deinit();
}
pub fn boundImage(_pos: Vec2f, _dim: Vec2f) void {
imagePipeline.bind(getScissor());
customShadedImage(&imageUniforms, _pos, _dim);
}
pub fn boundSubImage(_pos: Vec2f, _dim: Vec2f, uvOffset: Vec2f, uvDim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
pos = @floor(pos);
dim = @ceil(dim);
imagePipeline.bind(getScissor());
c.glUniform2f(imageUniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(imageUniforms.start, pos[0], pos[1]);
c.glUniform2f(imageUniforms.size, dim[0], dim[1]);
c.glUniform1i(imageUniforms.color, @bitCast(color));
c.glUniform2f(imageUniforms.uvOffset, uvOffset[0], 1 - uvOffset[1] - uvDim[1]);
c.glUniform2f(imageUniforms.uvDim, uvDim[0], uvDim[1]);
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
pub fn customShadedImage(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
pos = @floor(pos);
dim = @ceil(dim);
c.glUniform2f(uniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(uniforms.start, pos[0], pos[1]);
c.glUniform2f(uniforms.size, dim[0], dim[1]);
c.glUniform1i(uniforms.color, @bitCast(color));
c.glUniform2f(uniforms.uvOffset, 0, 0);
c.glUniform2f(uniforms.uvDim, 1, 1);
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
// ----------------------------------------------------------------------------
// MARK: customShadedRect()
pub fn customShadedRect(uniforms: anytype, _pos: Vec2f, _dim: Vec2f) void {
var pos = _pos;
var dim = _dim;
pos *= @splat(scale);
pos += translation;
dim *= @splat(scale);
pos = @floor(pos);
dim = @ceil(dim);
c.glUniform2f(uniforms.screen, @floatFromInt(Window.width), @floatFromInt(Window.height));
c.glUniform2f(uniforms.start, pos[0], pos[1]);
c.glUniform2f(uniforms.size, dim[0], dim[1]);
c.glUniform1i(uniforms.color, @bitCast(color));
c.glUniform1f(uniforms.scale, scale);
c.glBindVertexArray(rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
// ----------------------------------------------------------------------------
// MARK: text()
pub fn text(_text: []const u8, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void {
TextRendering.renderText(_text, x, y, fontSize, .{.color = @truncate(@as(u32, @bitCast(color)))}, alignment);
}
pub inline fn print(comptime format: []const u8, args: anytype, x: f32, y: f32, fontSize: f32, alignment: TextBuffer.Alignment) void {
const string = std.fmt.allocPrint(main.stackAllocator.allocator, format, args) catch unreachable;
defer main.stackAllocator.free(string);
text(string, x, y, fontSize, alignment);
}
};
pub const TextBuffer = struct { // MARK: TextBuffer
pub const Alignment = enum {
left,
center,
right,
};
pub const FontEffect = packed struct(u28) {
color: u24 = 0xffffff,
bold: bool = false,
italic: bool = false,
underline: bool = false,
strikethrough: bool = false,
fn hasLine(self: FontEffect, comptime isUnderline: bool) bool {
if(isUnderline) return self.underline;
return self.strikethrough;
}
};
const Line = struct {
start: f32,
end: f32,
color: u24,
isUnderline: bool,
};
const LineBreak = struct {
index: u32,
width: f32,
};
const GlyphData = struct {
x_advance: f32,
y_advance: f32,
x_offset: f32,
y_offset: f32,
character: u21,
index: u32,
cluster: u32,
fontEffect: FontEffect,
characterIndex: u32,
};
alignment: Alignment,
width: f32,
buffer: ?*hbft.hb_buffer_t,
glyphs: []GlyphData,
lines: main.List(Line),
lineBreaks: main.List(LineBreak),
fn addLine(self: *TextBuffer, line: Line) void {
if(line.start != line.end) {
self.lines.append(line);
}
}
fn initLines(self: *TextBuffer, comptime isUnderline: bool) void {
var line: Line = Line{.start = 0, .end = 0, .color = 0, .isUnderline = isUnderline};
var lastFontEffect: FontEffect = .{};
for(self.glyphs) |glyph| {
const fontEffect = glyph.fontEffect;
if(lastFontEffect.hasLine(isUnderline)) {
if(fontEffect.color != lastFontEffect.color) {
self.addLine(line);
line.color = fontEffect.color;
line.start = line.end;
} else if(!fontEffect.hasLine(isUnderline)) {
self.addLine(line);
}
} else if(fontEffect.hasLine(isUnderline)) {
line.start = line.end;
line.color = fontEffect.color;
}
lastFontEffect = fontEffect;
line.end += glyph.x_advance;
}
if(lastFontEffect.hasLine(isUnderline)) {
self.addLine(line);
}
}
pub const Parser = struct {
unicodeIterator: std.unicode.Utf8Iterator,
currentFontEffect: FontEffect,
parsedText: main.List(u32),
fontEffects: main.List(FontEffect),
characterIndex: main.List(u32),
showControlCharacters: bool,
curChar: u21 = undefined,
curIndex: u32 = 0,
fn appendControlGetNext(self: *Parser) ?void {
if(self.showControlCharacters) {
self.fontEffects.append(.{.color = 0x808080});
self.parsedText.append(self.curChar);
self.characterIndex.append(self.curIndex);
}
self.curIndex = @intCast(self.unicodeIterator.i);
self.curChar = self.unicodeIterator.nextCodepoint() orelse return null;
}
fn appendGetNext(self: *Parser) ?void {
self.fontEffects.append(self.currentFontEffect);
self.parsedText.append(self.curChar);
self.characterIndex.append(self.curIndex);
self.curIndex = @intCast(self.unicodeIterator.i);
self.curChar = self.unicodeIterator.nextCodepoint() orelse return null;
}
fn peekNextByte(self: *Parser) u8 {
const next = self.unicodeIterator.peek(1);
if(next.len == 0) return 0;
return next[0];
}
fn parse(self: *Parser) void {
self.curIndex = @intCast(self.unicodeIterator.i);
self.curChar = self.unicodeIterator.nextCodepoint() orelse return;
while(true) switch(self.curChar) {
'*' => {
self.appendControlGetNext() orelse return;
if(self.curChar == '*') {
self.appendControlGetNext() orelse return;
self.currentFontEffect.bold = !self.currentFontEffect.bold;
} else {
self.currentFontEffect.italic = !self.currentFontEffect.italic;
}
},
'_' => {
if(self.peekNextByte() == '_') {
self.appendControlGetNext() orelse return;
self.appendControlGetNext() orelse return;
self.currentFontEffect.underline = !self.currentFontEffect.underline;
} else {
self.appendGetNext() orelse return;
}
},
'~' => {
if(self.peekNextByte() == '~') {
self.appendControlGetNext() orelse return;
self.appendControlGetNext() orelse return;
self.currentFontEffect.strikethrough = !self.currentFontEffect.strikethrough;
} else {
self.appendGetNext() orelse return;
}
},
'\\' => {
self.appendControlGetNext() orelse return;
self.appendGetNext() orelse return;
},
'#' => {
self.appendControlGetNext() orelse return;
var shift: u5 = 20;
while(true) : (shift -= 4) {
self.currentFontEffect.color = (self.currentFontEffect.color & ~(@as(u24, 0xf) << shift)) | @as(u24, switch(self.curChar) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => self.curChar - '0',
'a', 'b', 'c', 'd', 'e', 'f' => self.curChar - 'a' + 10,
'A', 'B', 'C', 'D', 'E', 'F' => self.curChar - 'A' + 10,
else => 0,
}) << shift;
self.appendControlGetNext() orelse return;
if(shift == 0) break;
}
},
'§' => {
self.currentFontEffect = .{.color = self.currentFontEffect.color};
self.appendControlGetNext() orelse return;
},
else => {
self.appendGetNext() orelse return;
},
};
}
pub fn countVisibleCharacters(text: []const u8) usize {
var unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0};
var count: usize = 0;
var curChar = unicodeIterator.nextCodepoint() orelse return count;
while(true) switch(curChar) {
'*' => {
curChar = unicodeIterator.nextCodepoint() orelse break;
},
'_' => {
curChar = unicodeIterator.nextCodepoint() orelse break;
if(curChar == '_') {
curChar = unicodeIterator.nextCodepoint() orelse break;
} else {
count += 1;
}
},
'~' => {
curChar = unicodeIterator.nextCodepoint() orelse break;
if(curChar == '~') {
curChar = unicodeIterator.nextCodepoint() orelse break;
} else {
count += 1;
}
},
'\\' => {
curChar = unicodeIterator.nextCodepoint() orelse break;
curChar = unicodeIterator.nextCodepoint() orelse break;
count += 1;
},
'#' => {
for(0..7) |_| curChar = unicodeIterator.nextCodepoint() orelse break;
},
'§' => {
curChar = unicodeIterator.nextCodepoint() orelse break;
},
else => {
count += 1;
curChar = unicodeIterator.nextCodepoint() orelse break;
},
};
return count;
}
};
pub fn init(allocator: NeverFailingAllocator, text: []const u8, initialFontEffect: FontEffect, showControlCharacters: bool, alignment: Alignment) TextBuffer {
var self: TextBuffer = undefined;
self.alignment = alignment;
// Parse the input text:
var parser = Parser{
.unicodeIterator = std.unicode.Utf8Iterator{.bytes = text, .i = 0},
.currentFontEffect = initialFontEffect,
.parsedText = .init(main.stackAllocator),
.fontEffects = .init(allocator),
.characterIndex = .init(allocator),
.showControlCharacters = showControlCharacters,
};
defer parser.fontEffects.deinit();
defer parser.parsedText.deinit();
defer parser.characterIndex.deinit();
self.lines = .init(allocator);
self.lineBreaks = .init(allocator);
parser.parse();
if(parser.parsedText.items.len == 0) {
self.lineBreaks.append(.{.index = 0, .width = 0});
self.glyphs = &[0]GlyphData{};
return self;
}
// Let harfbuzz do its thing:
const buffer = hbft.hb_buffer_create() orelse @panic("Out of Memory while creating harfbuzz buffer");
defer hbft.hb_buffer_destroy(buffer);
hbft.hb_buffer_add_utf32(buffer, parser.parsedText.items.ptr, @intCast(parser.parsedText.items.len), 0, @intCast(parser.parsedText.items.len));
hbft.hb_buffer_set_direction(buffer, hbft.HB_DIRECTION_LTR);
hbft.hb_buffer_set_script(buffer, hbft.HB_SCRIPT_COMMON);
hbft.hb_buffer_set_language(buffer, hbft.hb_language_get_default());
hbft.hb_shape(TextRendering.harfbuzzFont, buffer, null, 0);
var glyphInfos: []hbft.hb_glyph_info_t = undefined;
var glyphPositions: []hbft.hb_glyph_position_t = undefined;
{
var len: c_uint = 0;
glyphInfos.ptr = hbft.hb_buffer_get_glyph_infos(buffer, &len).?;
glyphPositions.ptr = hbft.hb_buffer_get_glyph_positions(buffer, &len).?;
glyphInfos.len = len;
glyphPositions.len = len;
}
// Guess the text index from the given cluster indices. Only works if the number of glyphs and the number of characters in a cluster is the same.
const textIndexGuess = main.stackAllocator.alloc(u32, glyphInfos.len);
defer main.stackAllocator.free(textIndexGuess);
for(textIndexGuess, 0..) |*index, i| {
if(i == 0 or glyphInfos[i - 1].cluster != glyphInfos[i].cluster) {
index.* = glyphInfos[i].cluster;
} else {
index.* = @min(textIndexGuess[i - 1] + 1, @as(u32, @intCast(parser.parsedText.items.len - 1)));
for(glyphInfos[i..]) |glyphInfo| {
if(glyphInfo.cluster != glyphInfos[i].cluster) {
index.* = @min(index.*, glyphInfo.cluster - 1);
break;
}
}
}
}
// Merge it all together:
self.glyphs = allocator.alloc(GlyphData, glyphInfos.len);
for(self.glyphs, 0..) |*glyph, i| {
glyph.x_advance = @as(f32, @floatFromInt(glyphPositions[i].x_advance))/4.0;
glyph.y_advance = @as(f32, @floatFromInt(glyphPositions[i].y_advance))/4.0;
glyph.x_offset = @as(f32, @floatFromInt(glyphPositions[i].x_offset))/4.0;
glyph.y_offset = @as(f32, @floatFromInt(glyphPositions[i].y_offset))/4.0;
glyph.character = @intCast(parser.parsedText.items[textIndexGuess[i]]);
glyph.index = glyphInfos[i].codepoint;
glyph.cluster = glyphInfos[i].cluster;
glyph.fontEffect = parser.fontEffects.items[textIndexGuess[i]];
glyph.characterIndex = parser.characterIndex.items[textIndexGuess[i]];
}
// Find the lines:
self.initLines(true);
self.initLines(false);
self.lineBreaks.append(.{.index = 0, .width = 0});
self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = 0});
return self;
}
pub fn deinit(self: TextBuffer) void {
self.lines.allocator.free(self.glyphs);
self.lines.deinit();
self.lineBreaks.deinit();
}
fn getLineOffset(self: TextBuffer, line: usize) f32 {
const factor: f32 = switch(self.alignment) {
.left => 0,
.center => 0.5,
.right => 1,
};
const diff = self.width - self.lineBreaks.items[line + 1].width;
return diff*factor;
}
pub fn mousePosToIndex(self: TextBuffer, mousePos: Vec2f, bufferLen: usize) u32 {
var line: usize = @intFromFloat(@max(0, mousePos[1]/16.0));
line = @min(line, self.lineBreaks.items.len - 2);
var x: f32 = self.getLineOffset(line);
const start = self.lineBreaks.items[line].index;
const end = self.lineBreaks.items[line + 1].index;
for(self.glyphs[start..end]) |glyph| {
if(mousePos[0] < x + glyph.x_advance/2) {
return @intCast(glyph.characterIndex);
}
x += glyph.x_advance;
}
return @intCast(if(end < self.glyphs.len) self.glyphs[end - 1].characterIndex else bufferLen);
}
pub fn indexToCursorPos(self: TextBuffer, index: u32) Vec2f {
var x: f32 = 0;
var y: f32 = 0;
var i: usize = 0;
while(true) {
x = self.getLineOffset(i);
for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| {
if(glyph.characterIndex == index) {
return .{x, y};
}
x += glyph.x_advance;
y -= glyph.y_advance;
}
i += 1;
if(i >= self.lineBreaks.items.len - 1) {
return .{x, y};
}
y += 16;
}
}
/// Returns the calculated dimensions of the text block.
pub fn calculateLineBreaks(self: *TextBuffer, fontSize: f32, maxLineWidth: f32) Vec2f {
self.lineBreaks.clearRetainingCapacity();
const spaceCharacterWidth = 8;
self.lineBreaks.append(.{.index = 0, .width = 0});
const scaledMaxWidth = maxLineWidth/fontSize*16.0;
var lineWidth: f32 = 0;
var lastSpaceWidth: f32 = 0;
var lastSpaceIndex: u32 = 0;
for(self.glyphs, 0..) |glyph, i| {
lineWidth += glyph.x_advance;
if(glyph.character == ' ') {
lastSpaceWidth = lineWidth;
lastSpaceIndex = @intCast(i + 1);
}
if(glyph.character == '\n') {
self.lineBreaks.append(.{.index = @intCast(i + 1), .width = lineWidth - spaceCharacterWidth});
lineWidth = 0;
lastSpaceIndex = 0;
lastSpaceWidth = 0;
}
if(lineWidth > scaledMaxWidth) {
if(lastSpaceIndex != 0) {
lineWidth -= lastSpaceWidth;
self.lineBreaks.append(.{.index = lastSpaceIndex, .width = lastSpaceWidth - spaceCharacterWidth});
lastSpaceIndex = 0;
lastSpaceWidth = 0;
} else {
self.lineBreaks.append(.{.index = @intCast(i), .width = lineWidth - glyph.x_advance});
lineWidth = glyph.x_advance;
lastSpaceIndex = 0;
lastSpaceWidth = 0;
}
}
}
self.width = maxLineWidth;
self.lineBreaks.append(.{.index = @intCast(self.glyphs.len), .width = lineWidth});
return Vec2f{maxLineWidth*fontSize/16.0, @as(f32, @floatFromInt(self.lineBreaks.items.len - 1))*fontSize};
}
pub fn drawSelection(self: TextBuffer, pos: Vec2f, selectionStart: u32, selectionEnd: u32) void {
std.debug.assert(selectionStart <= selectionEnd);
var x: f32 = self.getLineOffset(0);
var y: f32 = 0;
var i: usize = 0;
var j: usize = 0;
// Find the start row:
outer: while(i < self.lineBreaks.items.len - 1) : (i += 1) {
x = self.getLineOffset(i);
while(j < self.lineBreaks.items[i + 1].index) : (j += 1) {
const glyph = self.glyphs[j];
if(glyph.characterIndex >= selectionStart) break :outer;
x += glyph.x_advance;
y -= glyph.y_advance;
}
y += 16;
}
while(i < self.lineBreaks.items.len - 1) {
const startX = x;
while(j < self.lineBreaks.items[i + 1].index and j < selectionEnd) : (j += 1) {
const glyph = self.glyphs[j];
if(glyph.characterIndex >= selectionEnd) break;
x += glyph.x_advance;
y -= glyph.y_advance;
}
draw.rect(pos + Vec2f{startX, y}, .{x - startX, 16});
i += 1;
if(i >= self.lineBreaks.items.len - 1) break;
x = self.getLineOffset(i);
y += 16;
}
}
pub fn render(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void {
self.renderShadow(_x, _y, _fontSize);
const oldTranslation = draw.setTranslation(.{_x, _y});
defer draw.restoreTranslation(oldTranslation);
const oldScale = draw.setScale(_fontSize/16.0);
defer draw.restoreScale(oldScale);
var x: f32 = 0;
var y: f32 = 0;
TextRendering.pipeline.bind(draw.getScissor());
c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(main.Window.width), @floatFromInt(main.Window.height));
c.glUniform1f(TextRendering.uniforms.ratio, draw.scale);
c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0);
c.glActiveTexture(c.GL_TEXTURE0);
c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]);
c.glBindVertexArray(draw.rectVAO);
const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1);
defer main.stackAllocator.free(lineWraps);
var i: usize = 0;
while(i < self.lineBreaks.items.len - 1) : (i += 1) {
x = self.getLineOffset(i);
for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| {
if(glyph.character != '\n') {
const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue;
TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(glyph.fontEffect));
}
x += glyph.x_advance;
y -= glyph.y_advance;
}
lineWraps[i] = x - self.getLineOffset(i);
x = 0;
y += 16;
}
for(self.lines.items) |_line| {
var line: Line = _line;
y = 0;
y += if(line.isUnderline) 15 else 8;
const oldColor = draw.color;
draw.setColor(line.color | (@as(u32, 0xff000000) & draw.color));
defer draw.setColor(oldColor);
for(lineWraps, 0..) |lineWrap, j| {
const lineStart = @max(0, line.start);
const lineEnd = @min(lineWrap, line.end);
if(lineStart < lineEnd) {
const start = Vec2f{lineStart + self.getLineOffset(j), y};
const dim = Vec2f{lineEnd - lineStart, 1};
draw.rect(start, dim);
}
line.start -= lineWrap;
line.end -= lineWrap;
y += 16;
}
}
}
fn shadowColor(color: u24) u24 {
const r: f32 = @floatFromInt(color >> 16);
const g: f32 = @floatFromInt(color >> 8 & 255);
const b: f32 = @floatFromInt(color & 255);
const perceivedBrightness = @sqrt(0.299*r*r + 0.587*g*g + 0.114*b*b);
if(perceivedBrightness < 64) {
return 0xffffff; // Make shadows white for better readability.
} else {
return 0;
}
}
fn renderShadow(self: TextBuffer, _x: f32, _y: f32, _fontSize: f32) void { // Basically a copy of render with some color and position changes.
const oldTranslation = draw.setTranslation(.{_x + _fontSize/16.0, _y + _fontSize/16.0});
defer draw.restoreTranslation(oldTranslation);
const oldScale = draw.setScale(_fontSize/16.0);
defer draw.restoreScale(oldScale);
var x: f32 = 0;
var y: f32 = 0;
TextRendering.pipeline.bind(draw.getScissor());
c.glUniform2f(TextRendering.uniforms.scene, @floatFromInt(main.Window.width), @floatFromInt(main.Window.height));
c.glUniform1f(TextRendering.uniforms.ratio, draw.scale);
c.glUniform1f(TextRendering.uniforms.alpha, @as(f32, @floatFromInt(draw.color >> 24))/255.0);
c.glActiveTexture(c.GL_TEXTURE0);
c.glBindTexture(c.GL_TEXTURE_2D, TextRendering.glyphTexture[0]);
c.glBindVertexArray(draw.rectVAO);
const lineWraps: []f32 = main.stackAllocator.alloc(f32, self.lineBreaks.items.len - 1);
defer main.stackAllocator.free(lineWraps);
var i: usize = 0;
while(i < self.lineBreaks.items.len - 1) : (i += 1) {
x = self.getLineOffset(i);
for(self.glyphs[self.lineBreaks.items[i].index..self.lineBreaks.items[i + 1].index]) |glyph| {
if(glyph.character != '\n') {
const ftGlyph = TextRendering.getGlyph(glyph.index) catch continue;
var fontEffect = glyph.fontEffect;
fontEffect.color = shadowColor(fontEffect.color);
TextRendering.drawGlyph(ftGlyph, x + glyph.x_offset, y - glyph.y_offset, @bitCast(fontEffect));
}
x += glyph.x_advance;
y -= glyph.y_advance;
}
lineWraps[i] = x - self.getLineOffset(i);
x = 0;
y += 16;
}
for(self.lines.items) |_line| {
var line: Line = _line;
y = 0;
y += if(line.isUnderline) 15 else 8;
const oldColor = draw.color;
draw.setColor(shadowColor(line.color) | (@as(u32, 0xff000000) & draw.color));
defer draw.setColor(oldColor);
for(lineWraps, 0..) |lineWrap, j| {
const lineStart = @max(0, line.start);
const lineEnd = @min(lineWrap, line.end);
if(lineStart < lineEnd) {
const start = Vec2f{lineStart + self.getLineOffset(j), y};
const dim = Vec2f{lineEnd - lineStart, 1};
draw.rect(start, dim);
}
line.start -= lineWrap;
line.end -= lineWrap;
y += 16;
}
}
}
};
const TextRendering = struct { // MARK: TextRendering
const Glyph = struct {
textureX: i32,
size: Vec2i,
bearing: Vec2i,
advance: f32,
};
var pipeline: Pipeline = undefined;
var uniforms: struct {
texture_rect: c_int,
scene: c_int,
offset: c_int,
ratio: c_int,
fontEffects: c_int,
fontSize: c_int,
alpha: c_int,
} = undefined;
var freetypeLib: hbft.FT_Library = undefined;
var freetypeFace: hbft.FT_Face = undefined;
var harfbuzzFace: ?*hbft.hb_face_t = undefined;
var harfbuzzFont: ?*hbft.hb_font_t = undefined;
var glyphMapping: main.List(u31) = undefined;
var glyphData: main.List(Glyph) = undefined;
var glyphTexture: [2]c_uint = undefined;
var textureWidth: i32 = 1024;
const textureHeight: i32 = 16;
var textureOffset: i32 = 0;
fn ftError(errorCode: hbft.FT_Error) !void {
if(errorCode == 0) return;
const errorString = hbft.FT_Error_String(errorCode);
std.log.err("Got freetype error {s}", .{errorString});
return error.freetype;
}
fn init() !void {
pipeline = Pipeline.init(
"assets/cubyz/shaders/graphics/Text.vert",
"assets/cubyz/shaders/graphics/Text.frag",
"",
&uniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.alphaBlending}},
);
pipeline.bind(null);
errdefer pipeline.deinit();
c.glUniform1f(uniforms.alpha, 1.0);
c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight));
try ftError(hbft.FT_Init_FreeType(&freetypeLib));
try ftError(hbft.FT_New_Face(freetypeLib, "assets/cubyz/fonts/unscii-16-full.ttf", 0, &freetypeFace));
try ftError(hbft.FT_Set_Pixel_Sizes(freetypeFace, 0, textureHeight));
harfbuzzFace = hbft.hb_ft_face_create_referenced(freetypeFace);
harfbuzzFont = hbft.hb_font_create(harfbuzzFace);
glyphMapping = .init(main.globalAllocator);
glyphData = .init(main.globalAllocator);
glyphData.append(undefined); // 0 is a reserved value.
c.glGenTextures(2, &glyphTexture);
c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]);
c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, textureWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT);
c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[1]);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT);
}
fn deinit() void {
pipeline.deinit();
ftError(hbft.FT_Done_FreeType(freetypeLib)) catch {};
glyphMapping.deinit();
glyphData.deinit();
c.glDeleteTextures(2, &glyphTexture);
hbft.hb_font_destroy(harfbuzzFont);
}
fn resizeTexture(newWidth: i32) void {
textureWidth = newWidth;
const swap = glyphTexture[1];
glyphTexture[1] = glyphTexture[0];
glyphTexture[0] = swap;
c.glActiveTexture(c.GL_TEXTURE0);
c.glBindTexture(c.GL_TEXTURE_2D, glyphTexture[0]);
c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, newWidth, textureHeight, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null);
c.glCopyImageSubData(glyphTexture[1], c.GL_TEXTURE_2D, 0, 0, 0, 0, glyphTexture[0], c.GL_TEXTURE_2D, 0, 0, 0, 0, textureOffset, textureHeight, 1);
pipeline.bind(draw.getScissor());
c.glUniform2f(uniforms.fontSize, @floatFromInt(textureWidth), @floatFromInt(textureHeight));
}
fn uploadData(bitmap: hbft.FT_Bitmap) void {
const width: i32 = @bitCast(bitmap.width);
const height: i32 = @bitCast(bitmap.rows);
const buffer = bitmap.buffer orelse return;
if(textureOffset + width > textureWidth) {
resizeTexture(textureWidth*2);
}
c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1);
c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, textureOffset, 0, width, height, c.GL_RED, c.GL_UNSIGNED_BYTE, buffer);
textureOffset += width;
}
fn getGlyph(index: u32) !Glyph {
if(index >= glyphMapping.items.len) {
glyphMapping.appendNTimes(0, index - glyphMapping.items.len + 1);
}
if(glyphMapping.items[index] == 0) { // glyph was not initialized yet.
try ftError(hbft.FT_Load_Glyph(freetypeFace, index, hbft.FT_LOAD_RENDER));
const glyph = freetypeFace.*.glyph;
const bitmap = glyph.*.bitmap;
const width = bitmap.width;
const height = bitmap.rows;
glyphMapping.items[index] = @intCast(glyphData.items.len);
glyphData.addOne().* = Glyph{
.textureX = textureOffset,
.size = Vec2i{@intCast(width), @intCast(height)},
.bearing = Vec2i{glyph.*.bitmap_left, 16 - glyph.*.bitmap_top},
.advance = @as(f32, @floatFromInt(glyph.*.advance.x))/@as(f32, 1 << 6),
};
uploadData(bitmap);
}
return glyphData.items[glyphMapping.items[index]];
}
fn drawGlyph(glyph: Glyph, _x: f32, _y: f32, fontEffects: u28) void {
var x = _x;
var y = _y;
x *= draw.scale;
y *= draw.scale;
x += draw.translation[0];
y += draw.translation[1];
x = @floor(x);
y = @ceil(y);
c.glUniform1i(uniforms.fontEffects, fontEffects);
if(fontEffects & 0x1000000 != 0) { // bold
c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1);
c.glUniform4f(uniforms.texture_rect, @floatFromInt(glyph.textureX), -1, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1] + 1));
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
// Just draw another thing on top in x direction. The y-direction is handled in the shader.
c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x + 0.5, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y - 1);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
} else {
c.glUniform2f(uniforms.offset, @as(f32, @floatFromInt(glyph.bearing[0]))*draw.scale + x, @as(f32, @floatFromInt(glyph.bearing[1]))*draw.scale + y);
c.glUniform4f(uniforms.texture_rect, @floatFromInt(glyph.textureX), 0, @floatFromInt(glyph.size[0]), @floatFromInt(glyph.size[1]));
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
}
}
fn renderText(text: []const u8, x: f32, y: f32, fontSize: f32, initialFontEffect: TextBuffer.FontEffect, alignment: TextBuffer.Alignment) void {
const buf = TextBuffer.init(main.stackAllocator, text, initialFontEffect, false, alignment);
defer buf.deinit();
buf.render(x, y, fontSize);
}
};
pub fn init() void { // MARK: init()
draw.initCircle();
draw.initDrawRect();
draw.initImage();
draw.initLine();
draw.initRect();
draw.initRectBorder();
TextRendering.init() catch |err| {
std.log.err("Error while initializing TextRendering: {s}", .{@errorName(err)});
};
block_texture.init();
if(glslang.glslang_initialize_process() == glslang.false) std.log.err("glslang_initialize_process failed", .{});
}
pub fn deinit() void {
draw.deinitCircle();
draw.deinitDrawRect();
draw.deinitImage();
draw.deinitLine();
draw.deinitRect();
draw.deinitRectBorder();
TextRendering.deinit();
block_texture.deinit();
glslang.glslang_finalize_process();
}
const Shader = struct { // MARK: Shader
id: c_uint,
fn compileToSpirV(allocator: NeverFailingAllocator, source: []const u8, filename: []const u8, defines: []const u8, shaderStage: glslang.glslang_stage_t) ![]c_uint {
const versionLineEnd = if(std.mem.indexOfScalar(u8, source, '\n')) |len| len + 1 else 0;
const versionLine = source[0..versionLineEnd];
const sourceLines = source[versionLineEnd..];
var sourceWithDefines = main.List(u8).init(main.stackAllocator);
defer sourceWithDefines.deinit();
sourceWithDefines.appendSlice(versionLine);
sourceWithDefines.appendSlice(defines);
sourceWithDefines.appendSlice(sourceLines);
sourceWithDefines.append(0);
const input = glslang.glslang_input_t{
.language = glslang.GLSLANG_SOURCE_GLSL,
.stage = shaderStage,
.client = glslang.GLSLANG_CLIENT_OPENGL,
.client_version = glslang.GLSLANG_TARGET_OPENGL_450,
.target_language = glslang.GLSLANG_TARGET_SPV,
.target_language_version = glslang.GLSLANG_TARGET_SPV_1_0,
.code = sourceWithDefines.items.ptr,
.default_version = 100,
.default_profile = glslang.GLSLANG_NO_PROFILE,
.force_default_version_and_profile = glslang.false,
.forward_compatible = glslang.false,
.messages = glslang.GLSLANG_MSG_DEFAULT_BIT,
.resource = glslang.glslang_default_resource(),
.callbacks = .{}, // TODO: Add support for shader includes
.callbacks_ctx = null,
};
const shader = glslang.glslang_shader_create(&input);
defer glslang.glslang_shader_delete(shader);
if(glslang.glslang_shader_preprocess(shader, &input) == 0) {
std.log.err("Error preprocessing shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)});
return error.FailedCompiling;
}
if(glslang.glslang_shader_parse(shader, &input) == 0) {
std.log.err("Error parsing shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)});
return error.FailedCompiling;
}
const program = glslang.glslang_program_create();
defer glslang.glslang_program_delete(program);
glslang.glslang_program_add_shader(program, shader);
if(glslang.glslang_program_link(program, glslang.GLSLANG_MSG_SPV_RULES_BIT | glslang.GLSLANG_MSG_VULKAN_RULES_BIT) == 0) {
std.log.err("Error linking shader {s}:\n{s}\n{s}\n", .{filename, glslang.glslang_shader_get_info_log(shader), glslang.glslang_shader_get_info_debug_log(shader)});
return error.FailedCompiling;
}
glslang.glslang_program_SPIRV_generate(program, shaderStage);
const result = allocator.alloc(c_uint, glslang.glslang_program_SPIRV_get_size(program));
glslang.glslang_program_SPIRV_get(program, result.ptr);
return result;
}
fn addShader(self: *const Shader, filename: []const u8, defines: []const u8, shaderStage: c_uint) !void {
const source = main.files.read(main.stackAllocator, filename) catch |err| {
std.log.err("Couldn't read shader file: {s}", .{filename});
return err;
};
defer main.stackAllocator.free(source);
// SPIR-V will be used for the Vulkan, now it's completely useless due to lack of support in Vulkan drivers
const glslangStage: glslang.glslang_stage_t = if(shaderStage == c.GL_VERTEX_SHADER) glslang.GLSLANG_STAGE_VERTEX else if(shaderStage == c.GL_FRAGMENT_SHADER) glslang.GLSLANG_STAGE_FRAGMENT else glslang.GLSLANG_STAGE_COMPUTE;
main.stackAllocator.free(try compileToSpirV(main.stackAllocator, source, filename, defines, glslangStage));
const shader = c.glCreateShader(shaderStage);
defer c.glDeleteShader(shader);
const versionLineEnd = if(std.mem.indexOfScalar(u8, source, '\n')) |len| len + 1 else 0;
const versionLine = source[0..versionLineEnd];
const sourceLines = source[versionLineEnd..];
const sourceLen: [3]c_int = .{@intCast(versionLine.len), @intCast(defines.len), @intCast(sourceLines.len)};
c.glShaderSource(shader, 3, &[3][*c]const u8{versionLine.ptr, defines.ptr, sourceLines.ptr}, &sourceLen);
c.glCompileShader(shader);
var success: c_int = undefined;
c.glGetShaderiv(shader, c.GL_COMPILE_STATUS, &success);
if(success != c.GL_TRUE) {
var len: u32 = undefined;
c.glGetShaderiv(shader, c.GL_INFO_LOG_LENGTH, @ptrCast(&len));
var buf: [4096]u8 = undefined;
c.glGetShaderInfoLog(shader, 4096, @ptrCast(&len), &buf);
std.log.err("Error compiling shader {s}:\n{s}\n", .{filename, buf[0..len]});
return error.FailedCompiling;
}
c.glAttachShader(self.id, shader);
}
fn link(self: *const Shader, file: []const u8) !void {
c.glLinkProgram(self.id);
var success: c_int = undefined;
c.glGetProgramiv(self.id, c.GL_LINK_STATUS, &success);
if(success != c.GL_TRUE) {
var len: u32 = undefined;
c.glGetProgramiv(self.id, c.GL_INFO_LOG_LENGTH, @ptrCast(&len));
var buf: [4096]u8 = undefined;
c.glGetProgramInfoLog(self.id, 4096, @ptrCast(&len), &buf);
std.log.err("Error Linking Shader program {s}:\n{s}\n", .{file, buf[0..len]});
return error.FailedLinking;
}
}
fn init(vertex: []const u8, fragment: []const u8, defines: []const u8, uniformStruct: anytype) Shader {
const shader = Shader{.id = c.glCreateProgram()};
shader.addShader(vertex, defines, c.GL_VERTEX_SHADER) catch return shader;
shader.addShader(fragment, defines, c.GL_FRAGMENT_SHADER) catch return shader;
shader.link(fragment) catch return shader;
if(@TypeOf(uniformStruct) != @TypeOf(null)) {
inline for(@typeInfo(@TypeOf(uniformStruct.*)).@"struct".fields) |field| {
if(field.type == c_int) {
@field(uniformStruct, field.name) = c.glGetUniformLocation(shader.id, field.name[0..]);
}
}
}
return shader;
}
fn initCompute(compute: []const u8, defines: []const u8, uniformStruct: anytype) Shader {
const shader = Shader{.id = c.glCreateProgram()};
shader.addShader(compute, defines, c.GL_COMPUTE_SHADER) catch return shader;
shader.link(compute) catch return shader;
if(@TypeOf(uniformStruct) != @TypeOf(null)) {
inline for(@typeInfo(@TypeOf(uniformStruct.*)).@"struct".fields) |field| {
if(field.type == c_int) {
@field(uniformStruct, field.name) = c.glGetUniformLocation(shader.id, field.name[0..]);
}
}
}
return shader;
}
fn bind(self: *const Shader) void {
c.glUseProgram(self.id);
}
fn deinit(self: *const Shader) void {
c.glDeleteProgram(self.id);
}
};
pub const Pipeline = struct { // MARK: Pipeline
shader: Shader,
rasterState: RasterizationState,
multisampleState: MultisampleState = .{}, // TODO: Not implemented
depthStencilState: DepthStencilState,
blendState: ColorBlendState,
const RasterizationState = struct {
depthClamp: bool = true,
rasterizerDiscard: bool = false,
polygonMode: PolygonMode = .fill,
cullMode: CullModeFlags = .back,
frontFace: FrontFace = .counterClockwise,
depthBias: ?DepthBias = null,
lineWidth: f32 = 1,
const PolygonMode = enum(c.VkPolygonMode) {
fill = c.VK_POLYGON_MODE_FILL,
line = c.VK_POLYGON_MODE_LINE,
point = c.VK_POLYGON_MODE_POINT,
fillRectangleNV = c.VK_POLYGON_MODE_FILL_RECTANGLE_NV,
};
const CullModeFlags = enum(c.VkCullModeFlags) {
none = c.VK_CULL_MODE_NONE,
front = c.VK_CULL_MODE_FRONT_BIT,
back = c.VK_CULL_MODE_BACK_BIT,
frontAndBack = c.VK_CULL_MODE_FRONT_AND_BACK,
};
const FrontFace = enum(c.VkFrontFace) {
counterClockwise = c.VK_FRONT_FACE_COUNTER_CLOCKWISE,
clockwise = c.VK_FRONT_FACE_CLOCKWISE,
};
const DepthBias = struct {
constantFactor: f32,
clamp: f32,
slopeFactor: f32,
};
};
const MultisampleState = struct {
rasterizationSamples: Count = .@"1",
sampleShading: bool = false,
minSampleShading: f32 = undefined,
sampleMask: [*]const c.VkSampleMask = &.{0, 0},
alphaToCoverage: bool = false,
alphaToOne: bool = false,
const Count = enum(c.VkSampleCountFlags) {
@"1" = c.VK_SAMPLE_COUNT_1_BIT,
@"2" = c.VK_SAMPLE_COUNT_2_BIT,
@"4" = c.VK_SAMPLE_COUNT_4_BIT,
@"8" = c.VK_SAMPLE_COUNT_8_BIT,
@"16" = c.VK_SAMPLE_COUNT_16_BIT,
@"32" = c.VK_SAMPLE_COUNT_32_BIT,
@"64" = c.VK_SAMPLE_COUNT_64_BIT,
};
};
const DepthStencilState = struct {
depthTest: bool,
depthWrite: bool = true,
depthCompare: CompareOp = .less,
depthBoundsTest: ?DepthBoundsTest = null,
stencilTest: ?StencilTest = null,
const CompareOp = enum(c.VkCompareOp) {
never = c.VK_COMPARE_OP_NEVER,
less = c.VK_COMPARE_OP_LESS,
equal = c.VK_COMPARE_OP_EQUAL,
lessOrEqual = c.VK_COMPARE_OP_LESS_OR_EQUAL,
greater = c.VK_COMPARE_OP_GREATER,
notEqual = c.VK_COMPARE_OP_NOT_EQUAL,
greateOrEqual = c.VK_COMPARE_OP_GREATER_OR_EQUAL,
always = c.VK_COMPARE_OP_ALWAYS,
};
const StencilTest = struct {
front: StencilOpState,
back: StencilOpState,
const StencilOpState = struct {
failOp: StencilOp,
passOp: StencilOp,
depthFailOp: StencilOp,
compareOp: CompareOp,
compareMask: u32,
writeMask: u32,
reference: u32,
const StencilOp = enum(c.VkStencilOp) {
keep = c.VK_STENCIL_OP_KEEP,
zero = c.VK_STENCIL_OP_ZERO,
replace = c.VK_STENCIL_OP_REPLACE,
incrementAndClamp = c.VK_STENCIL_OP_INCREMENT_AND_CLAMP,
decrementAndClamp = c.VK_STENCIL_OP_DECREMENT_AND_CLAMP,
invert = c.VK_STENCIL_OP_INVERT,
incrementAndWrap = c.VK_STENCIL_OP_INCREMENT_AND_WRAP,
decrementAndWrap = c.VK_STENCIL_OP_DECREMENT_AND_WRAP,
};
};
};
const DepthBoundsTest = struct {
min: f32,
max: f32,
};
};
const ColorBlendAttachmentState = struct {
enabled: bool = true,
srcColorBlendFactor: BlendFactor,
dstColorBlendFactor: BlendFactor,
colorBlendOp: BlendOp,
srcAlphaBlendFactor: BlendFactor,
dstAlphaBlendFactor: BlendFactor,
alphaBlendOp: BlendOp,
colorWriteMask: ColorComponentFlags = .all,
pub const alphaBlending: ColorBlendAttachmentState = .{
.srcColorBlendFactor = .srcAlpha,
.dstColorBlendFactor = .oneMinusSrcAlpha,
.colorBlendOp = .add,
.srcAlphaBlendFactor = .srcAlpha,
.dstAlphaBlendFactor = .oneMinusSrcAlpha,
.alphaBlendOp = .add,
};
pub const noBlending: ColorBlendAttachmentState = .{
.enabled = false,
.srcColorBlendFactor = undefined,
.dstColorBlendFactor = undefined,
.colorBlendOp = undefined,
.srcAlphaBlendFactor = undefined,
.dstAlphaBlendFactor = undefined,
.alphaBlendOp = undefined,
};
const BlendFactor = enum(c.VkBlendFactor) {
zero = c.VK_BLEND_FACTOR_ZERO,
one = c.VK_BLEND_FACTOR_ONE,
srcColor = c.VK_BLEND_FACTOR_SRC_COLOR,
oneMinusSrcColor = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR,
dstColor = c.VK_BLEND_FACTOR_DST_COLOR,
oneMinusDstColor = c.VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR,
srcAlpha = c.VK_BLEND_FACTOR_SRC_ALPHA,
oneMinusSrcAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
dstAlpha = c.VK_BLEND_FACTOR_DST_ALPHA,
oneMinusDstAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA,
constantColor = c.VK_BLEND_FACTOR_CONSTANT_COLOR,
oneMinusConstantColor = c.VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR,
constantAlpha = c.VK_BLEND_FACTOR_CONSTANT_ALPHA,
oneMinusConstantAlpha = c.VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA,
srcAlphaSaturate = c.VK_BLEND_FACTOR_SRC_ALPHA_SATURATE,
src1Color = c.VK_BLEND_FACTOR_SRC1_COLOR,
oneMinusSrc1Color = c.VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR,
src1Alpha = c.VK_BLEND_FACTOR_SRC1_ALPHA,
oneMinusSrc1Alpha = c.VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA,
fn toGl(self: BlendFactor) c.GLenum {
return switch(self) {
.zero => c.GL_ZERO,
.one => c.GL_ONE,
.srcColor => c.GL_SRC_COLOR,
.oneMinusSrcColor => c.GL_ONE_MINUS_SRC_COLOR,
.dstColor => c.GL_DST_COLOR,
.oneMinusDstColor => c.GL_ONE_MINUS_DST_COLOR,
.srcAlpha => c.GL_SRC_ALPHA,
.oneMinusSrcAlpha => c.GL_ONE_MINUS_SRC_ALPHA,
.dstAlpha => c.GL_DST_ALPHA,
.oneMinusDstAlpha => c.GL_ONE_MINUS_DST_ALPHA,
.constantColor => c.GL_CONSTANT_COLOR,
.oneMinusConstantColor => c.GL_ONE_MINUS_CONSTANT_COLOR,
.constantAlpha => c.GL_CONSTANT_ALPHA,
.oneMinusConstantAlpha => c.GL_ONE_MINUS_CONSTANT_ALPHA,
.srcAlphaSaturate => c.GL_SRC_ALPHA_SATURATE,
.src1Color => c.GL_SRC1_COLOR,
.oneMinusSrc1Color => c.GL_ONE_MINUS_SRC1_COLOR,
.src1Alpha => c.GL_SRC1_ALPHA,
.oneMinusSrc1Alpha => c.GL_ONE_MINUS_SRC1_ALPHA,
};
}
};
const BlendOp = enum(c.VkBlendOp) {
add = c.VK_BLEND_OP_ADD,
subtract = c.VK_BLEND_OP_SUBTRACT,
reverseSubtract = c.VK_BLEND_OP_REVERSE_SUBTRACT,
min = c.VK_BLEND_OP_MIN,
max = c.VK_BLEND_OP_MAX,
fn toGl(self: BlendOp) c.GLenum {
return switch(self) {
.add => c.GL_FUNC_ADD,
.subtract => c.GL_FUNC_SUBTRACT,
.reverseSubtract => c.GL_FUNC_REVERSE_SUBTRACT,
.min => c.GL_MIN,
.max => c.GL_MAX,
};
}
};
const ColorComponentFlags = packed struct {
r: bool,
g: bool,
b: bool,
a: bool,
pub const all: ColorComponentFlags = .{.r = true, .g = true, .b = true, .a = true};
pub const none: ColorComponentFlags = .{.r = false, .g = false, .b = false, .a = false};
};
};
const ColorBlendState = struct {
logicOp: ?LogicOp = null,
attachments: []const ColorBlendAttachmentState,
blendConstants: [4]f32 = .{0, 0, 0, 0},
const LogicOp = enum(c.VkLogicOp) {
clear = c.VK_LOGIC_OP_CLEAR,
@"and" = c.VK_LOGIC_OP_AND,
andReverse = c.VK_LOGIC_OP_AND_REVERSE,
copy = c.VK_LOGIC_OP_COPY,
andInverted = c.VK_LOGIC_OP_AND_INVERTED,
noOp = c.VK_LOGIC_OP_NO_OP,
xor = c.VK_LOGIC_OP_XOR,
@"or" = c.VK_LOGIC_OP_OR,
nor = c.VK_LOGIC_OP_NOR,
equivalent = c.VK_LOGIC_OP_EQUIVALENT,
invert = c.VK_LOGIC_OP_INVERT,
orReverse = c.VK_LOGIC_OP_OR_REVERSE,
copyInverted = c.VK_LOGIC_OP_COPY_INVERTED,
orInverted = c.VK_LOGIC_OP_OR_INVERTED,
nand = c.VK_LOGIC_OP_NAND,
set = c.VK_LOGIC_OP_SET,
};
};
pub fn init(vertexPath: []const u8, fragmentPath: []const u8, defines: []const u8, uniformStruct: anytype, rasterState: RasterizationState, depthStencilState: DepthStencilState, blendState: ColorBlendState) Pipeline {
std.debug.assert(depthStencilState.depthBoundsTest == null); // Only available in Vulkan 1.3
std.debug.assert(depthStencilState.stencilTest == null); // TODO: Not yet implemented
std.debug.assert(rasterState.lineWidth <= 1); // Larger values are poorly supported among drivers
std.debug.assert(blendState.logicOp == null); // TODO: Not yet implemented
return .{
.shader = .init(vertexPath, fragmentPath, defines, uniformStruct),
.rasterState = rasterState,
.multisampleState = .{}, // TODO: Not implemented
.depthStencilState = depthStencilState,
.blendState = blendState,
};
}
pub fn deinit(self: Pipeline) void {
self.shader.deinit();
}
fn conditionalEnable(typ: c.GLenum, val: bool) void {
if(val) {
c.glEnable(typ);
} else {
c.glDisable(typ);
}
}
pub fn bind(self: Pipeline, scissor: ?c.VkRect2D) void {
self.shader.bind();
if(scissor) |s| {
c.glEnable(c.GL_SCISSOR_TEST);
c.glScissor(s.offset.x, s.offset.y, @intCast(s.extent.width), @intCast(s.extent.height));
} else {
c.glDisable(c.GL_SCISSOR_TEST);
}
conditionalEnable(c.GL_DEPTH_CLAMP, self.rasterState.depthClamp);
conditionalEnable(c.GL_RASTERIZER_DISCARD, self.rasterState.rasterizerDiscard);
conditionalEnable(c.GL_RASTERIZER_DISCARD, self.rasterState.rasterizerDiscard);
c.glPolygonMode(c.GL_FRONT_AND_BACK, switch(self.rasterState.polygonMode) {
.fill => c.GL_FILL,
.line => c.GL_LINE,
.point => c.GL_POINT,
else => unreachable,
});
if(self.rasterState.cullMode != .none) {
c.glEnable(c.GL_CULL_FACE);
c.glCullFace(switch(self.rasterState.cullMode) {
.front => c.GL_FRONT,
.back => c.GL_BACK,
.frontAndBack => c.GL_FRONT_AND_BACK,
else => unreachable,
});
} else {
c.glDisable(c.GL_CULL_FACE);
}
c.glFrontFace(switch(self.rasterState.frontFace) {
.counterClockwise => c.GL_CCW,
.clockwise => c.GL_CW,
});
if(self.rasterState.depthBias) |depthBias| {
c.glEnable(c.GL_POLYGON_OFFSET_FILL);
c.glEnable(c.GL_POLYGON_OFFSET_LINE);
c.glEnable(c.GL_POLYGON_OFFSET_POINT);
c.glPolygonOffset(depthBias.slopeFactor, depthBias.constantFactor);
} else {
c.glDisable(c.GL_POLYGON_OFFSET_FILL);
c.glDisable(c.GL_POLYGON_OFFSET_LINE);
c.glDisable(c.GL_POLYGON_OFFSET_POINT);
}
c.glLineWidth(self.rasterState.lineWidth);
// TODO: Multisampling
conditionalEnable(c.GL_DEPTH_TEST, self.depthStencilState.depthTest);
c.glDepthMask(@intFromBool(self.depthStencilState.depthWrite));
c.glDepthFunc(switch(self.depthStencilState.depthCompare) {
.never => c.GL_NEVER,
.less => c.GL_LESS,
.equal => c.GL_EQUAL,
.lessOrEqual => c.GL_LEQUAL,
.greater => c.GL_GREATER,
.notEqual => c.GL_NOTEQUAL,
.greateOrEqual => c.GL_GEQUAL,
.always => c.GL_ALWAYS,
});
// TODO: stencilTest
// TODO: logicOp
for(self.blendState.attachments, 0..) |attachment, i| {
c.glColorMask(@intFromBool(attachment.colorWriteMask.r), @intFromBool(attachment.colorWriteMask.g), @intFromBool(attachment.colorWriteMask.b), @intFromBool(attachment.colorWriteMask.a));
if(!attachment.enabled) {
c.glDisable(c.GL_BLEND);
continue;
}
c.glEnable(c.GL_BLEND);
c.glBlendEquationSeparatei(@intCast(i), attachment.colorBlendOp.toGl(), attachment.alphaBlendOp.toGl());
c.glBlendFuncSeparatei(@intCast(i), attachment.srcColorBlendFactor.toGl(), attachment.dstColorBlendFactor.toGl(), attachment.srcAlphaBlendFactor.toGl(), attachment.dstAlphaBlendFactor.toGl());
}
c.glBlendColor(self.blendState.blendConstants[0], self.blendState.blendConstants[1], self.blendState.blendConstants[2], self.blendState.blendConstants[3]);
}
};
pub const ComputePipeline = struct { // MARK: ComputePipeline
shader: Shader,
pub fn init(computePath: []const u8, defines: []const u8, uniformStruct: anytype) ComputePipeline {
return .{
.shader = .initCompute(computePath, defines, uniformStruct),
};
}
pub fn deinit(self: ComputePipeline) void {
self.shader.deinit();
}
pub fn bind(self: ComputePipeline) void {
self.shader.bind();
}
};
pub const SSBO = struct { // MARK: SSBO
bufferID: c_uint,
pub fn init() SSBO {
var self = SSBO{.bufferID = undefined};
c.glGenBuffers(1, &self.bufferID);
return self;
}
pub fn initStatic(comptime T: type, data: []const T) SSBO {
var self = SSBO{.bufferID = undefined};
c.glGenBuffers(1, &self.bufferID);
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID);
c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, 0);
return self;
}
pub fn initStaticSize(comptime T: type, len: usize) SSBO {
var self = SSBO{.bufferID = undefined};
c.glGenBuffers(1, &self.bufferID);
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID);
c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, @intCast(len*@sizeOf(T)), null, 0);
return self;
}
pub fn deinit(self: SSBO) void {
c.glDeleteBuffers(1, &self.bufferID);
}
pub fn bind(self: SSBO, binding: c_uint) void {
c.glBindBufferBase(c.GL_SHADER_STORAGE_BUFFER, binding, self.bufferID);
}
pub fn bufferData(self: SSBO, comptime T: type, data: []const T) void {
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID);
c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(data.len*@sizeOf(T)), data.ptr, c.GL_STATIC_DRAW);
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0);
}
pub fn createDynamicBuffer(self: SSBO, size: usize) void {
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID);
c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size), null, c.GL_DYNAMIC_DRAW);
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0);
}
};
pub const SubAllocation = struct {
start: u31,
len: u31,
};
/// A big SSBO that is able to allocate/free smaller regions.
pub fn LargeBuffer(comptime Entry: type) type { // MARK: LargerBuffer
return struct {
ssbo: SSBO,
freeBlocks: main.List(SubAllocation),
fences: [3]c.GLsync,
fencedFreeLists: [3]main.List(SubAllocation),
activeFence: u8,
capacity: u31,
used: u31,
binding: c_uint,
const Self = @This();
fn createBuffer(self: *Self, size: u31) void {
self.ssbo = .init();
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID);
const flags = c.GL_MAP_WRITE_BIT | c.GL_DYNAMIC_STORAGE_BIT;
const bytes = @as(c.GLsizeiptr, size)*@sizeOf(Entry);
c.glBufferStorage(c.GL_SHADER_STORAGE_BUFFER, bytes, null, flags);
self.ssbo.bind(self.binding);
self.capacity = size;
}
pub fn init(self: *Self, allocator: NeverFailingAllocator, size: u31, binding: c_uint) void {
self.binding = binding;
self.createBuffer(size);
self.activeFence = 0;
for(&self.fences) |*fence| {
fence.* = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
for(&self.fencedFreeLists) |*list| {
list.* = .init(allocator);
}
self.freeBlocks = .init(allocator);
self.freeBlocks.append(.{.start = 0, .len = size});
}
pub fn deinit(self: *Self) void {
for(self.fences) |fence| {
c.glDeleteSync(fence);
}
for(self.fencedFreeLists) |list| {
list.deinit();
}
self.ssbo.deinit();
self.freeBlocks.deinit();
}
pub fn beginRender(self: *Self) void {
self.activeFence += 1;
if(self.activeFence == self.fences.len) self.activeFence = 0;
const startTime = std.time.milliTimestamp();
while(self.fencedFreeLists[self.activeFence].popOrNull()) |allocation| {
self.finalFree(allocation);
if(std.time.milliTimestamp() -% startTime > 5) break; // TODO: Remove after #1434
}
self.fencedFreeLists[self.activeFence].clearRetainingCapacity();
_ = c.glClientWaitSync(self.fences[self.activeFence], 0, c.GL_TIMEOUT_IGNORED); // Make sure the render calls that accessed these parts of the buffer have finished.
}
pub fn endRender(self: *Self) void {
c.glDeleteSync(self.fences[self.activeFence]);
self.fences[self.activeFence] = c.glFenceSync(c.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
pub fn rawAlloc(self: *Self, size: u31) SubAllocation {
var smallestBlock: ?*SubAllocation = null;
for(self.freeBlocks.items, 0..) |*block, i| {
if(size == block.len) {
self.used += size;
return self.freeBlocks.swapRemove(i);
}
if(size < block.len and if(smallestBlock) |_smallestBlock| block.len < _smallestBlock.len else true) {
smallestBlock = block;
}
}
if(smallestBlock) |block| {
const result = SubAllocation{.start = block.start, .len = size};
block.start += size;
block.len -= size;
self.used += size;
return result;
} else {
std.log.info("Resizing internal mesh buffer from {} MiB to {} MiB", .{@as(usize, self.capacity)*@sizeOf(Entry) >> 20, (@as(usize, self.capacity)*@sizeOf(Entry) >> 20)*2});
const oldBuffer = self.ssbo;
defer oldBuffer.deinit();
const oldCapacity = self.capacity;
self.createBuffer(self.capacity*|2); // TODO: Is there a way to free the old buffer before creating the new one?
if(self.capacity == oldCapacity) @panic("Not enough addressable GPU memory available.");
self.used += self.capacity - oldCapacity;
self.finalFree(.{.start = oldCapacity, .len = self.capacity - oldCapacity});
c.glBindBuffer(c.GL_COPY_READ_BUFFER, oldBuffer.bufferID);
c.glBindBuffer(c.GL_COPY_WRITE_BUFFER, self.ssbo.bufferID);
c.glCopyBufferSubData(c.GL_COPY_READ_BUFFER, c.GL_COPY_WRITE_BUFFER, 0, 0, @as(c.GLsizeiptr, oldCapacity)*@sizeOf(Entry));
return rawAlloc(self, size);
}
}
fn finalFree(self: *Self, _allocation: SubAllocation) void {
if(_allocation.len == 0) return;
self.used -= _allocation.len;
var allocation = _allocation;
for(self.freeBlocks.items, 0..) |*block, i| {
if(allocation.start + allocation.len == block.start) {
allocation.len += block.len;
_ = self.freeBlocks.swapRemove(i);
break;
}
}
for(self.freeBlocks.items) |*block| {
if(allocation.start == block.start + block.len) {
block.len += allocation.len;
return;
}
}
self.freeBlocks.append(allocation);
}
pub fn free(self: *Self, allocation: SubAllocation) void {
if(allocation.len == 0) return;
self.fencedFreeLists[self.activeFence].append(allocation);
}
/// Must unmap after use!
pub fn allocateAndMapRange(self: *Self, len: usize, allocation: *SubAllocation) []Entry {
self.free(allocation.*);
if(len == 0) {
allocation.len = 0;
return &.{};
}
allocation.* = self.rawAlloc(@intCast(len));
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID);
const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange(
c.GL_SHADER_STORAGE_BUFFER,
@as(c.GLintptr, allocation.start)*@sizeOf(Entry),
@as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry),
c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT,
)));
return ptr[0..len];
}
pub fn unmapRange(self: *Self, range: []Entry) void {
if(range.len == 0) return;
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID);
std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE);
}
pub fn uploadData(self: *Self, data: []const Entry, allocation: *SubAllocation) void {
self.free(allocation.*);
if(data.len == 0) {
allocation.len = 0;
return;
}
allocation.* = self.rawAlloc(@intCast(data.len));
c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.ssbo.bufferID);
const ptr: [*]Entry = @ptrCast(@alignCast(c.glMapBufferRange(
c.GL_SHADER_STORAGE_BUFFER,
@as(c.GLintptr, allocation.start)*@sizeOf(Entry),
@as(c.GLsizeiptr, allocation.len)*@sizeOf(Entry),
c.GL_MAP_WRITE_BIT | c.GL_MAP_INVALIDATE_RANGE_BIT,
)));
@memcpy(ptr, data);
std.debug.assert(c.glUnmapBuffer(c.GL_SHADER_STORAGE_BUFFER) == c.GL_TRUE);
}
};
}
pub const FrameBuffer = struct { // MARK: FrameBuffer
frameBuffer: c_uint,
texture: c_uint,
hasDepthTexture: bool,
depthTexture: c_uint,
pub fn init(self: *FrameBuffer, hasDepthTexture: bool, textureFilter: c_int, textureWrap: c_int) void {
self.* = FrameBuffer{
.frameBuffer = undefined,
.texture = undefined,
.depthTexture = undefined,
.hasDepthTexture = hasDepthTexture,
};
c.glGenFramebuffers(1, &self.frameBuffer);
c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer);
if(hasDepthTexture) {
c.glGenTextures(1, &self.depthTexture);
c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap);
c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_DEPTH_ATTACHMENT, c.GL_TEXTURE_2D, self.depthTexture, 0);
}
c.glGenTextures(1, &self.texture);
c.glBindTexture(c.GL_TEXTURE_2D, self.texture);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, textureFilter);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, textureFilter);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, textureWrap);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, textureWrap);
c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, c.GL_TEXTURE_2D, self.texture, 0);
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
}
pub fn deinit(self: *FrameBuffer) void {
c.glDeleteFramebuffers(1, &self.frameBuffer);
if(self.hasDepthTexture) {
c.glDeleteRenderbuffers(1, &self.depthTexture);
}
c.glDeleteTextures(1, &self.texture);
}
pub fn updateSize(self: *FrameBuffer, _width: u31, _height: u31, internalFormat: c_int) void {
const width = @max(_width, 1);
const height = @max(_height, 1);
c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer);
if(self.hasDepthTexture) {
c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture);
c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_DEPTH_COMPONENT32F, width, height, 0, c.GL_DEPTH_COMPONENT, c.GL_FLOAT, null);
}
c.glBindTexture(c.GL_TEXTURE_2D, self.texture);
c.glTexImage2D(c.GL_TEXTURE_2D, 0, internalFormat, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
}
pub fn clear(_: FrameBuffer, clearColor: Vec4f) void {
c.glDepthFunc(c.GL_LESS);
c.glDepthMask(c.GL_TRUE);
c.glDisable(c.GL_SCISSOR_TEST);
c.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
c.glClear(c.GL_COLOR_BUFFER_BIT | c.GL_DEPTH_BUFFER_BIT);
}
pub fn validate(self: *const FrameBuffer) bool {
c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer);
defer c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
if(c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER) != c.GL_FRAMEBUFFER_COMPLETE) {
std.log.err("Frame Buffer Object error: {}", .{c.glCheckFramebufferStatus(c.GL_FRAMEBUFFER)});
return false;
}
return true;
}
pub fn bindTexture(self: *const FrameBuffer, target: c_uint) void {
c.glActiveTexture(target);
c.glBindTexture(c.GL_TEXTURE_2D, self.texture);
}
pub fn bindDepthTexture(self: *const FrameBuffer, target: c_uint) void {
std.debug.assert(self.hasDepthTexture);
c.glActiveTexture(target);
c.glBindTexture(c.GL_TEXTURE_2D, self.depthTexture);
}
pub fn bind(self: *const FrameBuffer) void {
c.glBindFramebuffer(c.GL_FRAMEBUFFER, self.frameBuffer);
}
pub fn unbind(_: *const FrameBuffer) void {
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
}
};
pub const TextureArray = struct { // MARK: TextureArray
textureID: c_uint,
pub fn init() TextureArray {
var self: TextureArray = undefined;
c.glGenTextures(1, &self.textureID);
return self;
}
pub fn deinit(self: TextureArray) void {
c.glDeleteTextures(1, &self.textureID);
}
pub fn bind(self: TextureArray) void {
c.glBindTexture(c.GL_TEXTURE_2D_ARRAY, self.textureID);
}
fn lodColorInterpolation(colors: [4]Color, alphaCorrection: bool) Color {
var r: [4]f32 = undefined;
var g: [4]f32 = undefined;
var b: [4]f32 = undefined;
var a: [4]f32 = undefined;
for(0..4) |i| {
r[i] = @floatFromInt(colors[i].r);
g[i] = @floatFromInt(colors[i].g);
b[i] = @floatFromInt(colors[i].b);
a[i] = @floatFromInt(colors[i].a);
}
// Use gamma corrected average(https://stackoverflow.com/a/832314/13082649):
var aSum: f32 = 0;
var rSum: f32 = 0;
var gSum: f32 = 0;
var bSum: f32 = 0;
for(0..4) |i| {
const w = if(alphaCorrection) a[i]*a[i] else 1;
aSum += a[i]*a[i];
rSum += w*r[i]*r[i];
gSum += w*g[i]*g[i];
bSum += w*b[i]*b[i];
}
aSum = @sqrt(aSum)/2;
rSum = @sqrt(rSum)/2;
gSum = @sqrt(gSum)/2;
bSum = @sqrt(bSum)/2;
if(alphaCorrection and aSum != 0) {
rSum /= aSum;
gSum /= aSum;
bSum /= aSum;
}
return Color{.r = @intFromFloat(rSum), .g = @intFromFloat(gSum), .b = @intFromFloat(bSum), .a = @intFromFloat(aSum)};
}
/// (Re-)Generates the GPU buffer.
pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void {
var maxWidth: u31 = 0;
var maxHeight: u31 = 0;
for(images) |image| {
maxWidth = @max(maxWidth, image.width);
maxHeight = @max(maxHeight, image.height);
}
// Make sure the width and height use a power of 2:
if(maxWidth - 1 & maxWidth != 0) {
maxWidth = @as(u31, 2) << std.math.log2_int(u31, maxWidth);
}
if(maxHeight - 1 & maxHeight != 0) {
maxHeight = @as(u31, 2) << std.math.log2_int(u31, maxHeight);
}
std.log.debug("Creating Texture Array of size {}×{} with {} layers.", .{maxWidth, maxHeight, images.len});
self.bind();
const maxLOD = if(mipmapping) 1 + std.math.log2_int(u31, @min(maxWidth, maxHeight)) else 1;
for(0..maxLOD) |i| {
c.glTexImage3D(c.GL_TEXTURE_2D_ARRAY, @intCast(i), c.GL_RGBA8, @max(0, maxWidth >> @intCast(i)), @max(0, maxHeight >> @intCast(i)), @intCast(images.len), 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
}
var arena = main.heap.NeverFailingArenaAllocator.init(main.stackAllocator);
defer arena.deinit();
const lodBuffer: [][]Color = arena.allocator().alloc([]Color, maxLOD);
for(lodBuffer, 0..) |*buffer, i| {
buffer.* = arena.allocator().alloc(Color, (maxWidth >> @intCast(i))*(maxHeight >> @intCast(i)));
}
for(images, 0..) |image, i| {
// Fill the buffer using nearest sampling. Probably not the best solutions for all textures, but that's what happens when someone doesn't use power of 2 textures...
for(0..maxWidth) |x| {
for(0..maxHeight) |y| {
const index = x + y*maxWidth;
const imageIndex = (x*image.width)/maxWidth + image.width*((y*image.height)/maxHeight);
lodBuffer[0][index] = image.imageData[imageIndex];
}
}
// Calculate the mipmap levels:
for(0..lodBuffer.len) |_lod| {
const lod: u5 = @intCast(_lod);
const curWidth = maxWidth >> lod;
const curHeight = maxHeight >> lod;
if(lod != 0) {
for(0..curWidth) |x| {
for(0..curHeight) |y| {
const index = x + y*curWidth;
const index2 = 2*x + 2*y*2*curWidth;
const colors = [4]Color{
lodBuffer[lod - 1][index2],
lodBuffer[lod - 1][index2 + 1],
lodBuffer[lod - 1][index2 + curWidth*2],
lodBuffer[lod - 1][index2 + curWidth*2 + 1],
};
lodBuffer[lod][index] = lodColorInterpolation(colors, alphaCorrectMipmapping);
}
}
}
}
// Give the correct color to alpha 0 pixels, to avoid dark pixels:
for(1..lodBuffer.len) |_lod| {
const lod: u5 = @intCast(lodBuffer.len - 1 - _lod);
const curWidth = maxWidth >> lod;
const curHeight = maxHeight >> lod;
for(0..curWidth) |x| {
for(0..curHeight) |y| {
const index = x + y*curWidth;
const index2 = x/2 + y/2*curWidth/2;
if(lodBuffer[lod][index].a == 0) {
lodBuffer[lod][index].r = lodBuffer[lod + 1][index2].r;
lodBuffer[lod][index].g = lodBuffer[lod + 1][index2].g;
lodBuffer[lod][index].b = lodBuffer[lod + 1][index2].b;
}
}
}
}
// Upload:
for(0..lodBuffer.len) |_lod| {
const lod: u5 = @intCast(lodBuffer.len - 1 - _lod);
const curWidth = maxWidth >> lod;
const curHeight = maxHeight >> lod;
c.glTexSubImage3D(c.GL_TEXTURE_2D_ARRAY, lod, 0, 0, @intCast(i), curWidth, curHeight, 1, c.GL_RGBA, c.GL_UNSIGNED_BYTE, lodBuffer[lod].ptr);
}
}
c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAX_LOD, maxLOD);
//glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST_MIPMAP_LINEAR);
c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT);
c.glTexParameteri(c.GL_TEXTURE_2D_ARRAY, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT);
}
};
pub const Texture = struct { // MARK: Texture
textureID: c_uint,
pub fn init() Texture {
var self: Texture = undefined;
c.glGenTextures(1, &self.textureID);
return self;
}
pub fn initFromFile(path: []const u8) Texture {
const self = Texture.init();
const image = Image.readFromFile(main.stackAllocator, path) catch |err| blk: {
std.log.err("Couldn't read image from {s}: {s}", .{path, @errorName(err)});
break :blk Image.defaultImage;
};
defer image.deinit(main.stackAllocator);
self.generate(image);
return self;
}
pub fn deinit(self: Texture) void {
c.glDeleteTextures(1, &self.textureID);
}
pub fn bindTo(self: Texture, binding: u5) void {
c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding));
c.glBindTexture(c.GL_TEXTURE_2D, self.textureID);
}
pub fn bind(self: Texture) void {
c.glBindTexture(c.GL_TEXTURE_2D, self.textureID);
}
/// (Re-)Generates the GPU buffer.
pub fn generate(self: Texture, image: Image) void {
self.bind();
c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_RGBA8, image.width, image.height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, image.imageData.ptr);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT);
}
pub fn render(self: Texture, pos: Vec2f, dim: Vec2f) void {
self.bindTo(0);
draw.boundImage(pos, dim);
}
pub fn size(self: Texture) Vec2i {
self.bind();
var result: Vec2i = undefined;
c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_WIDTH, &result[0]);
c.glGetTexLevelParameteriv(c.GL_TEXTURE_2D, 0, c.GL_TEXTURE_HEIGHT, &result[1]);
return result;
}
};
pub const CubeMapTexture = struct { // MARK: CubeMapTexture
textureID: c_uint,
pub fn init() CubeMapTexture {
var self: CubeMapTexture = undefined;
c.glGenTextures(1, &self.textureID);
return self;
}
pub fn deinit(self: CubeMapTexture) void {
c.glDeleteTextures(1, &self.textureID);
}
pub fn bindTo(self: CubeMapTexture, binding: u5) void {
c.glActiveTexture(@intCast(c.GL_TEXTURE0 + binding));
c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID);
}
pub fn bind(self: CubeMapTexture) void {
c.glBindTexture(c.GL_TEXTURE_CUBE_MAP, self.textureID);
}
/// (Re-)Generates the GPU buffer.
pub fn generate(self: CubeMapTexture, width: u31, height: u31) void {
self.bind();
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexImage2D(c.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, c.GL_RGBA8, width, height, 0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, null);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_WRAP_R, c.GL_CLAMP_TO_EDGE);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_BASE_LEVEL, 0);
c.glTexParameteri(c.GL_TEXTURE_CUBE_MAP, c.GL_TEXTURE_MAX_LEVEL, 0);
}
pub fn faceNormal(face: usize) Vec3f {
const normals = [_]Vec3f{
.{1, 0, 0}, // +x
.{-1, 0, 0}, // -x
.{0, 1, 0}, // +y
.{0, -1, 0}, // -y
.{0, 0, 1}, // +z
.{0, 0, -1}, // -z
};
return normals[face];
}
pub fn faceUp(face: usize) Vec3f {
const ups = [_]Vec3f{
.{0, -1, 0}, // +x
.{0, -1, 0}, // -x
.{0, 0, 1}, // +y
.{0, 0, -1}, // -y
.{0, -1, 0}, // +z
.{0, -1, 0}, // -z
};
return ups[face];
}
pub fn faceRight(face: usize) Vec3f {
comptime var rights: [6]Vec3f = undefined;
inline for(0..6) |i| {
rights[i] = comptime vec.cross(faceNormal(i), faceUp(i));
}
return rights[face];
}
pub fn bindToFramebuffer(self: CubeMapTexture, fb: FrameBuffer, face: c_uint) void {
fb.bind();
c.glFramebufferTexture2D(c.GL_FRAMEBUFFER, c.GL_COLOR_ATTACHMENT0, @as(c_uint, c.GL_TEXTURE_CUBE_MAP_POSITIVE_X) + face, self.textureID, 0);
}
};
pub const Color = extern struct { // MARK: Color
r: u8,
g: u8,
b: u8,
a: u8,
pub fn toARBG(self: Color) u32 {
return @as(u32, self.a) << 24 | @as(u32, self.r) << 16 | @as(u32, self.g) << 8 | @as(u32, self.b);
}
};
pub const Image = struct { // MARK: Image
var defaultImageData = [4]Color{
Color{.r = 0, .g = 0, .b = 0, .a = 255},
Color{.r = 255, .g = 0, .b = 255, .a = 255},
Color{.r = 255, .g = 0, .b = 255, .a = 255},
Color{.r = 0, .g = 0, .b = 0, .a = 255},
};
pub const defaultImage = Image{
.width = 2,
.height = 2,
.imageData = &defaultImageData,
};
var emptyImageData = [1]Color{
Color{.r = 0, .g = 0, .b = 0, .a = 0},
};
pub const emptyImage = Image{
.width = 1,
.height = 1,
.imageData = &emptyImageData,
};
var whiteImageData = [1]Color{
Color{.r = 255, .g = 255, .b = 255, .a = 255},
};
pub const whiteEmptyImage = Image{
.width = 1,
.height = 1,
.imageData = &whiteImageData,
};
width: u31,
height: u31,
imageData: []Color,
pub fn init(allocator: NeverFailingAllocator, width: u31, height: u31) Image {
return Image{
.width = width,
.height = height,
.imageData = allocator.alloc(Color, width*height),
};
}
pub fn deinit(self: Image, allocator: NeverFailingAllocator) void {
if(self.imageData.ptr == &defaultImageData or self.imageData.ptr == &emptyImageData or self.imageData.ptr == &whiteImageData) return;
allocator.free(self.imageData);
}
pub fn readFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image {
var result: Image = undefined;
var channel: c_int = undefined;
const nullTerminatedPath = std.fmt.allocPrintZ(main.stackAllocator.allocator, "{s}", .{path}) catch unreachable; // TODO: Find a more zig-friendly image loading library.
errdefer main.stackAllocator.free(nullTerminatedPath);
stb_image.stbi_set_flip_vertically_on_load(1);
const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse {
return error.FileNotFound;
};
main.stackAllocator.free(nullTerminatedPath);
result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]);
stb_image.stbi_image_free(data);
return result;
}
pub fn readUnflippedFromFile(allocator: NeverFailingAllocator, path: []const u8) !Image {
var result: Image = undefined;
var channel: c_int = undefined;
const nullTerminatedPath = std.fmt.allocPrintZ(main.stackAllocator.allocator, "{s}", .{path}) catch unreachable; // TODO: Find a more zig-friendly image loading library.
errdefer main.stackAllocator.free(nullTerminatedPath);
const data = stb_image.stbi_load(nullTerminatedPath.ptr, @ptrCast(&result.width), @ptrCast(&result.height), &channel, 4) orelse {
return error.FileNotFound;
};
main.stackAllocator.free(nullTerminatedPath);
result.imageData = allocator.dupe(Color, @as([*]Color, @ptrCast(data))[0 .. result.width*result.height]);
stb_image.stbi_image_free(data);
return result;
}
pub fn exportToFile(self: Image, path: []const u8) !void {
const nullTerminated = main.stackAllocator.dupeZ(u8, path);
defer main.stackAllocator.free(nullTerminated);
_ = stb_image.stbi_write_png(nullTerminated.ptr, self.width, self.height, 4, self.imageData.ptr, self.width*4); // TODO: Handle the return type.
}
pub fn getRGB(self: Image, x: usize, y: usize) Color {
std.debug.assert(x < self.width);
std.debug.assert(y < self.height);
const index = x + y*self.width;
return self.imageData[index];
}
pub fn setRGB(self: Image, x: usize, y: usize, rgb: Color) void {
std.debug.assert(x < self.width);
std.debug.assert(y < self.height);
const index = x + y*self.width;
self.imageData[index] = rgb;
}
};
pub const Fog = struct { // MARK: Fog
fogColor: Vec3f,
skyColor: Vec3f,
density: f32,
fogLower: f32,
fogHigher: f32,
};
const block_texture = struct { // MARK: block_texture
var uniforms: struct {
transparent: c_int,
} = undefined;
var pipeline: Pipeline = undefined;
var depthTexture: Texture = undefined;
const textureSize = 128;
fn init() void {
pipeline = Pipeline.init(
"assets/cubyz/shaders/item_texture_post.vert",
"assets/cubyz/shaders/item_texture_post.frag",
"",
&uniforms,
.{.cullMode = .none},
.{.depthTest = false, .depthWrite = false},
.{.attachments = &.{.noBlending}},
);
depthTexture = .init();
depthTexture.bind();
var data: [128*128]f32 = undefined;
const z: f32 = 134;
const near = main.renderer.zNear;
const far = main.renderer.zFar;
const depth = ((far + near)/(near - far)*-z + 2*near*far/(near - far))/z*0.5 + 0.5;
@memset(&data, depth);
c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R32F, textureSize, textureSize, 0, c.GL_RED, c.GL_FLOAT, &data);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_NEAREST);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_REPEAT);
c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_REPEAT);
}
fn deinit() void {
pipeline.deinit();
depthTexture.deinit();
}
};
pub fn generateBlockTexture(blockType: u16) Texture {
const block = main.blocks.Block{.typ = blockType, .data = 0}; // TODO: Use natural standard data.
const textureSize = block_texture.textureSize;
c.glViewport(0, 0, textureSize, textureSize);
var frameBuffer: FrameBuffer = undefined;
frameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT);
defer frameBuffer.deinit();
frameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA16F);
frameBuffer.bind();
if(block.transparent()) {
frameBuffer.clear(.{0.683421, 0.6854237, 0.685426, 1});
} else {
frameBuffer.clear(.{0, 0, 0, 0});
}
const projMatrix = Mat4f.perspective(0.013, 1, 64, 256);
const oldViewMatrix = main.game.camera.viewMatrix;
main.game.camera.viewMatrix = Mat4f.identity().mul(Mat4f.rotationX(std.math.pi/4.0)).mul(Mat4f.rotationZ(-5.0*std.math.pi/4.0));
defer main.game.camera.viewMatrix = oldViewMatrix;
const uniforms = if(block.transparent()) &main.renderer.chunk_meshing.transparentUniforms else &main.renderer.chunk_meshing.uniforms;
var faceData: main.ListUnmanaged(main.renderer.chunk_meshing.FaceData) = .{};
defer faceData.deinit(main.stackAllocator);
const model = main.blocks.meshes.model(block).model();
if(block.hasBackFace()) {
model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, 1, 1, 1, true);
for(main.chunk.Neighbor.iterable) |neighbor| {
model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, 1, 1, 1, true);
}
}
model.appendInternalQuadsToList(&faceData, main.stackAllocator, block, 1, 1, 1, false);
for(main.chunk.Neighbor.iterable) |neighbor| {
model.appendNeighborFacingQuadsToList(&faceData, main.stackAllocator, block, neighbor, 1 + neighbor.relX(), 1 + neighbor.relY(), 1 + neighbor.relZ(), false);
}
for(faceData.items) |*face| {
face.position.lightIndex = 0;
}
var allocation: SubAllocation = .{.start = 0, .len = 0};
main.renderer.chunk_meshing.faceBuffer.uploadData(faceData.items, &allocation);
var lightAllocation: SubAllocation = .{.start = 0, .len = 0};
main.renderer.chunk_meshing.lightBuffer.uploadData(&.{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, &lightAllocation);
{
const i = 6; // Easily switch between the 8 rotations.
var x: f64 = -65.5 + 1.5;
var y: f64 = -65.5 + 1.5;
var z: f64 = -92.631 + 1.5;
if(i & 1 != 0) x = -x + 3;
if(i & 2 != 0) y = -y + 3;
if(i & 4 != 0) z = -z + 3;
var chunkAllocation: SubAllocation = .{.start = 0, .len = 0};
main.renderer.chunk_meshing.chunkBuffer.uploadData(&.{.{
.position = .{0, 0, 0},
.min = undefined,
.max = undefined,
.voxelSize = 1,
.lightStart = lightAllocation.start,
.vertexStartOpaque = undefined,
.faceCountsByNormalOpaque = undefined,
.vertexStartTransparent = undefined,
.vertexCountTransparent = undefined,
.visibilityState = 0,
.oldVisibilityState = 0,
}}, &chunkAllocation);
defer main.renderer.chunk_meshing.chunkBuffer.free(chunkAllocation);
if(block.transparent()) {
c.glBlendEquation(c.GL_FUNC_ADD);
c.glBlendFunc(c.GL_ONE, c.GL_SRC1_COLOR);
main.renderer.chunk_meshing.bindTransparentShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z});
} else {
main.renderer.chunk_meshing.bindShaderAndUniforms(projMatrix, .{1, 1, 1}, .{x, y, z});
}
c.glUniform1f(uniforms.contrast, 0.25);
c.glActiveTexture(c.GL_TEXTURE0);
main.blocks.meshes.blockTextureArray.bind();
c.glActiveTexture(c.GL_TEXTURE1);
main.blocks.meshes.emissionTextureArray.bind();
c.glActiveTexture(c.GL_TEXTURE2);
main.blocks.meshes.reflectivityAndAbsorptionTextureArray.bind();
block_texture.depthTexture.bindTo(5);
c.glDrawElementsInstancedBaseVertexBaseInstance(c.GL_TRIANGLES, @intCast(6*faceData.items.len), c.GL_UNSIGNED_INT, null, 1, allocation.start*4, chunkAllocation.start);
}
c.glDisable(c.GL_CULL_FACE);
var finalFrameBuffer: FrameBuffer = undefined;
finalFrameBuffer.init(false, c.GL_NEAREST, c.GL_REPEAT);
finalFrameBuffer.updateSize(textureSize, textureSize, c.GL_RGBA8);
finalFrameBuffer.bind();
const texture = Texture{.textureID = finalFrameBuffer.texture};
defer c.glDeleteFramebuffers(1, &finalFrameBuffer.frameBuffer);
block_texture.pipeline.bind(null);
c.glUniform1i(block_texture.uniforms.transparent, if(block.transparent()) c.GL_TRUE else c.GL_FALSE);
frameBuffer.bindTexture(c.GL_TEXTURE3);
c.glBindVertexArray(draw.rectVAO);
c.glDrawArrays(c.GL_TRIANGLE_STRIP, 0, 4);
c.glBindFramebuffer(c.GL_FRAMEBUFFER, 0);
main.renderer.chunk_meshing.faceBuffer.free(allocation);
c.glViewport(0, 0, main.Window.width, main.Window.height);
c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
return texture;
}