New release: Tiled Terrain Manager v1.20

I was meaning to release this at some point, couldn’t find the time to wrap everything up. Thanks @ayrin for the boost.

Tiled Terrain Manager v1.00

Features:

  • Terrain generator from tiled heightmaps. The heightmaps and colormaps (splatmaps) have to be provided in a specific named notion. Included there are two sample terrain projects created with WorldMachine.
  • Texture generator from tiled colormaps (splatmaps). The user can provide up to 4 textures and the shader mixes them on the terrain based on the channels of the colormap (R,G,B and Alpha).
  • All methods included in the generators are run using an async loop, which splits them between the frames. This allows generation, rendering and texturing to be done during run-time creating only momentary pauses.
  • A special collision mesh can be created if required with a smaller subdivision count than the rendered model for physics/collisions.
  • Normals are fixed and re-calculated on border, during generation to avoid seams between the tiles.
  • A visibility manager can be configured to render only a specific number of tiles based on the active camera. The tiles get visible/hidden based on the position of the camera on the terrain. If memory is managed correctly this allows for infinite terrain size/tiles.
  • A sample actor is provided in the project (named Aj), with a simple 3rd person controller.

Test build:
https://playcanv.as/b/v0Gdrp2F/

Public project:
https://playcanvas.com/project/484386/overview/tiled-terrain-manager-v100

Enjoy!

13 Likes

Fantastics stuff! I’m going to be needing one of those shortly :slight_smile:

3 Likes

New release: v1.10 - Distribution maps added

  • A special script can be added which takes as input a black/white distribution map and uses the non-black areas to populate and distribute/scatter the tagged objects on the terrain.

Note: for this feature to work the Collision flag on the TerrainManager needs to be checked, as the placement code uses a raycast to find the terrain height.

Public project:
https://playcanvas.com/project/484386/overview/tiled-terrain-manager-v110

Test build:
https://playcanv.as/b/7DHAmFNu/

  • So create your distribution maps:

  • Add the terrainDistribute.js script and your input entities:

  • Enjoy a non boring terrain:

4 Likes

That’s a really nice feature!

Now those trees are ideal for a bit of mesh baking I bet to reduce draw calls :slight_smile:

1 Like

Ahaha, good idea! I have your toolbar loaded always, looking forward in using the decimator and the mesh combiner feature.

By the way, is there a chance that you release the mesh combiner as an API? When content is procedural generated it would help a lot to combine meshes like grass for example.

@whydoidoit I tried to bake the tree and a rock model. Steps:

  1. Selected the entity from the hierarchy.
  2. Pressed bake and put 200 or 350 on max vertices.
  3. A new baked entity was created and a new asset as well.

But the asset is empty, on all cases. Any ideas?

{"model":{"version":2,"nodes":[{"name":"root","position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]}],"parents":[-1],"skins":[],"vertices":[],"meshes":[],"meshInstances":[]}}

If you download the source to the toolbar from GitHub it contains a runtime decimator.

This is the scripts 1 version (the GitHub code is Scripts 2.0 and ES6/babel/webpack):

pc.script.attribute('enabled', 'boolean', true)
pc.script.attribute('deleteOriginal', 'boolean', true)
pc.script.attribute('collision', 'boolean', false)
pc.script.create('meshcombiner', function (app) {


    var MeshCombiner = function MeshCombiner(entity) {
        this.entity = entity
        //this.enabled = this.enabled;
    }

    var validTypes = {
        "POSITION": true,
        "NORMAL": true,
        "TANGENT": true,
        "TEXCOORD0": true
    }

    window.nextId = window.nextId || 1
    var fixer
    var id = 0
    MeshCombiner.prototype = {
        postInitialize: function () {
            var self = this
            if (!this.enabled && this.entity.enabled) {
                return
            }
            this.entity.setPosition(0,0,0)
            this.entity.setEulerAngles(0,0,0)
            console.log("COMBINING", this.entity.name)
            this.enabled = false
            var meshes = []

            pc.utils.ofType(this.entity, 'marker').forEach(function (m) {
                m.entity.removeComponent('model')
            })

            pc.utils.ofType(this.entity, 'model')
                .forEach(function (model) {
                    if (model.model && model.enabled && model.entity._enabled) {
                        model.model.meshInstances.forEach(function (mesh) {
                            meshes.push({
                                mesh: mesh,
                                material: mesh.material,
                                model: model
                            })
                        })
                    }
                })
            var byMaterial = _.filter(_.groupBy(meshes, function (mesh) {
                return mesh.material.name
            }), function (g) {
                return g.length > 0
            })

            var replace = new pc.Entity(app)
//            replace.syncHierarchy = makeEasy(replace);
            replace.name = "Replace"
            replace.enabled = true
            var transform = new pc.Mat4()
            var worldToLocal = new pc.Mat4()
            worldToLocal.copy(this.entity.getWorldTransform())
            worldToLocal.invert()
            var models = []

            _.forEachRight(byMaterial, function (list) {
                var material = list[0].material
                var combined = new pc.Entity()
                replace.addChild(combined)
                combined.addComponent('model')
                combined.name = material.name + " Holder"
                combined.enabled = true
                combined.model.castShadows = true
                
                combined.model.receiveShadows = true
                //combined.model.type = 'box'

                var pos = []
                var uv = []
                var normal = []
                var indices = []
                var tangents = []
                var p = 0
                var ind = 0

                //Now loop through and transform everything
                list.forEach(function (m) {
                    //First get the world transform of the item
                    transform.copy(m.mesh.node.getWorldTransform())
                    transform.mul(worldToLocal)

                    var vb = m.mesh.mesh.vertexBuffer
                    var ib = m.mesh.mesh.indexBuffer[pc.RENDERSTYLE_SOLID]
                    var iblocked = ib.lock()
                    var indexes = new Uint16Array(iblocked)
                    var locked = vb.lock()
                    var format = vb.getFormat()
                    var base = m.mesh.mesh.primitive[0].base
                    var stride = format.size / 4
                    var data = {}
                    for (j = 0; j < format.elements.length; j++) {
                        var element = format.elements[j]
                        if (validTypes[element.name]) {
                            data[element.name] = new Float32Array(locked, element.offset)
                        }
                    }
                    var positions = data["POSITION"]
                    var vec = new pc.Vec3()
                    var t = p

                    //Make room for the new ones
                    for (var i = 0; i < Math.floor(positions.length / stride); i++) {
                        pos.push(0)
                        pos.push(0)
                        pos.push(0)
                        uv.push(0)
                        uv.push(0)
                        tangents.push(0)
                        tangents.push(0)
                        tangents.push(0)
                        normal.push(0)
                        normal.push(0)
                        normal.push(0)
                    }
                    var tv
                    for (i = 0; i < positions.length; i += stride) {
                        vec.set(positions[i], positions[i + 1], positions[i + 2])
                        tv = transform.transformPoint(vec)
                        pos[t] = tv.x
                        pos[t + 1] = tv.y
                        pos[t + 2] = tv.z
                        t += 3
                    }
                    var normals = data["NORMAL"]
                    t = p
                    if (normals) {
                        for (i = 0; i < normals.length; i += stride) {
                            vec.set(normals[i], normals[i + 1], normals[i + 2])
                            vec = transform.transformVector(vec)
                            normal[t] = vec.x
                            normal[t + 1] = vec.y
                            normal[t + 2] = vec.z
                            t += 3
                        }

                    }
                    var uvs = data["TEXCOORD0"]
                    t = p / 3 * 2
                    if (uvs) {
                        for (i = 0; i < uvs.length; i += stride, t += 2) {
                            uv[t] = uvs[i]
                            uv[t + 1] = uvs[i + 1]
                        }
                    }

                    var numIndices = m.mesh.mesh.primitive[0].count

                    for (i = 0; i < numIndices; i++) {
                        indices.push(indexes[i + base] + p / 3)
                    }
                    p += (positions.length / stride) * 3
                    //Turn off the existing object
                    models.push(m.model.entity)
                    vb.unlock()
                    ib.unlock()

                })

                var mesh = pc.scene.procedural.createMesh(app.graphicsDevice, pos, {
                    normals: normal,
                    uvs: uv,
                    indices: indices,
                    tangents: pc.calculateTangents(pos, normal, uv, indices)
                })

                console.log(material.name, "triangles", indices.length/3, "vertices", pos.length/3, "models", list.length)

                var root = new pc.scene.GraphNode()
                var instance = new pc.scene.MeshInstance(root, mesh, material)
                instance._aabb = mesh.aabb
                var model = new pc.scene.Model()
                model.graph = root
                model.meshInstances = [instance]
                var asset = new pc.Asset('Combined Mesh', 'model')
                asset.loaded = true
                asset.resource = model
                app.assets.add(asset)
                combined.model.data.asset = asset
                combined.model.data.type = 'asset'
                combined.model.model = model
                if (self.collision) {
                    combined.addComponent('collision',
                        {
                            type: 'mesh',
                            model: model
                        })
                    combined.collision.model = model
                }

                var id = nextId++
                combined.enabled = true

            })
            replace.enabled = true
            this.entity.addChild(replace)
            models.forEach(function(m) {
                if(self.deleteOriginal) {
                    m.destroy()
                } else {
                    if(m.model)
                        m.removeComponent('model')
                    delete m.model
                }
            })

//            setTimeout(function () {
//                self.entity.destroy();
//            });
//            }.bind(this));
        }

    }

    return MeshCombiner

})


This requires lodash on _ - oh crap it also needs my pc.utils.ofType (but that could easily be rewritten). The GitHub version uses import for all of this stuff

1 Like

Hmmm that’s odd. Does everything have more than that number of verts?

Yes, the tree is 540, tried many numbers (and zero, and the max number 540).

By the way to get it right, the bake feature is to:

  • combine meshes
  • and reduce vertices?

Also when I select a model asset on the asset browser, on the inspector I can see a label “Decimate” but with no optionse

You got meta data on that model? It should be making it anyway… Can you let me have that model? CDT console might be showing an error I’d guess

Here you go, rock and tree, both failed:

http://pirron.one/leonidas/lowpolytree.fbx
http://pirron.one/leonidas/rock1.fbx

Thx I’ll take a look. Works on all the ones I tried, but that’s not exactly an exhaustive list.

Bake will just reduce draw calls by combining models into one big static mesh - works really well with low poly stuff because it’s all CPU bound. Decimate will reduce complexity.

Understood, though where do I have to go to decimate?

Check my last screengrab above, selecting the tree asset, decimate on the inspector is empty.

That’s why I asked for the models :slight_smile:

Ah, right, got it, you parse the meta data to prepare the panel. :wink:

Yeah I need the data from it, if it won’t parse then something might be up anywhere lol. Just on my laptop and the dev version of the toolbar is on my desktop. I’ll see if I can get it working.

1 Like

Ok there’s a bug in the baker if the models don’t have UV2. But the decimate opens properly for me. So I’m guessing it’s something up in my webpack build. I’ll fix that fault and put up a new build shortly with some settings played with.

Do you have a console error when you select the model in the assets panel?

What version does it say you are running BTW?

Great that you found something. Indeed there is a console error: