Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terrain mesh generation/manipulation #66

Open
BryceBubbles opened this issue May 26, 2020 · 30 comments
Open

Terrain mesh generation/manipulation #66

BryceBubbles opened this issue May 26, 2020 · 30 comments
Labels
enhancement New feature or request

Comments

@BryceBubbles
Copy link

@HungryProton we discussed a Plane Mesh from Transforms node for terrain here: #59 (comment)

The main idea is to be able to generate terrain meshes from noise.
But it would be nice to apply other operations/deformations as well.

You asked if it would need to be a perfectly aligned grid of transforms to generate a terrain?
Yes and no.

  • A uniform terrain mesh requires a single layer grid of points, where some jitter that's less than grid density is ok.
  • A triangulated mesh (Delauney) can be completely non-uniform, but still needs to be based on a 2D plane of points. Note: triangulation slows down generation considerably.

So yes the issue is that a NODE_3D input can be a very random set of 3D points.

Mesh from Transforms - Marching Cubes?
A proper Mesh from Transforms node would have to apply a marching cubes algorithm I think.
This would be very cool, but it's much more complex and possibly out of scope for this project?
It's definitely not what I'm intending here.

Plane Mesh options:
You pointed out that for a Plane Mesh node, a direct noise input might be better.
This solves the above problems, but it limits the possibilities.

Here's some examples of what I'd like to make possible:

  • Apply multiple Simplex Noises.
  • Apply 2D curves to each noise.
  • Apply 3D curves on top to add specific details (is CSG best here? I'm not familiar with it)
  • Start from a Voronoi diagram and apply Simplex Noise on top.
  • Simply generate a mesh from a heightmap

Some possible solutions:

  • Apply multiple noises - this could be partly solved with dynamic slots on the noise input, but that wouldn't allow for applying other manipulations as well.

    • A Merge Noises node might help here, and could be more widely useful? But if it wasn't working on a NODE_3D array (which it can't be here), I think it would have to pass a noise wrapper class around that encapsulates looping over multiple noises when sampling a point. This wrapper could also handle any new noise implementations we might get? (I've seen you mention that).
  • Apply a 2D curve to each noise - a CURVE_2D input on the Simplex Noise node could be worthwhile, as I can see it being useful elsewhere!?

    • A separate Smooth Noise node is another option, but again it has to be applied to a node array, or a noise wrapper class that applies the curve to the noise at sample time.

This comment is already way to long, so I'll stop there.
Let me know your thoughts.

@HungryProton
Copy link
Member

  • So that confirms my fear that the input needs to be specific enough to work. The add-on doesn't have a hard defined scope so marching cubes could be an option (but I don't have the skills for that).

  • Initially, I only thought about the heightmap terrains, so I thought of adding nodes that edit heightmaps textures directly (that would be a new data type). Not sure how feasible it is with gdscript only though.

    • Another option is to add nodes that work on vertices directly and have the same operations that are available on regular transforms.
  • I'm not sure I understand what "Apply 2D curves to noise" means?

  • Making a Noise Combiner node is a valid option. It would work a bit like the CSG Mesh combiner and expose a generic Noise object. An since it simply exposes a new Noise, it can be chained in another combiner if needed.
    The merge options could be add / substract / multiply and so on (I don't know which operation is needed / possible, the same as the scalar math node I guess?) This could be useful for other use cases as well.

@BryceBubbles
Copy link
Author

Marching cubes
I don't have the knowledge for this right now either, but with time I'm pretty sure I could crack it. But this would definitely require gdnative implementation, so let's leave it for now.

Heightmap modification
We have Zylanns plugin for this, as well as other software, so as a procedural gen plugin I wouldn't go hard with this. And yeah modifying images with gdscript is slow.

Perhaps just a node that loads a heightmap into whatever format we decide on. This way you can use a heightmap as a starting point, or use it as is and be able to place objects on the resulting terrain.

*Work directly on vertices (Vector3)
So basically a new input/output type and nodes specific to terrain? That sounds great to me!

With that in mind, I wonder if it would be faster generation wise, if the modification nodes just pass along the operations to apply, and then the final Mesh node would run them all in a single loop? Maybe, maybe not.

Apply Curve to Noise
Each time you sample the Noise, you convert it from -1:1 to 0:1, then apply 2D curve interpolation in order to manipulate the noise into more realistic/usable terrain.

You can spend a lot of time tweaking this,
and Godots Curve inspector editor is a bit basic and fidgity for modifying curves accurately. Also switching from the graph to the inspector to tweak these curves is a bit tedious.
So ultimately a better in graph 2D curve editor would be nice.

Anyway, should adding a curve to a noise be done as a CURVE_2D input on the Simplex Noise node, or a separate node that saves the curve object into the generic noise object?

Noise Combiner
Just for clarity, combining noises means saving the noise objects in an array, then looping over them all each time you sample a point. And also applying the curves if supplied.

And yeah these noise changes will definitely be useful throughout.

@HungryProton
Copy link
Member

HungryProton commented May 26, 2020

Heightmap modification

  • Something I don't like about most heightmap tools is their non parametric, destructive nature. You paint on the terrain (which is a manual operation and requires more artistic skills) and you can't edit what you did later on.
    If you have a Flatten map node for example that takes a spatial and a heightmap in input, flattens an area around the spatial node and then connect it to an Erosion map node, you can decide later on to move the spatial in another location and it just updates everything for you, other tools don't allow that. (Well, GAEA does but it's paid and closed source)

  • Editing pictures is slow but maybe is still fast enough? It doesn't have to be an actual picture either, even though a 1k heightmap is still 1 million values so I'm not sure there's actually a useful data structure beside images.

Vertex operations

  • Vertex operations wouldn't necessarily need a new type, they should work on any Mesh objects. But adding terrain specific nodes is also an option if that's necessary.

  • Passing all operations at once wouldn't work if at least one of these operations requires data about neighboring vertices. (Like Smooth mesh for example, it needs to average a position using all the connected vertices and lerp the current vertex toward this averaged position. So you still need to apply every operations one by one and that would defeat the purpose of packing them together)

Noise

  • Yes, the noise combiner would be a new noise object (it needs a Noise super class), that exposes the same things as a regular noise would, except internally it queries other noise objects and combine their result. I still don't understand if the curve thing should be part of the noise or be something separated, I think I need more time to understand that part

@BryceBubbles
Copy link
Author

I totally agree with you about heightmap tools. Non-destructive parametric editing all the way! :)

Flatten, Erode, Smooth, etc
My recent work in Godot has been about generating infinite terrain as fast as possible, so I have quite a different, and likely more limited perspective on this than you. So this discussion is good.

While we don't have to generate things as fast here, if we're not careful, chaining a few of these operations could very quickly slow things to a crawl.

I'm definitely keen for these types of nodes, especially an erosion node (note there's code for this in another project - perhaps Zylann's), but I do wonder how long this sort of node would run? Is using another thread for long operations an option for stopping the editor from hanging?

Image manipulation
While images are a decent storage medium for heightmap data, when manipulating the images, working on a raw array of bytes/floats is much faster.

The get_pixel() and set_pixel() Image functions are really slow in gdscript. Using get_data() to get a PoolByteArray, and then doing all chained operations on that array would be much faster (but likely trickier). But if you're wanting to pass an actual image between nodes, I think that could slow things down a fair bit. I should test this assumption!

Performance aside, I think we should definitely have nodes for loading and saving a heightmap, as well as a node for setting shader parameters on a material.

But in order to use the same nodes on both heightmaps and meshes, I think we should convert a heightmap to a mesh (or mesh arrays) and pass that around. Then convert back to a heightmap only when needed.

Vertex operations
Working on any Mesh object - Ah I see what you're wanting to do, and yeah it makes sense to generalise all this rather than make it just about terrain. This is likely much more complex than I was anticipating when I started the terrain discussion, but I'm up for it.

Just like with my comments about image manipulation above, I also wonder about the overhead of passing around MeshInstances and regenerating meshes on every node. I assume it would be much faster to pass around an array of mesh arrays, with the actual mesh generation only happening at output time. Again I should test this assumption! Any thoughts?

Applying a curve to noise
Modifying SimplexNoise with a Curve is really powerful! It allows you to smooth, sharpen, flatten, invert, ridge and generally tame the noise. This is great for terrain, but would also be useful when using noise to modify or exclude transforms.

So while a separate curve operation node would also be good, I do think it's worthwhile being able to couple a Curve object directly with a Noise object and have the changes show in the noise preview.

@HungryProton
Copy link
Member

HungryProton commented May 28, 2020

  • That's true, chaining a lot of heavy operations will hang the editor for a while. All the code to handle multi-threading is already there, but it's disabled because of an issue I haven't figured out yet. I guess that's my next priority if we want heightmap tools.

  • What could be a good test for this?

    • I don't know how Godot handle the PoolArrays internally or if there's a better data structure already available.
    • If you want to test, I think you can keep things simple without trying to create a new node in the plugin. Write a function in a tool script, create a PoolArray large enough to hold a 1k heightmap (more or less, I don't know what's the average useful size for this) and monitor how long it takes to loop through all values and fill it.
    • You can use OS.get_ticks_msecs at the beginning and at the end of the function and subtract both values to know how long it took. If it takes a few seconds at most it's probably fine, if it take several minutes ... we'll figure out something.
  • If we're talking about pure heightmaps (so vertical displacement only, not on all axis like, the Halo Wars terrain for example) then heightmap operations and vertex operations should be two separate things.

  • I should look into the engine source code just to be sure but I think a MeshInstance is just a container for an ArrayMesh so It probably won't change anything to pass around the raw ArrayMesh (plus a transform). I can't stop you from testing this but I don't think it's really important for now.

    • I did some benchmark on the simple tree, the midpoint displacement node is the slowest one, mesh generation was faster than updating a curve. (Which probably means my implementation is awful)
  • Ok I think I'm starting to understand what you mean with the curve. Is it something similar to the color curve found in most image processing software?

image

If so, yes it could definitely be integrated in the noise object.
(If I still got this wrong ... do you have an article / tutorial / example somewhere ?)

@HungryProton HungryProton added the enhancement New feature or request label May 28, 2020
@BryceBubbles
Copy link
Author

Multi-threading

Um, this might not be higher priority than some of the other features right now!?
But it would definitely make the editor more responsive and less likely to hang when modifying properties on complex graphs.

Pool Arrays

The docs say they're optimised for memory usage, as they don't fragment the memory.
And that they're passed by value, where as generic Arrays are passed by reference.

But based on some discussions I read, it's not as simple as that:
godotengine/godot#8409
godotengine/godot#17526
godotengine/godot#15265

But it seem performance depends on what you do with the array.

Testing with tool script and OS.get_ticks_msecs

Yup that's how I normally do it.
But this benchmark project is even better.

On my system with Gogot 3.2 stable these are some significant results:

  • array append() ~200% faster than PoolIntArray append()
  • array[index] ~150% faster than array append()
  • array[index] ~3% faster than PoolIntArray[index] - I added this test for this

So basically don't append, especially to pool arrays, but if you access the pool arrays by index they're as fast as normal arrays, even if you pass them around (I added a tested for this too).

Some other things to consider:

  • empty function call 300% slower than not calling a function
    • So any function you call inside a large loop is expensive (like surface_get_arrays)
  • for ~130% faster while
  • for i in int: ~30% faster than for i in range(int):
  • if array: ~100% faster than if array.size():
  • if array: ~400% faster than if array == []:
    • the same is true for Dictionary, as well as float/int to a lesser degree
  • var init outside loop ~25% faster than var init inside loop

As an aside - a Timer node would be cool! It could pass through the input/output of other nodes, and have a second input/output to pass the msec value between two Timer nodes

MeshInstance vs ArrayMesh vs Raw Surface Arrays

So I added tests for this to my fork of the benchmark project if you ever want to take a look.

You're right that passing ArrayMesh around isn't significantly faster than using MeshInstance.

But what I meant was that passing around the raw surface arrays should be faster, and it is.
How much faster varies widely depending on 3 main things:

  • array size
  • number of loops
  • complexity of each loop body

But the bottom line is that it's can be between 25-100% times faster to pass around the raw surface arrays, rather than getting the surface arrays out and back into the ArrayMesh.

Heightmap vs Vertex operations

I haven't specifically tested this yet, but in previous tests I've done, the same was true for getting raw data in and out of images.
So heightmap operations should also pass around their raw array data if possible.
So in order to use use the same modification nodes for both, we might as well load it into a raw surface array!?

Like curves in image processing software

Yes that's a much better way to explain it - silly me.
When applying a curve to an image, it's basically a complex way to do brightness, contrast, invert and much more.
Cool lets add a curve input to the generic Noise class we add!
I can work on this after I get the Transform nodes cleaned up

@HungryProton
Copy link
Member

HungryProton commented May 29, 2020

  • I didn't knew about this benchmark project, I'll check it out. In this use case we will never have to append anything to the PoolArray since it size should never change. Getting and setting values are the only two that matters.

  • Ok that's really good to know! That means the MESH type currently in use should no longer represents a mesh instance but an array of raw surfaces.

    • Should we add the mesh instance transform somewhere in there to not lose this information?
    • Would creating a new object with two members, surfaces and transform, have a performance impact compared to passing the array only?
  • I don't know, heightmaps aren't really a mesh, their raw data is nothing more than a PoolIntArray (or PoolByteArray?)

    • I think it's best to separate heightmap and meshes operations.
    • Then when you actually want to turn the raw heightmap into a mesh, there's a node to do that and you can chain its mesh output to vertex operations nodes
      • But doing so, you lose the advantage of using a simplified runtime mesh generation and collision handling and are forced to use a per polygon collision. Which may or may not be an issue depending on the use case
    • If you don't need to generate a mesh here, there's a node to save the raw data in a picture and then use it in a heightmap system tailored for you game (which is not part of the plugin)
  • For the noise curves, feel free to work on it if you have time. It needs a new ConceptGraphNoise class (I wish namespaces were a thing in gdscript) that expose an API similar to what OpenSimplexNoise provides (just the most important parts). Not entirely sure of all the details for now

@BryceBubbles
Copy link
Author

BryceBubbles commented May 29, 2020

Class wrapper with surfaces and transform?
I just tested this, and it doesn't seem to add an impact.
So yeah a ConceptGraphMesh class?

Heightmaps separate
Ok, yeah that makes sense.
So a new type that just passes around the raw data?
PoolByteArray is a pain to work with, so for sanity we may want convert to/from an int or float array.
Will test what's best some time.

What do you mean by this?

lose the advantage of using a simplified runtime mesh generation and collision handling

Heightmap system not part of the plugin
Yeah not full on, but we could have a node for setting shader params on a material, and a basic vertex shader example, just to show the workflow.

ConceptGraphNoise
Cool I think I have a pretty good idea of what's required.

@HungryProton
Copy link
Member

HungryProton commented May 29, 2020

  • Thanks for the tests! ConceptGraphMesh sounds good to me.
  • PoolRealArray it is then, unless there's a good reason to use the int variant. Also, probably going to need its own wrapper class for meta data like heightmap range or other things I may have forgot about.
  • What I mean by simplified runtime mesh generation and collision handling is
    • Godot has a built in HeightMapShape object to handle terrain collisions.
    • It's faster for the physics engine to use this shape compared to using a full collision mesh because it only have a height check to do.
    • If you apply a vertex operation after generating the heightmap mesh, the collision data won't match the geometry anymore, especially if the vertices are moved in another axis than the vertical one.
      • We could add a node that project a mesh geometry to a heightmap but that's yet another feature

@BryceBubbles
Copy link
Author

BryceBubbles commented May 29, 2020

HeightMapShape
Oh ok, yes I use this in my infinite terrain project.

It's not so much about heightmap vs mesh, it's just a question of whether you have a uniform grid of points or not.

I've messed with trimesh shapes for triangulated "low poly" terrain too, and while it's much slower to generate, I've never noticed any runtime performance issues with it.

I didn't have lots of physics bodies interacting with it though, just a character controller.

I've actually experienced more issues with the heightmap shape than with trimesh. Things like clipping on inclines when mesh resolution is low, and falling through when moving at speed.

Heightmap offers control
I see what your getting at though, mesh nodes can modify x/z, so it's best to stay in heightmap territory.

Possible Solution
When converting from heightmap to mesh, have a flag for whether x/z have been modified, and if so generate a trimesh shape for collision, otherwise generate a heightmap shape.

Then switch the flag in mesh modification nodes when the x/z of verts will be modified. Like if they're not 0 in an amount vector3 input.

On the same page
I see that you're pointing towards this heightmap stuff being the better simple terrain solution (which is what I originally wanted to work on). I've just been getting hung up on meshes vs images thing I guess.

So yeah I think we're on the same page now.

@HungryProton
Copy link
Member

It's not going to be easy to have a reliable flag for x/z while making it clear for the end user what they can or can't do though. But I can start working on a prototype later this week end if I can fix two more pressing issues first.

@BryceBubbles
Copy link
Author

I was just thinking a property on the new ConceptGraphMesh could work, but yeah checks would have to be added to every Mesh node that can modify x/z.
And yeah the UX of it could be rather tricky.

So I'd say, lets implement Heightmap stuff on it's own for now, and see how it progresses.

This flag thing isn't priority imo, but that's up to you.

@HungryProton
Copy link
Member

My bad, I meant I can work on a minimal set of heightmap nodes just to see how it goes

@BryceBubbles
Copy link
Author

Oh ok, but just note...

I've done quite a bit of work around terrain generation in recent months, so I have bits of pretty optimised code for this stuff already. So yeah that's why I was pretty keen to work on it if possible.

I have stuff like working with PoolByteArray, generating heightmaps and normalmaps from raw float arrays, an example vertex displacement shader with basic vertex coloring. Then there's the terrain mesh generation stuff, which could just be done as a simple end node for outputting the heightmap (for visualisation if nothing else).

But it would definitely be good if you could setup the Heightmap input/output type, as I haven't looked into that part of the code yet, so aren't sure what's involved.

I have some time this weekend too (public holiday on Monday too), so let me know what's priority for me to work on:

  • Node naming cleanup (I've done most of this, as well as tweaking the node panel to sort alphabetically)
  • Noise class/node (merging multiple, adding curves)
  • Heightmap nodes

@HungryProton
Copy link
Member

I think it's best to finish what was already started before jumping to new features. I'll setup the new data type for heightmaps but then, work on what you want the most !

@HungryProton
Copy link
Member

HungryProton commented May 30, 2020

So I've added some very basic heightmap stuff in cab8450

There's a new ConceptGraphDataType.HEIGHTMAP type for node slots
There's a new ConceptGraphHeightmap class in src/data_structures/heightmap.gd which is just a wrapper around a PoolRealArray

Nothing is optimized since it's mostly throwaway code

  • It works fine with really low resolutions (like 128*128)
  • 256 already takes a lot longer (around 30 - 40 seonds) but at least it doesn't freeze the editor since everything runs on a separate thread now.
  • I tried 1k and the editor crashed so I assume Godot doesn't like continuous 1 million elements poolarrays, I haven't really investigated.

@BryceBubbles
Copy link
Author

Awesome, thanks for setting that up.

So yeah, writing into a PoolXArray property on an object is very slow. This is because they're only passed by reference when reading, but have copy-on-write functionality (for mutability?). The same is not true of normal Arrays.

So in heightmap_from_noise doing map.data[i] = height inside the loop, copies the whole array every iteration - crazy huh. Juan from Godot seems to think GDscript shouldn't need to use pool arrays much, so I think fixing this is pretty low priority for him.

Anyway, this can be fixed while still using a PoolRealArray, by copying it to a local var before the loop and copying it back to the object after. In GDscript (and other languages), doing this for any variable reference external to a function is faster - when you're going to r/w it multiple times that is.

But, so that no one hits this issue in future, lets stick to using generic Array's, and only convert to Pool arrays when a Godot method requires it - as their constructor functions allow this and the overhead is minimal.

While this allows for much more flexibility with the ConceptGraphHeightmap class , there's still performance differences between common options - values are for my basic laptop with i7 5500U 2.8Ghz:

  1. data[i] local var - 550ms (same for Pool or Array)
  2. map.data[i] - 700ms (this is where a Pool array causes issues, but Array is fine)
  3. map.set_index(i, height) - 780ms (same for Pool or Array, but unnecessary overhead for Array)
  4. map.set_point(x, y, height) - 900ms (same for Pool or Array and is a useful method)
    set_point could be set_data, but set_varname usually implies setting the whole value. set_height could be another option

Those are much better generation times huh! :)

Note that this loop is only running get_noise_2d, which is fairly fast. More complex operations will slow things down a lot more, and may make the difference between these options disappear.

So I'll add those class methods (already have locally), and we can test performance again once we have more nodes to chain. I'll probably use option 1 for now. Will PR soon.

Finally, in my quick benchmark test of this the other day, I was storing the object array to a local var, because it's something usually do anyway. So that's why the issue didn't show up then.

Then optimising Mesh from Heightmap will be my next task I think

@HungryProton
Copy link
Member

Thank you for the detailed explanation, that makes it a lot clearer! I completely missed the copy on write part, no wonder it was so slow.

I tried generating a 2k heightmap and it worked well. It took 4 seconds on my CPU and the code only uses 1 thread so there's still room for optimization even before trying to do this in gdnative. (But that's not a priority).

I'd like to experiment with new heightmap nodes but I don't want to rewrite code you may already have done before. Let me know if there's things I shouldn't be working on.

@BryceBubbles
Copy link
Author

My top priority will be adding from_image() and to_image() methods to the heightmap class, as I have decent code for doing this already. Feel free to suggest different function names.

I've also started work on:

  • Noise combining with curves
    • Have the new class done and Simplex Noise node updated (will PR)
    • Was going to add Curve input to Simplex Noise, but might add a separate node instead.
  • Optimizing Heightmap to Mesh
    • I have this mostly sorted, but need to iron out bugs
    • I've also added and tweaked some inputs (might discuss in separate issue)
    • But I had to stop work on this because of those errors I'm having.

And here's some nodes I'd like to see added (I'm keen to work on any)

  • Heightmap from Image (will use from_image()).
  • Heightmap to Image (will use to_image()).
  • Generate Normal Map (easy, I have code).
  • Generate Colour Map - based on height and slope (also have code for this), and noise masks etc.
  • Generate Splat Map - similar to above but output to RGBA channels rather than pixel colors.
  • Heightmap Splitting - output an array of multiple heightmaps for terrain chunking.
  • Heightmap to Mesh Grid - take the above and generate multiple meshes in a grid (optionally flat planes for use with a vertex shader).
  • Set Shader Param - for directly assigning images to materials.
  • Heightmap to Transforms - for scattering objects on the terrain.
  • Transform nodes to help with scattering:
    • Place/Mask based on height and slope.
    • Trees/rocks - Landscaper for Unity does some cool stuff.
    • Grass - a special case, as you have to account for mesh face normals or you get the floating grass issue (I've done some work on this).
  • Deformation - adding rivers or specific mountains based on curve 3D and other inputs.
  • Erosion - Zylanns Heightmap plugin has code for this.

So feel free to make a more final list which:

  • Adds your ideas
  • Removes ideas you aren't keen on adding (let me know why)
  • Sort in order of priority (or just point out which should be done first)
  • And notes the stuff you want to work on.

@HungryProton
Copy link
Member

Heightmap to mesh
I've started working on a simple deformation node which made me realize there's a few inputs that should be moved to the heightmap class itself, like the mesh size, to allow operations like this:

image
It's available on the latest commit of develop, nothing definitive yet, I just canted to see if it would work.

New heightmap nodes
Looks good to me. I don't know if there's a need to prioritize anything. I was also thinking of using vertex colors to avoid generating color textures and use a shader to handle the tiling and blending. That could be an other option in addition to everything else.

@BryceBubbles
Copy link
Author

BryceBubbles commented Jun 5, 2020

That flatten area node is very cool, and I can see where it's leading.

As for needing to move properties into the Heightmap class?
I've been working on this the past few days, and it's finally working well! Here's what I've done:
heightmap-nodes
I've got it working really robustly, so you can set the values to anything and it will always output a mesh. It even outputs a flat mesh when the noise is disconnected - I was wondering if having a Generate Heightmap node with separate Apply Noise and Apply Image nodes would be better? Heightmap from Noise is definitely quicker all in one option.

Anyway, as you can see, I've changed all properties to scalars. I did this because they're quicker to use (no extra nodes required), and they have the step, min, max options. Those options help make things more robust and easier to edit. There seems to be no way to limit a vector node the same way? Could vector inputs be in the node by default like scalars? Anyway, the current code isn't using separate x/y sizes yet, and terrain meshes are usually square.We can change this if you think we need to?

I also changed Density to Subdivide as it's easier to understand, and it allows you to connect either of the size values to it, optionally passing them through a scalar division (for lod output perhaps?). Changing the Texture Size scales the noise, which allows you to mess with resolution without the terrain changing (super helpful).

So with all this, you can make Texture Size match Subdivision (better for fast update), and then later you can easily up the texture resolution for generating normal/color/splat maps.

Here's an example terrain that's using 3 blended noise nodes. The noise stuff has been really tricky, but I've almost got it sussed.
heightmap-example

I'll get things cleaned up and will PR over the weekend.
I feel like I've been a bit slow on getting this stuff sorted, but it's going to be worth it :)

@HungryProton
Copy link
Member

Awesome work! Let me know if there's strange conflicts with the base branch when making the PR.

Apply Noise and Apply Image
I think it would be better, this would keep all the generation settings in one place without having to duplicate the code (I don't think users need to generate a lot of heightmaps in a single graph anyways), and having an Apply Noise node make it possible to chain them.

Could vector inputs be in the node by default like scalars
It used to be possible, but the UI was really bad and made every nodes twice as wide. Initially I thought it wouldn't be a big deal to separate them but now I realize how annoying it is to create a new vector node every time. I'm going to add them back and copy Blender's UI for this.

Parameter changes
Good catch on the texture size, my prototype just didn't cared at all about this, maybe we should add a setting to the noise preview too so the picture matches what's actually used, for now it displays a 175px square of noise. Also I guess the Subdivide parameter should be called Subdivision.

How does the Blend Noise work? The blend formula is usually (A * (1-x)) + (B * x) but I see the second input accepts multiple connections?

@BryceBubbles
Copy link
Author

Apply Noise - ok will add this.

Naming format again
When naming the transform nodes I used brackets for the "variant" name:
Heightmap (Apply Noise)
But now I'm thinking a colon is better:
Heightmap: Apply Noise

Having "Heightmap" in there is mostly necessary for search. But you're going to make search also use the category we could drop "Heightmap:" It is still good for visual clarity though.
Perhaps we should decide on the node and input naming conventions and post it somewhere here for people to refer to?

Blend Noise formula
(A * (1-x)) + (B * x) - this is a "lerp" formula right?

My current code does this in the loop of noises:
get_noise -> apply curve -> clamp -> lerp (blend value) -> add to total
Then after the loop total / num_noise - when curves are involved this isn't perfect averaging.

I was thinking of adding a Blend Type input with various math options like Add, Subtract, Min, Max - kind of like your Flatten Area node. But this adds even more overhead, which is costly when calling get_nose thousands of times. It also makes the averaging (total / num_noise) part tricky.

Adding multiple noises to the second input isn't strictly necessary, and it might make things easier if we don't allow that.

Note: having the Apply Noise node makes Blend Noise slightly unnecessary for Heightmaps. It might still be useful for other situations, but I'm not sure how useful. Having a curve on the noise node is the most useful part.
It would be interesting to see if the Blend Noise setup (looping noises each get_noise) is faster or slower than looping heightmap points multiple times but only sampling a single noise in each loop.

@HungryProton
Copy link
Member

HungryProton commented Jun 5, 2020

I'll add a dedicated page for naming conventions in the wiki.

Blend Noise formula
Oh right, that's a lerp formula, my bad.

Having a Blend Noise node is still useful for users doing something unrelated to heightmaps. Having multiple operations could be useful too, maybe in a separate Math Noise node (maybe not the best name) but I think this one can wait until after your PR

Edit
New UI for vectors and scalars
image

@HungryProton HungryProton added this to the 0.5 release milestone Jun 6, 2020
@BryceBubbles
Copy link
Author

Along with the height map to vertex shader node...

I'm working on an apply image node that takes a texture input from an inspector property.

Currently I have two vector3s for scale and offset, with the defaults fitting the image to the full size of the heightmap (like apply noise node).

I could change this to take multiple box inputs for stamping multiple instances of the image onto the heightmap? Or perhaps that should be a separate node?
Does that sound good, or can you think of a better workflow?

@HungryProton
Copy link
Member

It would probably be simpler if the stamping was its own node. Otherwise you have an Apply Noise and an Apply Image that sounds like they do the same thing but from different sources, except Apply image has extra parameters and features.
So you would have to port that feature back in Apply Noise and I guess this makes a good rule of thumb : If two nodes can share the same feature then it should be extracted in its own node.

@BryceBubbles
Copy link
Author

Hey @HungryProton
I was working on the Apply Image node by looping over the image data, but I had some trouble getting scaling and rotation right. I mostly have it sorted now, but...

I've jumped into an even better solution for real-time heightmap editing!
It uses canvas_item shaders on TextureRects inside of a Viewport, whose output is applied directly to a vertex/fragment shader on a PlaneMesh

It's basically using the Viewport like a document in Photoshop, where the TextureRects are layers and the canvas_item shader enables blending the layers.

So I'm now planning to redo the Heightmap class as an interface for setting up the Viewports and adding/transforming the TextureRects inside of it.

It's going to be epic! Hopefully I can get something usable setup this week.

@HungryProton
Copy link
Member

HungryProton commented Jun 17, 2020

If it works, it's going to be great! Let me know if you need anything, I'm mostly working on bug fixing for now

@BryceBubbles
Copy link
Author

Hey I'm still working on this viewport based heightmap solution, but have been busy recently.
I have a bunch of it working well in real time, but still have a bunch to do to get it matching all the current features.
I may just put it up on my fork some time for you to look at, and then perhaps you can move forward with it quicker?

@HungryProton
Copy link
Member

Hey! No worries, I've been quite busy too lately. I glad to see that your idea worked! Feel free to put it online so I can take a look.

I don't know if you heard about it but I'm turning the add-on into a full standalone software. It shouldn't affect your work at all, it's still made with Godot and I'm only rewriting the interface, I'm not changing anything about the nodes. Being standalone makes it much more stable and a bit faster too!

But that means ConceptGraph now works at runtime and the next big thing on my roadmap is to make work in a game, not only in an editor. I think that's something you were interested in too. I'll let you know once I made more progress on this part, it would be interesting to see how far we can go with your new height-map system 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants