Need help fixing OpenGL renderer

I’m working on a game engine project with a custom data structure list thing based off of the std.MultiArrayList (basically a multiarraylist sparse set, I’m thinking about making a separate thread getting feedback on it as I plan to stick with it until any serious issues come up). Once I changed all my data over to using these lists, I noticed some strange behavior with the rendering (note: I do not know if this was a problem before as I switched to this new data structure before ever testing removing objects). Two things are happening:

  1. When I use my remove function, say at index 10, whatever object is at half of that index (5) will also disappear when rendering (the data for index 5 is still there, it just doesn’t render on to screen) (image 1).

  2. (the far bigger problem) around half of the objects I add to the list just don’t render at all (image 2, there should be 11 total) (to get the screenshot for #1 I had to append far more than 10 objects, like 20-30).

I do not believe it has anything to do with my sparse set implementation as the stored data seems to be as expected in these conditions (I can confirm this after printing all objects in the list, and printing the vertices/indices that go through functions like updateBuffers() for both problem #1 and #2).

Relevant code (can provide more if requested):

main.zig

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    try window.init();
    defer window.deinit();

    try renderer.init();
    defer renderer.deinit(allocator);

    for (0..10) |i| {
        const delta: f32 = @floatFromInt(i);
        _ = try renderer.newObject(
            allocator,
            &[_]renderer.Vertex{
                .{ .position = .{ -0.5, 0, 0.5 }, .color = .{ 1, 0, 0 }, .tex_coord = .{ 0, 0 } },
                .{ .position = .{ -0.5, 0, -0.5 }, .color = .{ 0, 1, 0 }, .tex_coord = .{ 0, 0 } },
                .{ .position = .{  0.5, 0, -0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
                .{ .position = .{  0.5, 0, 0.5 }, .color = .{ 1, 1, 0 }, .tex_coord = .{ 0, 0 } },
                .{ .position = .{  0.0, 0.8, 0 }, .color = .{ 1, 1, 1 }, .tex_coord = .{ 0, 0 } },
            },
            &[_]usize{ 
                0, 1, 2,
                0, 2, 3,
                0, 1, 4,
                1, 2, 4,
                2, 3, 4,
                3, 0, 4
            },
            .{ 0, 0 + delta, 0 },
            .{ 0 + (4*delta), 0, 0 },
        );
    }

    _ = try renderer.newObject(
        allocator,
        &[_]renderer.Vertex{
            .{ .position = .{ -0.5, 0, 0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
            .{ .position = .{ -0.5, 0, -0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
            .{ .position = .{  0.5, 0, -0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
            .{ .position = .{  0.5, 0, 0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
            .{ .position = .{  0.0, 0.8, 0 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
        },
        &[_]usize{ 
            0, 1, 2,
            0, 2, 3,
            0, 1, 4,
            1, 2, 4,
            2, 3, 4,
            3, 0, 4
        },
        .{ 0, 0, 0 },
        .{ 0, 0, 0 },
    );
    
    // temp player camera
    var cam = renderer.Camera{
        .position = .{ 0, -0.5, -5 },
        .rotation = .{ 0, 0, 0, 1 },
        .fov = 90,
        .near_plane = 0.001, // temp
        .far_plane = 10000, // temp
        .owner = null,
    };

    // update buffers
    try renderer.updateBuffers();

    // main loop
    try window.update(&cam);

    // cleanup
    try network.deinit(allocator);
}

updateBuffers()

pub fn updateBuffers() !void {
    const vertex_count: isize = @intCast(renderer.vertices.len);
    const index_count: isize = @intCast(renderer.indices.len);

    // not sure how performant this approach is, might change later

    // update the vertex buffer with the new vertices
    gl.BufferData(gl.ARRAY_BUFFER, @sizeOf(renderer.Vertex) * vertex_count, null, gl.DYNAMIC_DRAW);
    for (renderer.vertices.getOrder()) |i| {
        const vertex: renderer.Vertex = renderer.vertices.getAssertExists(i);
        gl.BufferSubData(gl.ARRAY_BUFFER, @sizeOf(renderer.Vertex) * @as(isize, @intCast(i)), @sizeOf(renderer.Vertex), &vertex);
    }

    // update the index buffer with the new indices
    gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(u32) * index_count, null, gl.DYNAMIC_DRAW);
    for (renderer.indices.getOrder()) |i| {
        const index: usize = renderer.indices.getAssertExists(i);
        gl.BufferSubData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(u32) * @as(isize, @intCast(i)), @sizeOf(u32), &index);
    }
}

update()

// setup model matrix for each mesh and draw the vertices
    for (renderer.objects.getOrder()) |i| {
        const object = renderer.objects.getAssertExists(i);

        var model: zmath.Mat = zmath.identity();
        
        // rot
        model = zmath.matFromRollPitchYaw(object.rotation[0] * (std.math.pi / 180.0), object.rotation[1] * (std.math.pi / 180.0), object.rotation[2] * (std.math.pi / 180.0)); // converts to quaternion under the hood

        // test local rotation
        model = zmath.mul(model, zmath.rotationY(delta_rotation * (std.math.pi / 180.0)));
        
        // pos
        model = zmath.mul(model, zmath.translation(object.position[0], object.position[1], object.position[2]));

        gl.UniformMatrix4fv(model_location, 1, gl.FALSE, zmath.arrNPtr(&model));
        gl.DrawElements(gl.TRIANGLES, @intCast(object.index_count), gl.UNSIGNED_INT, object.index_start * @sizeOf(usize)); // whyd the last argument change from *usize to usize????
    }

newObject()

pub fn newObject(allocator: std.mem.Allocator, new_vertices: []const Vertex, new_indices: []const usize, position: [3]f32, rotation: [3]f32) !Object {
    const object = Object {
        .position = position,
        .rotation = rotation,

        .vertex_start = @enumFromInt(vertices.len),
        .vertex_count = @intCast(new_vertices.len),

        .index_start = @intCast(indices.len),
        .index_count = @intCast(new_indices.len),

        .material = @enumFromInt(0), // not implemented yet
    };

    for (new_vertices) |vertex| {
        try vertices.append(allocator, vertex);
    }

    for (new_indices) |index| {
        try indices.append(allocator, index + @intFromEnum(object.vertex_start));
    }

    // add the object to the object list
    try objects.append(allocator, object);

    return object;
}

removeObject()

pub fn removeObject(index: usize) !void {
    const object: Object = try objects.get(index);

    for (@intFromEnum(object.vertex_start)..(@intFromEnum(object.vertex_start) + object.vertex_count)) |i| {
        try vertices.remove(i);
    }

    for (object.index_start..(object.index_start + object.index_count)) |i| {
        try indices.remove(i);
    }

    try objects.remove(index);
}

Also, another thing I’ve noticed is that the position data is behaving abnormally. For example, the first 10 objects I add will gradually increase vertically by 1 unit (as programmed, using a delta value in for loop) (again, only 5 of them render). Then once I add the 11th object (outside of the for loop, colored blue this time) its position just doesn’t matter at all. I can set the 11th object’s position to literally anything and it will not respect the value I give it. It will always be at the top of how ever many rendered.

If any more information or code would be helpful in figuring this out, please just let me know what specifically you want to see and I can reply with it.

Shouldn’t the last argument be a pointer to the memory that contains the array of indizies?

glDrawElements

I think you use a opengl binding, maybe that changes the signature and creates some kind of wrapper around the original opengl function?

Yes, so I had thought the same a while ago, but weirdly enough it turns out the wrapper I use (zigglgen) is not using a pointer for that parameter.


To clarify on the comment I added, I believe I used a different OpenGL wrapper at one point, and (after switching) the weird change zigglgen made caught me off guard.

When glDrawElements is called, it uses count sequential elements from an enabled array, starting at indices to construct a sequence of geometric primitives. mode specifies what kind of primitives are constructed and how the array elements construct these primitives. If more than one array is enabled, each is used.

Read starting from the heading: Element Buffer Objects

Basically you need to create an EBO upload your index data into that and then as the last parameter you put the ebo-id. I think that is how it works, but it has been a while since I wrote opengl code.

Ahh okay the last parameter should just be 0, it is the first index within the already setup index buffer that is used to start drawing primitives. Basically if you set the last parameter to 3 with GL_TRIANGLES as primitive then you skip the first triangle.

1 Like

OpenGL has a lot of obscure undefined behavior, and no feedback on what your’re doing wrong. I highly recommend switching to Vulkan.
With that said, your problem could be either in your set implementation or in how you’re using OpenGL. What you described sounds to me like not obeying the weird OpenGL memory layout. Did you check if you data needs to have padding at the end? OpenGL uses 16-byte alignment for a bunch of stuff, and Zig (or C) won’t use that alignment by default.

1 Like

Creating one call to gl.BufferSubData for every vertex/index is likely really bad for performance, normally you would just upload everything once and then possibly update sections that have changed if the array is big enough to warrant partial updates, or do full updates if the buffer is small and doesn’t get updated often.

1 Like

Also just a general tip https://renderdoc.org/ can be quite helpful when you are figuring out stuff with opengl.

2 Likes

(Edit: nevermind) Thank you, this fixed the objects not rendering. I had thought that the last parameter was supposed to be where the object’s indices started.

However, after changing it to a zero, the colors of the pyramids are off.

The top one should be blue, but the color does not change.

This is my current solution as my sparse set wrapper of std.MultiArrayList does not manage a slice of exclusively active objects in the list. Though, I can probably add a function to compute a slice of active elements on runtime for cases like this. This compute function would have to be O(n), but I’m assuming it would still be far better than constantly calling gl.BufferSubData.

Just for some context, the reason the last parameter is a usize and not a pointer is because glDrawElements behaves differently depending on if an element array buffer is bound or not:

  • If an element array buffer is bound, it’s a byte offset into the buffer, relative to the bound buffer.
  • If no element array buffer is bound, it’s a pointer to a client-side array which the draw operation will source data from.

However, the second behavior is only supported under the Compatibility profile and explicitly illegal under the Core profile. In other words, you must bind an element array buffer before calling glDrawElements. So I chose usize as the type override since it makes the most sense under Core (I don’t think a lot of people will be writing Zig code targeting Compatibility).

(The last parameter of the glVertexAttribPointer family of functions has a similar story.)

3 Likes

std.MultiArrayList has a capacity and a length, the length tells you what is the set of active elements.

I am not quite sure how you manage your vertex buffer, it seems like you have a vertex buffer where all attributes of a vertex are close to another in this style pos color pos color pos color

However MultiArrayList is for data that is stored like pos pos pos color color color for that memory layout it would be easier to define your vertex attributes non interleaved then you could update the whole buffer with one call because the layouts would match.

Alternatively you could switch from MultiArrayList to an ArrayList of Vertex that way both would be in interleaved style, this again would allow you to use just one call to update everything.

1 Like

Hey @castholm, thanks for the clarification, and for making zigglgen in general as it has made using OpenGL with zig very easy.

1 Like

Where would I change the attributes arrangement? Below is my OpenGL init code:

// link vertex attributes
const position_attrib: c_uint = @intCast(gl.GetAttribLocation(shader_program, "a_Position"));
gl.EnableVertexAttribArray(position_attrib);
gl.VertexAttribPointer(
    position_attrib,
    @typeInfo(@TypeOf(@as(renderer.Vertex, undefined).position)).Array.len,
    gl.FLOAT,
    gl.FALSE,
    @sizeOf(renderer.Vertex),
    @offsetOf(renderer.Vertex, "position"),
);

const color_attrib: c_uint = @intCast(gl.GetAttribLocation(shader_program, "a_Color"));
gl.EnableVertexAttribArray(color_attrib);
gl.VertexAttribPointer(
    color_attrib,
    @typeInfo(@TypeOf(@as(renderer.Vertex, undefined).color)).Array.len,
    gl.FLOAT,
    gl.FALSE,
    @sizeOf(renderer.Vertex),
    @offsetOf(renderer.Vertex, "color"),
);

Also, I’m more interested in accessing the active elements themselves than in the number of active elements. If there’s an easy way to access active elements in std.MultiArrayList while maintaining element positions, please let me know.

To clarify, the main benefits I am getting from the sparse set wrapper are:

  1. Constant dense array indices
  2. Reusing empty slots
  3. Iteration order is already stored in the sparse array, and I can iterate through it from [0..len] to get active elements’ indices in the dense array

Yes this is interleaved mode vertex attribute layout, this is useful when accessing individual verticies when you don’t have an upper bound of verticies, because for non interleaved layout within a single buffer you would have to know the amount of vertexes when setting up the vertex attributes.

I think for now the simplest thing for you would be to avoid MultiArrayList and just use a ArrayList(Vertex), because that would match your current vertex attribute layout.

I don’t know what you mean with “sparse set wrapper”, a MultiArrayList is n parallel arrays that can be accessed with SoA and AoS semantics depending on which functions you use and has SoA memory layout.

Do you mean something like this?: ECS back and forth

Yes, it is very similar to that.

I will just give you the full source code to my implementation if it helps:

/// The StorageList is a wrapper around std.ArrayListUnmanaged to support the sparse set data structure.
/// Note that sparse sets do not retain order once items are removed.
pub fn StorageList(comptime T: type) type {
    return struct {
        var list: std.ArrayListUnmanaged(T) = .{};
        var sparse_array: std.ArrayListUnmanaged(usize) = .{};
        var dense_to_sparse: std.ArrayListUnmanaged(usize) = .{};

        len: usize = 0,
        capacity: usize = 0,

        const Self = @This();

        /// Returns a slice of all the elements in a given field. Intended to be used for iteration (same as `std.ArrayListUnmanaged`).
        /// 
        /// Includes items that have been removed! Please use `checkExists()` to check if an index is valid on each iteration of the returned slice.
        pub fn items(self: Self) []T {
            _ = self;
            return list.items;
        }

        /// Sets the element at index `index` to `elem`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn set(self: *Self, index: usize, elem: T) !void {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            list.items[index] = elem;
        }

        /// Returns the element at index `index`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn get(self: Self, index: usize) !T {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            return list.items[index];
        }

        /// Returns the element at index `index`.
        /// Assumes that `index` is ALWAYS a valid index (in bounds).
        /// 
        /// Only use this if iterating over `getOrder()`'s returned slice.
        /// `getAssertExists()` removes the `checkExists()` call from `get`, as it is unnecessary if you are iterating over `getOrder()`'s returned slice.
        pub fn getAssertExists(self: Self, index: usize) T {
            _ = self;
            return list.items[index];
        }

        pub fn getPtr(self: *Self, index: usize) !*T {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            return &list.items[index];
        }

        /// Adds an element `elem` to the list.
        /// If a free slot is available, it will be used.
        /// Otherwise, it will grow the list as needed.
        pub fn append(self: *Self, allocator: std.mem.Allocator, elem: T) !void {
            if (self.len < self.capacity) {
                list.items[self.len] = elem;
                self.len += 1;
            } else {
                try list.append(allocator, elem);
                try sparse_array.append(allocator, self.len);
                try dense_to_sparse.append(allocator, self.len); // self.len is the same as sparse_array.items.len (which is what we would need)

                self.len += 1;
                self.capacity += 1;
            }
        }

        /// Removes the element at index `index`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn remove(self: *Self, index: usize) !void {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            const stored_dense_index = sparse_array.items[self.len - 1];
            const stored_sparse_index = sparse_array.items[dense_to_sparse.items[index]];

            sparse_array.items[dense_to_sparse.items[index]] = sparse_array.items[self.len - 1];
            sparse_array.items[self.len - 1] = stored_sparse_index;

            dense_to_sparse.items[stored_dense_index] = dense_to_sparse.items[index];
            dense_to_sparse.items[index] = self.len - 1;

            self.len -= 1;
        }

        /// Returns a slice of indices from the internal sparse array, and will not contain any indices that have been removed.
        /// Intended to be used for iteration.
        /// 
        /// For an example of iteration, see [NO DOCS YET].
        pub fn getOrder(self: Self) []usize {
            return sparse_array.items[0..self.len];
        }

        /// Frees memory from all removed slots and shrinks the list. This will completely reorder the dense array.
        pub fn shrinkAndFree(self: *Self, allocator: std.mem.Allocator) void {
            for (sparse_array.items[self.len..]) |i| {
                list.swapRemove(i);
            }
            list.shrinkAndFree(allocator, self.len);

            sparse_array.shrinkAndFree(allocator, self.len);
            dense_to_sparse.shrinkAndFree(allocator, self.len);
        }

        pub fn checkExists(self: Self, index: usize) bool {
            _ = self;

            if (sparse_array.items[dense_to_sparse.items[index]] == std.math.maxInt(usize)) {
                return false;
            } else {
                return true;
            }
        }

        pub fn deinit(self: Self,allocator: std.mem.Allocator) void {
            _ = self;

            list.deinit(allocator);
            sparse_array.deinit(allocator);
            dense_to_sparse.deinit(allocator);
        }
    };
}

/// The MultiStorageList is a wrapper around std.MultiArrayList to support the sparse set data structure.
/// Note that sparse sets do not retain order once items are removed.
pub fn MultiStorageList(comptime T: type) type {
    return struct {
        var list: std.MultiArrayList(T) = .{};
        var sparse_array: std.ArrayListUnmanaged(usize) = .{};
        var dense_to_sparse: std.ArrayListUnmanaged(usize) = .{};

        len: usize = 0,
        capacity: usize = 0,

        const Elem = switch (@typeInfo(T)) {
            .Struct => T,
            .Union => |u| struct {
                pub const Bare =
                    @Type(.{ .Union = .{
                    .layout = u.layout,
                    .tag_type = null,
                    .fields = u.fields,
                    .decls = &.{},
                } });
                pub const Tag =
                    u.tag_type orelse @compileError("MultiArrayList does not support untagged unions");
                tags: Tag,
                data: Bare,

                pub fn fromT(outer: T) @This() {
                    const tag = std.meta.activeTag(outer);
                    return .{
                        .tags = tag,
                        .data = switch (tag) {
                            inline else => |t| @unionInit(Bare, @tagName(t), @field(outer, @tagName(t))),
                        },
                    };
                }
                pub fn toT(tag: Tag, bare: Bare) T {
                    return switch (tag) {
                        inline else => |t| @unionInit(T, @tagName(t), @field(bare, @tagName(t))),
                    };
                }
            },
            else => @compileError("MultiArrayList only supports structs and tagged unions"),
        };

        pub const Field = std.meta.FieldEnum(Elem);

        const Self = @This();

        /// Returns a slice of all the elements in a given field. Intended to be used for iteration (same as `std.MultiArrayList`).
        /// 
        /// Includes items that have been removed! Please use `checkExists()` to check if an index is valid on each iteration of the returned slice (only needed once as it covers all fields).
        pub fn items(self: Self, comptime field: Field) []std.meta.fieldInfo(Elem, field).type {
            _ = self;
            return list.items(field);
        }

        /// Sets the element at index `index` to `elem`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn set(self: *Self, index: usize, elem: T) !void {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            list.set(index, elem);
        }

        /// Returns the element at index `index`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn get(self: Self, index: usize) !T {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            return list.get(index);
        }

        /// Returns the element at index `index`.
        /// Assumes that `index` is ALWAYS a valid index (in bounds).
        /// 
        /// Only use this if iterating over `getOrder()`'s returned slice.
        /// `getAssertExists()` removes the `checkExists()` call from `get`, as it is unnecessary if you are iterating over `getOrder()`'s returned slice.
        pub fn getAssertExists(self: Self, index: usize) T {
            _ = self;
            return list.get(index);
        }

        // TODO: getPtr()

        /// Adds an element `elem` to the list.
        /// If a free slot is available, it will be used.
        /// Otherwise, it will grow the list as needed.
        pub fn append(self: *Self, allocator: std.mem.Allocator, elem: T) !void {
            if (self.len < self.capacity) {
                list.set(self.len, elem);
                self.len += 1;
            } else {
                try list.append(allocator, elem);
                try sparse_array.append(allocator, self.len);
                try dense_to_sparse.append(allocator, self.len); // self.len is the same as sparse_array.items.len (which is what we would need)

                self.len += 1;
                self.capacity += 1;
            }
        }

        /// Removes the element at index `index`.
        /// Assumes that `index` is a valid index (in bounds), as it will error otherwise.
        pub fn remove(self: *Self, index: usize) !void {
            if (!self.checkExists(index)) {
                return StorageListError.OutOfBounds;
            }

            const stored_dense_index = sparse_array.items[self.len - 1];
            const stored_sparse_index = sparse_array.items[dense_to_sparse.items[index]];

            sparse_array.items[dense_to_sparse.items[index]] = sparse_array.items[self.len - 1];
            sparse_array.items[self.len - 1] = stored_sparse_index;

            dense_to_sparse.items[stored_dense_index] = dense_to_sparse.items[index];
            dense_to_sparse.items[index] = self.len - 1;

            self.len -= 1;
        }

        /// Returns a slice of indices from the internal sparse array, and will not contain any indices to items that have been removed.
        /// Intended to be used for iteration.
        /// 
        /// For an example of iteration, see [NO DOCS YET].
        pub fn getOrder(self: Self) []usize {
            return sparse_array.items[0..self.len];
        }

        /// Frees all "removed" items and shrinks the list. This will completely reorder the dense array.
        pub fn shrinkAndFree(self: *Self, allocator: std.mem.Allocator) void {
            for (sparse_array.items[self.len..]) |i| {
                list.swapRemove(i);
            }
            list.shrinkAndFree(allocator, self.len);

            sparse_array.shrinkAndFree(allocator, self.len);
            dense_to_sparse.shrinkAndFree(allocator, self.len);
        }

        pub fn checkExists(self: Self, index: usize) bool {
            _ = self;

            if (sparse_array.items[dense_to_sparse.items[index]] == std.math.maxInt(usize)) {
                return false;
            } else {
                return true;
            }
        }

        pub fn deinit(self: Self,allocator: std.mem.Allocator) void {
            _ = self;

            list.deinit(allocator);
            sparse_array.deinit(allocator);
            dense_to_sparse.deinit(allocator);
        }
    };
}

I would much rather stick to MultiArrayList (or similar, like my wrapper) because it makes working with SoA much easier, and I wouldn’t have to manually create and manage an ArrayList for each field of data.

1 Like

The thing I don’t understand is why changing the indices parameter lead to the vertex attribute arrangement needing to be changed.

It doesn’t need to be changed, but having a memory layout in ram that matches the vertex attribute layout would allow you to use less calls to gl.BufferSubData, if the layout matches exactly you need 1 call, you also could have situations where you use 1 call per attribute type and currently you have the worst case where you have one call per vertex.

Basically you want to hand over the data to the gpu in one big batch instead of tiny slices.

But that should only be a performance problem, the rendering should work anyway.

Sorry, I misinterpreted what you said. I thought you meant changing the layout would fix the color problem.

And thanks for the advice, I will definitely try to come up with a way to store it like that, or at least add a way to format it non-interleaved.

Any ideas on what might be the problem with the colors?

1 Like

I think you render 2 objects at position zero at the exact same position, one from the zeroth iteration of the for loop and one from the single blue one created afterwards.

My guess would be that either the non blue one gets rendered first and writes a depth value to the depth buffer which then causes all drawing from the blue one to be ignored (because it has the same depth instead of being in front)

Or the rendering is without depth comparison and happens in the reverse order so that the blue one gets overdrawn with the non-blue one.

What happens if you change this:

for (0..10) 

to:

for (1..11) 

?

Or alternatively what do you see when you only render the blue one and not the other ones?

I forgot to mention that I updated the position (after the initial fix) on the blue pyramid to 10 (the highest). I’ll attach an image of what it looks like at a vertical position of 11 (a gap in between the for-loop pyramids and the standalone, for clarity).

_ = try renderer.newObject(
    allocator,
    &[_]renderer.Vertex{
        .{ .position = .{ -0.5, 0, 0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
        .{ .position = .{ -0.5, 0, -0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
        .{ .position = .{  0.5, 0, -0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
        .{ .position = .{  0.5, 0, 0.5 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
        .{ .position = .{  0.0, 0.8, 0 }, .color = .{ 0, 0, 1 }, .tex_coord = .{ 0, 0 } },
    },
    &[_]usize{ 
        0, 1, 2,
        0, 2, 3,
        0, 1, 4,
        1, 2, 4,
        2, 3, 4,
        3, 0, 4
    },
    .{ 0, 11, 0 },
    .{ 0, 0, 0 },
);

You can see in the image that it does not have the rotation applied from the for-loop pyramids, but it also doesn’t have its new vertex colors like it should.

1 Like