u/ToastilyBreaded

971 post karma · 1,262 comment karma · joined Dec 2011

  1. 55points

    Working on a fire simulation. Build walls to block fire from spreading, especially in high winds! (v.redd.it) [selfpromo (games)]

    8 comments · source

  2. 179points

    Bonus: showcasing the dynamic voronoi graph (v.redd.it)

    6 comments · source

  3. 189points

    Procedural city building game progress: burn it down! (v.redd.it)

    4 comments · source

  4. 218points

    Procedural city building game progress: burn it down! (v.redd.it) [selfpromo (games)]

    9 comments · source

  5. 425points

    I'm prototyping a procedurally generated city building game (v.redd.it) [fun & memes]

    22 comments · source

ToastilyBreaded

It's for a procedural city building game: https://streamable.com/0gp8ce

ToastilyBreaded

This is what the game looks like so far: https://streamable.com/0gp8ce

ToastilyBreaded

Instead of locking anything, so long as new points used in the triangulation (i.e. centroids) are placed outside the associated circumcircles of "claimed" cells, those cells remain unchanged. At a high level it's this:

  1. Generate new points
  2. Filter out points inside any circumcircles associated with claimed cells
  3. Add back into to the set of points the centroids of claimed cells
  4. Regenerate the voronoi

This exactly regenerates the "claimed" cells every time which also results in providing a smooth connection to new cells. It's a super nice property of voronoi graphs!

ToastilyBreaded
ToastilyBreaded

Yes will try to tackle that next. Love the idea of walls stopping fire.

ToastilyBreaded

(3/3)

static func _add_line_to_multimesh(multimesh: MultiMesh, start: Vector2, end: Vector2, instance_idx: int, height_func: Callable = Callable()) -> void:
    var start_height: float = 0.0
    var end_height: float = 0.0
    if height_func.is_valid():
        start_height = height_func.call(start)
        end_height = height_func.call(end)

    var start_3d := Vector3(start.x, start_height, start.y)
    var end_3d := Vector3(end.x, end_height, end.y)
    var midpoint_3d := (start_3d + end_3d) / 2.0

    var edge_3d := end_3d - start_3d
    var length_3d: float = edge_3d.length()

    var edge_2d := Vector2(edge_3d.x, edge_3d.z)
    var horizontal_length := edge_2d.length()
    var horizontal_angle := edge_2d.angle()

    var slope_angle: float = 0.0
    if horizontal_length > 0.0001:
        slope_angle = atan2(edge_3d.y, horizontal_length)

    var instance_transform := Transform3D()
    instance_transform = instance_transform.scaled(Vector3(length_3d, 1.0, length_3d))
    instance_transform = instance_transform.rotated(Vector3.UP, -horizontal_angle + PI / 2.0)

    var local_x_axis := instance_transform.basis.x.normalized()
    instance_transform.basis = instance_transform.basis.rotated(local_x_axis, -slope_angle)

    instance_transform.origin = midpoint_3d


    multimesh.set_instance_transform(instance_idx, instance_transform)
ToastilyBreaded

(2/3)

static func create_plane_mesh(corner_heights: PackedFloat32Array = [0.0, 0.0, 0.0, 0.0]) -> ArrayMesh:
    var vertices := PackedVector3Array([
        Vector3(-0.5, corner_heights[0], -0.5),
        Vector3(0.5, corner_heights[1], -0.5),
        Vector3(0.5, corner_heights[2], 0.5),
        Vector3(-0.5, corner_heights[3], 0.5)
    ])

    var uvs := PackedVector2Array([
        Vector2(0.0, 0.0),
        Vector2(1.0, 0.0),
        Vector2(1.0, 1.0),
        Vector2(0.0, 1.0)
    ])

    var st := SurfaceTool.new()
    st.begin(Mesh.PRIMITIVE_TRIANGLES)
    st.add_triangle_fan(vertices, uvs)
    st.generate_normals()

    var mesh := ArrayMesh.new()
    st.commit(mesh)
    return mesh
ToastilyBreaded

And here's how to generate it as one multimesh (1/3):

# height_func := Callable(Vector2) -> float

static func create_line_mesh(material: ShaderMaterial, lines: Array[PackedVector2Array], height_func: Callable = Callable()) -> MultiMeshInstance3D:
    var base_mesh := Utils.create_plane_mesh([0.0, 0.0, 0.0, 0.0])
    var multimesh := MultiMesh.new()
    multimesh.transform_format = MultiMesh.TRANSFORM_3D
    multimesh.instance_count = lines.size()
    multimesh.mesh = base_mesh

    for i in lines.size():
        _add_line_to_multimesh(multimesh, lines[i][0], lines[i][1], i, height_func)

    var multimesh_instance := MultiMeshInstance3D.new()
    multimesh_instance.multimesh = multimesh
    multimesh_instance.material_override = material
    return multimesh_instance
ToastilyBreaded

Yep here's the shader:

shader_type spatial;
render_mode depth_test_disabled, cull_disabled, unshaded;


uniform float transparency : hint_range(0.0, 1.0, 0.1) = 1.0;
uniform float line_width : hint_range(0.5, 10.0, 0.1) = 2.0;
uniform vec3 color : source_color = vec3(1.0, 1.0, 0.0);


void fragment() {
    float center_x = 0.5;
    float uv_distance = abs(UV.x - center_x);
    float uv_to_screen = fwidth(UV.x);
    float uv_to_screen = fwidth(UV.x);
    float screen_distance = uv_distance / uv_to_screen;
    float half_width = line_width / 2.0;
    if (screen_distance < half_width) {
        ALBEDO = color;
        ALPHA = transparency;
    } else {
        ALPHA = 0.0;
    }
}
ToastilyBreaded
ToastilyBreaded

Best I can do for you is describing the algorithm. Given the Voronoi graph and cells you want to “claim” and thus be immutable to changes in the graph…

  1. Add your claimed cells’ centroids (Vector2) to an array (or better yet a k-d tree… personally that’s a TODO for me). The Vector2 is the index for your cell, and looking up a cell can only be done with that Vector2. You will have to do Vector2 comparisons via centroid_a.distance_squared_to(centroid_b) < epsilon to look up if a cell is claimed or not every time (i.e. you can’t rely on if (centroid_a == centroid_b), and there’s no clever way to hash the Vector2 or assign it a string ID… I’ve tried). I created a cell manager class that has a func is_claimed_cell(centroid: Vector2) method so I can relate my 3D cell objects back to the underlying graph, where the latter stores its centroid as its ID.
  2. Claiming a cell works by detecting a mouse click, then calling a func like claim_cell(centroid: Vector2) on your cell manager class.
  3. Add or remove new points (i.e. centroids) to your points array used for generating your Voronoi. If you want claimed cells to remain immutable, you can only add or remove points outside of circumcircles associated with claimed cells. In other words, when adding/removing a point, collect all associated Delaunay triangles with a cell (i.e. any triangle that shares one of its points with the centroid), get the circumcircles of those triangles, and if a point is within any of those areas, don’t allow it to be added/removed.
  4. Generate the new Voronoi graph. queue_free() any 3D cell objects not marked as claimed, and keep the ones that are. Iterate through the new cells list and remove any that have centroids you’ve marked as claimed (using is_claimed_cell). Then generate all the new 3D objects (mesh, collider, etc.) for those “unclaimed” cells. Also store their centroid ID in their cell script.

An important concept here is that each generation of your Voronoi graph and all the cells it generates are completely “new” each time. What the above achieves is a way to create cell geometry from the graph and map it back to each new generation of the graph. Hence why this is kinda complicated.

Edit: for posterity... I just realized if you tag the points you use for generating the voronoi graph, that is a surefire stable ID between graph generations.

ToastilyBreaded

If you switch out the poisson disc sampling for grid sampling, you can smoothly transition into a grid.

https://preview.redd.it/n2btmp2kg07g1.png?width=1292&format=png&auto=webp&s=0bedbf842f13238174fdac23c1a34a115b1922ae

ToastilyBreaded

Yes! I remember first playing around with Watabou's Medieval Fantasy City Generator several years ago and thinking "how could this be made into a dynamic city building game?". Finally decided to take a crack at it.

A game mechanic I'm planning for is city destruction. You have to keep rebuilding amidst large swaths of your city burning down!

ToastilyBreaded
ToastilyBreaded

Yea but check this out: https://streamable.com/um2xdj

I created a procedure for allowing changes to the graph without affecting cells that are "claimed". This can allow the city to grow with changing density as the game progresses. Everything I've found online about using voronoi graphs for city proc gen like this assumes you generate it once and then don't modify it anymore. Here you can keep changing it.

This is done by exploiting a property of voronoi graphs where any points added/removed outside of all circumcircles associated with a cell means that cell's shape won't change. This is good for city proc gen because you can keep getting nice connected shapes and not willy nilly change the plot of land already used to place roads and buildings.

ToastilyBreaded

Oh neat I just made the map quickly in Houdini. It also has some nice mountains out of frame. Future work is making the map procedurally generated, too.

ToastilyBreaded

I would trust the club and car more if it sounds balanced and consider your headphones to be the weaker reference. That F# is probably getting close to the lowest frequencies your headphones can reliably produce (and falls off faster than bigger speakers pushing more air). If it sounds too quiet compared to reference tracks hitting the same note, you might be missing harmonics that when you listen on headphones will compensate for the loudness you feel should be there. Try duplicating your sub for just that note, change to a 5th or octave, and lower the volume significantly. Adding saturation would basically do the same thing.

ToastilyBreaded

The benefit is OP shared something cool they put time and effort into using an interesting approach. They probably learned a lot and their passion is refreshing. Not everything needs to exist to benefit you.

ToastilyBreaded

Yes if I'm recording on the Babyface/DAW from the 12 I have to record one at a time (unless I'm cool with recording as a bus). If I wanted multi recording, there's still the instrument inputs on the Babyface or the ADA8200. Otherwise the 12's multi track can record all channels simultaneously without the Babyface and DAW. You could get clever with the sub/aux/phones outputs on the 12 for more simultaneous output tracks. As great as the Babyface is, another option is just use the 12 as a USB audio interface (or get a 16 or 24). Personally I like having all the gear for a lot of flexibility.

ToastilyBreaded

I also have a Babyface and complimented it with a Tascam 12 and ADA8200. The 12's stereo main goes into the Babyface and I ADAT the ADA8200. I plug everything into the 12 which makes routing quick and work without turning on my computer. I put my pedal board on the 12's sub mix and loop it back into a stereo channel which allows one-button routing to it from any channel. Currently, I just use the ADA8200 as computer output for loopback/re-amping which also goes into a stereo channel on the 12. If I had as many synth as you do, I could get a patchbay that goes into the synth channel. Overall it's a lot of flexibility and really fast to start up. Plus, the 12 has a multi track recorder if I really don't feel like dealing with Totalmix FX or a DAW. I tried a looback test from my DAW which went through the ADA8200, Tascam 12, Helix Stomp + pedals, and Babyface; I didn't actually null test but it sounded exactly the same to me!