Blocks

Hi,

I have not written an entry in the wiki about custom shapes, it is still on my to-do list. I do of course welcome contributions :wink:
I did however add a lot of javadoc explaining the inner workings of the mesh construction.

For a custom shape to be useable by Blocks, you need to register it with the ShapeRegistry:

ShapeRegistry shapeRegistry = BlocksConfig.getInstance().getShapeRegistry();
shapeRegistry.register("my_custom_shape", new CustomShape());

A new shape must implement the Shape interface. This interface has 1 method (Shape#add(Vec3i, Chunk, ChunkMesh)) that is called for each block in a chunk when the mesh is generated. Where:

  • Vec3i : location of the block inside the chunk
  • Chunk: the chunk that contains the block
  • ChunkMesh: the current ‘mesh’ of the chunk. This is the object where you should append your shape to.

The ChunkMesh holds lists of all the vertex positions, indexes, normals, tangents, … for the mesh construction of the chunk. You should add the vertices, uv’s, normals, … of your shape to these lists.

I’ll add this example of a shape that renders a block as a horizontal plane square.

A --- B
| \   |
|  \  |
|   \ |
C --- D

A horizontal square will have these positions:

  • (-0.5f, 0, -0.5f) A
  • (0.5f, 0, -0.5f) B
  • (-0.5f, 0, 0.5f) C
  • (0.5f, 0, 0.5f) D

these indexes (the 2 triangles):

  • (1, 0, 3) (BAD)
  • (0, 2, 3) (ACD)

and let’s say it’s pointing up, so we have these 4 normals:

  • (0, 1, 0)
  • (0, 1, 0)
  • (0, 1, 0)
  • (0, 1, 0)

and to end the uv coordinates:

  • (1, 1)
  • (0, 1)
  • (1, 0)
  • (0, 0)

you can also add the tangents but these are optional. Blocks will generate missing tangents.

Implementing this shape in blocks will be something like this:

public class Square implements Shape {

    @Override
    public void add(Vec3i location, Chunk chunk, ChunkMesh chunkMesh) {
        // get the scale of the blocks. The vertex positions of the shape should take this into account
        float blockScale = BlocksConfig.getInstance().getBlockScale();

        // we need to offset the positions of the shape so they are at the correct location in the chunk.
        // the Shape class has a helper method that will calculate the position of the vertex based on the location of the block in the chunk and the block scale.
        // this is exactly the same as doing: new Vector3f(0.5f, 0, -0.5f).addLocal(location.x, location.y, location.z).multLocal(blockScale);
        Vector3f p1 = Shape.createVertex(new Vector3f(0.5f, 0, -0.5f), location, blockScale);
        Vector3f p2 = Shape.createVertex(new Vector3f(-0.5f, 0, -0.5f), location, blockScale);
        Vector3f p3 = Shape.createVertex(new Vector3f(0.5f, 0, 0.5f), location, blockScale);
        Vector3f p4 = Shape.createVertex(new Vector3f(-0.5f, 0, 0.5f), location, blockScale);

        int startIndex = chunkMesh.getPositions().size();

        // add the vertices
        chunkMesh.getPositions().add(p1);
        chunkMesh.getPositions().add(p2);
        chunkMesh.getPositions().add(p3);
        chunkMesh.getPositions().add(p4);

        // connect the vertices to create triangles
        chunkMesh.getIndices().add(startIndex);
        chunkMesh.getIndices().add(startIndex + 1);
        chunkMesh.getIndices().add(startIndex + 2);
        chunkMesh.getIndices().add(startIndex + 1);
        chunkMesh.getIndices().add(startIndex + 3);
        chunkMesh.getIndices().add(startIndex + 2);

        // only create normals, tangents, uv's, ... for non-collision meshes
        if (!chunkMesh.isCollisionMesh()) {
            chunkMesh.getNormals().add(new Vector3f(0.0f, 1.0f, 0.0f));
            chunkMesh.getNormals().add(new Vector3f(0.0f, 1.0f, 0.0f));
            chunkMesh.getNormals().add(new Vector3f(0.0f, 1.0f, 0.0f));
            chunkMesh.getNormals().add(new Vector3f(0.0f, 1.0f, 0.0f));

            chunkMesh.getUvs().add(new Vector2f(1.0f, 1.0f));
            chunkMesh.getUvs().add(new Vector2f(0.0f, 1.0f));
            chunkMesh.getUvs().add(new Vector2f(1.0f, 0.0f));
            chunkMesh.getUvs().add(new Vector2f(0.0f, 0.0f));
        }

    }

}

To complete the example, i’ll add a ‘wooden plate’ block that uses this shape:

ShapeRegistry shapeRegistry = BlocksConfig.getInstance().getShapeRegistry();
shapeRegistry.register("plate", new Square());

Block woodenPlate = Block.builder()
                .name("wooden_plate")
                .type("oak_planks")
                .shape("plate")
                .solid(true)
                .build();

BlockRegistry blockRegistry = BlocksConfig.getInstance().getBlockRegistry();
blockRegistry.register(woodenPlate);

You can take a look at the current available shapes and implementations for more input.

2 Likes

In some case, (for example when UVs are complicated,…) I load shapes from JME mesh that I have build in Blender and exported them as Gltf model.

For example here is my custom RoundedCube shape with bitmasking support.

/**
 * A cube shape which all edges are rounded.
 *
 * @author Ali-RS
 */
public class RoundedCube implements Shape {

    private final Map<Integer, Mesh> meshIndex = new HashMap<>();

    public RoundedCube(AssetManager assetManager) {
        Node node = (Node) assetManager.loadModel(
                new ModelKey("Blocks/shapes/rounded-cube-meshes.gltf"));
        for (int i = 0; i < 64; i++) {
            String name = toBinaryString(i, 6);
            meshIndex.put(i, ((Geometry) node.getChild(name)).getMesh());
        }
    }

    @Override
    public void add(Vec3i location, Chunk chunk, ChunkMesh chunkMesh) {
        // get the block scale, we multiply it with the vertex positions
        float blockScale = BlocksConfig.getInstance().getBlockScale();

        int sideMask = 0;

        if(chunk.isFaceVisible(location, Direction.TOP)) {
            sideMask |= 0b000001;
        }
        if(chunk.isFaceVisible(location, Direction.BOTTOM)) {
            sideMask |= 0b000010;
        }
        if(chunk.isFaceVisible(location, Direction.LEFT)) {
            sideMask |= 0b000100;
        }
        if(chunk.isFaceVisible(location, Direction.RIGHT)) {
            sideMask |= 0b001000;
        }
        if(chunk.isFaceVisible(location, Direction.FRONT)) {
            sideMask |= 0b010000;
        }
        if(chunk.isFaceVisible(location, Direction.BACK)) {
            sideMask |= 0b100000;
        }

        Mesh mesh = meshIndex.get(sideMask);
        ShapeUtils.fillFromMesh(mesh, location, chunkMesh, blockScale);
    }

    private static String toBinaryString(int x, int len) {
        return String.format("%" + len + "s",
                Integer.toBinaryString(x)).replaceAll(" ", "0");
    }
}
2 Likes

For the more complicated shapes (stairs etc) I also used this approach.
I modeled the shape in Blender, exported and imported the gltf file in jme and used a utility class to extract the mesh and append it to the chunk mesh.

1 Like

Thanks for the info. Given that I’m writing an importer and not a few different shapes, a variation of this method seems like the best idea. It would be nice to be able to re-use the code for loading these meshes into standalone spatials.
The format I’m importing from (Minecraft blockmodels) also specifies when faces should be culled based on what blocks they’re adjacent to. My idea on how to do that was to write a subclass of Mesh that could store this data. Is that a good idea? How does that compare to what you did?

(ps, how does JMonkeyEngine handle material slots? What would I need to do if I wanted a different texture on different sides of the block?)

I can’t help but noticing that Cube generates each vertex 3 times, once for each connecting face. This can’t be good for performance, so does JMonkeyEngine have some special case that looks for these and merges them? Does Blocks do it? Is this okay for me to do that in my custom shape generator (which will also be used to generate standalone geometries).

A “vertex” is a combination of the UNIQUE position+normal+texture coordinate.

Just because a vertex shares a position with another vertex does not make it the same vertex.

1 Like

This is also available in Blocks. It is used for constructing the meshes of a chunk to not render unwanted triangles.

I use the ChunkMesh class, you can and should reuse it if you want to adapt or extend the mesh of a chunk. It’s just a bean that holds lists of positions, indices, uv’s, … and has a utility method to turn those lists of data into a Mesh.

As paul said, a vertex is not only a position. For example the upper south-east corner of a cube, will indeed have 3 ‘vertices’. One for the upper face, one for the south face and one for the east face. But each of those 3 will have a different normal, tangent, uv, …

The UV coordinates specify what part of the texture is used.

A new release of Blocks is available: v1.6.0-alpha. Version 1.6 has some major changes in the shape implementations and comes with a new standard in block id generation. Depending on the usage of the framework this release will not be backwards compatible with previous versions of Blocks.

An overview of the most important (visible) changes:

  • Upgraded Minie to v1.7
  • Add a new block type that supports an overlay color using an overlay map
  • Ability to register blocks from a file (yaml or json)
  • Add a BlockFactory to create blocks from a BlockDefinition or definition file (yaml or json)
  • Add leaves blocks

The complete list of changes is available on github.

Some examples:

Registering a block using a file:
Yaml:

- name: my-block-from-yaml
  type: grass
  shape: wedge_north
  solid: false
  transparent: true
  multiTexture: true

- type: oak_log

Json:

[
  {
    "type": "birch_planks",
    "shape": "stairs_north"
  }
]

The shape, solid, transparent and multiTexture parameters are optional. When a name (id) is not specified, it will be generated.

To register blocks from file:

BlockRegistry blockRegistry = BlocksConfig.getInstance().getBlockRegistry();
blockRegistry.load(InputStream);

Change the overlay color of a block that is using the new overlay material:

TypeRegistry typeRegistry = BlocksConfig.getInstance().getTypeRegistry();
grassMaterial = typeRegistry.get(TypeIds.GRASS);
grassMaterial.setColor("OverlayColor", ColorRGBA.randomColor());

7 Likes

@remy_vd Is there an example or tutorial for using the PhysicsChunkPager and the PhysicsChunkPagerState? I can run the PhysicsScene example OK, but when I try to use the PhysicsChunkPager I get native AccessViolations in jbullet. It would be good to have a simple example to follow.

I thought I created an example for all the pagers, but apparently I forget one for the Physics pager. I’ll add it on my backlog.

The PhysicsChunkPager works exactly the same as the ChunkPager. It attaches and detaches rigidbodies of chunk collision meshes to/from the physics space based on the location that it is given. The size of the physics grid can be configured in the BlocksConfig object.

You can use the PhysicsChunkPagerState to easily initialize, update and cleanup the PhysicsChunkPager. You can also choose to do this manually or use a GameSystem for example.

In short it should be something like this:

// set the size of the grid, the default physics grid is (5,3,5)
BlocksConfig.getInstance().setPhysicsGrid(new Vec3i(3, 1, 3));

// retrieve the physics space
BulletAppState bulletAppState = stateManager.getState(BulletAppState.class);
PhysicsSpace physicsSpace = bulletAppState.getPhysicsSpace();

// attach the pager
PhysicsChunkPager physicsChunkPager = new PhysicsChunkPager(physicsSpace, chunkManager);
stateManager.attach(new PhysicsChunkPagerState(physicsChunkPager));

If you want to move the center chunk of the grid, call PhysicsChunkPagerState#setLocation(Vector3f) or PhysicsChunkPager#setLocation(Vector3f).

If you still encouter an error, paste the code and the stacktrace here for more easy debugging.

1 Like

looks like an excellent project. It’s on my to do list to add it to my product. thanks!

1 Like

Thanks. I got it working by following your examples.
One issue I ran into was an error java.lang.NoSuchFieldError: normal
It turns out I had to remove all jbullet dependencies from my project and add Minie dependencies instead. I think I had those jbullet Maven entries left from previous attempts at getting physics working (without Blocks framework). You may want to mention this in any tutorial you write. It is not obvious to newcomers.

1 Like

I got an exception at runtime after upgrading to 1.6.0-alpha
java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/TSFBuilder
To fix, I had to add jackson-core as a runtime dependency in Maven.

That is probably related to some transitive dependencies. I’ll have a look.

I have a question about the performance of Block engine. I am hoping I can improve the performance when loading new Chunks.
My issue is that it takes a long time for the Chunks to actually become visible on screen. Generating the Chunks (via my ChunkGenerator) is quick. Adding the chunks to the physics engine is also quick. I can tell that the chunks have been added to the collision mesh because I can walk on them, even though I can’t see them.
Is there anything I can do to improve the performance here? Is this a part of the Blocks engine, or does this step happen in jmonkeyengine itself?
This may just be a limitation of what I am trying to do. Perhaps I am pushing the limits of how many blocks you can have visible on screen at any one time. I have seen similar behaviour in Minecraft.

I am talking about waiting 20-30+ seconds in some cases for chunks to become visible.

Another thing I have noticed (which I hope could be improved) is that distant Chunks can become visible well before Chunks that are nearby (or even chunks that I am standing on). Is there any prioritising done to load nearby chunks before distant ones?

To be clear; the chunks are generated and added to the physics mesh fine, but then take a long time to become visible on screen.

When using the pager to attach the chunks of the grid to the scene graph, multiple steps are executed:

  1. Try to fetch the chunk from the cache
  2. If chunk was not found in the cache, try to load the chunk from the repository
  3. If chunk was not found in the repository, try to generate it
  4. If meshes need (re)generation, generate them

The pager is possibly doing all 4 steps for each chunk in the grid. If you have a grid of 9x5x9, you have 405 chunks that are requested. No matter what you do, this will take some time.
To improve the perfomance, try to figure out what step is consuming the most time. When you enable the logging for the ChunkManager, timings are displayed for each step.
Try to increase the poolsize of the executorservice for that step and see if that helps.

Another way would be to show a loading screen until the chunkcache contains 80% (or another percentage) of the chunks. A chunk is added to the cache (and thus retrievable) when it is fully ‘ready’.

This really depends on the situation and complexity of the chunk. Are you doing heavy generation logic (noise, samples, octaves, …) and is it a complex mesh to generate?
I didn’t dive into very complex terrain generation yet, so I can’t really give good benchmarks. For mesh generation I had results from 1ms to 100+ms depending on the chunksize, complexity etc.

Correct, at this moment, the chunkmanager works with a first come, first serve principle. The chunk that is requested first, will be handled first. There is no custom logic to sort the requested queue of chunks.
I was thinking of adding a function to sort the queue (by default it would be an identity function so the behaviour will stay the same) that could be overwritten.

If you would use the loading screen approach, this behaviour will not occur. When moving, the directly surrounding chunks will already be in the cache and thus visible. Only the edges of the grid will be requested and show the pop-in effect.

1 Like

@remy_vd I enabled logging and was able to fix a few things.

  • I added calls to ChunkPager.setGridLowerBounds and ChunkPager.setGridUpperBounds. This seemed to help a lot straight up.
  • I stopped adding Blocks outside the chunk boundaries. This was creating a lot of noise in the logs.

I am now struggling with what to return from the ChunkGenerator for “empty” chunks. These are chunks that are deep underground and don’t need to be seen and are not required.

I was initially returning null for these chunks. I noticed a lot of exceptions in the log
Caused by: java.lang.NullPointerException
at com.rvandoosselaer.blocks.ChunkManager$GeneratorCallable.call(ChunkManager.java:669)
at com.rvandoosselaer.blocks.ChunkManager$GeneratorCallable.call(ChunkManager.java:660)

Basically it is not expecting to get nulls back from the ChunkGenerator. It tries to call update() on them.

So I tried returning empty Chunks instead of null. i.e. create the Chunks but with no calls to setBlock(). This worked OK with physics turned off. If I turned physics on my application crashes with a EXCEPTION_ACCESS_VIOLATION inside bulletjme.dll.

I then tried returning full Chunks. This absolutely kills performance unfortunately. It takes 5 minutes to finish rendering what previously was taking a few seconds.

Is there a reason to not allow returning null from ChunkGenerator? Chunks are very expensive in RAM, so it would be good to not create them if they contain no useful information. Or perhaps have a way of creating a EMPTY chunk that does not require allocating the Block array. To give you an idea, I cut my memory usage by around 70% when I initially switched from returning Empty Chunks to nulls.

I had a look at the timings in the log. I couldn’t see anything useful there. Most things are reporting 0ms. Some occasionally are a bit more (10ms).

1 Like

Probably caused by trying to create a collision mesh with a mesh that contains no data.

1 Like

An EXCEPTION_ACCESS_VIOLATION should result in the JVM dumping a crash log. Buried in the crash log there will be a stack trace. I, for one, am very interested in seeing that stack trace.

Yes reducing the bounds of the grid will stop it from trying to load/generate chunks outside of the grid boundaries.

That’s very good as this makes no sense, hence the warning :slight_smile:
If you directly add blocks to a chunk, it will log a warning when the chunk doesn’t contain the block location. You should use ChunkManager#addBlock instead. This will locate the correct chunk for you and you can just use block ‘world’ locations. So no need to calculate stuff on your own. When you use Chunk#addBlock you need to provide local block coordinates. If your chunk is 32x32x32, it will log a warning when you try to add a block to for example (0, 41, 16).
Normally you shouldn’t even worry about chunk management, that’s what the ChunkManager does. You should use ChunkManager#addBlock and ChunkManager#removeBlock to place and remove blocks.

Yes, only the ChunkRepository may return null chunks. I figured it makes no sense that the result of a chunk generation would be null.
The pager will request only chunks that are in the bounds of the grid. The generation logic should either return full chunks (under ground chunks), empty chunks (air) or half filled chunks (terrain crust). If you want an empty chunk, just return Chunk.createAt(new Vec3i(0,0,0)).

Mmm, this might be a bug in Blocks/Minie. As @jayfella mentioned, this is thrown when trying to a create a collisionshape mesh that is empty.

edit: I was indeed able to reproduce it. I can (and should) add an additional check to skip creating a physics object for empty chunks. if (... || chunk.isEmpty() || ....)

@sgold stacktrace:

Current thread (0x00007f7f451f5000):  JavaThread "jME3 Main" [_thread_in_native, id=91399, stack(0x000070000d0d1000,0x000070000d1d1000)]

Stack: [0x000070000d0d1000,0x000070000d1d1000],  sp=0x000070000d1d05a0,  free space=1021k
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [libbulletjme.dylib+0x74fea]  btQuantizedBvh::buildTree(int, int)+0xca
C  [libbulletjme.dylib+0x11742f]  btOptimizedBvh::build(btStridingMeshInterface*, bool, btVector3 const&, btVector3 const&)+0x59f
C  [libbulletjme.dylib+0x7d10c]  btBvhTriangleMeshShape::btBvhTriangleMeshShape(btStridingMeshInterface*, bool, bool)+0x7c
C  [libbulletjme.dylib+0x74706]  Java_com_jme3_bullet_collision_shapes_MeshCollisionShape_createShape+0x46
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.createShape(ZZJ)J+0
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.createShape([B)V+30
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.<init>([Lcom/jme3/scene/Mesh;)V+35
j  com.rvandoosselaer.blocks.examples.PhysicsScene.simpleUpdate(F)V+121
j  com.jme3.app.SimpleApplication.update()V+82
j  com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop()V+22
j  com.jme3.system.lwjgl.LwjglDisplay.runLoop()V+104
j  com.jme3.system.lwjgl.LwjglAbstractDisplay.run()V+136
j  java.lang.Thread.run()V+11 java.base@11.0.7
v  ~StubRoutines::call_stub
V  [libjvm.dylib+0x399a52]  JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0x21a
V  [libjvm.dylib+0x398e9c]  JavaCalls::call_virtual(JavaValue*, Klass*, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0xee
V  [libjvm.dylib+0x398f58]  JavaCalls::call_virtual(JavaValue*, Handle, Klass*, Symbol*, Symbol*, Thread*)+0x62
V  [libjvm.dylib+0x41cdc9]  thread_entry(JavaThread*, Thread*)+0x78
V  [libjvm.dylib+0x6ec12c]  JavaThread::thread_main_inner()+0x82
V  [libjvm.dylib+0x6ebf76]  JavaThread::run()+0x174
V  [libjvm.dylib+0x6e9e52]  Thread::call_run()+0x68
V  [libjvm.dylib+0x5f18a3]  thread_native_entry(Thread*)+0x139
C  [libsystem_pthread.dylib+0x6109]  _pthread_start+0x94
C  [libsystem_pthread.dylib+0x1b8b]  thread_start+0xf

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.createShape(ZZJ)J+0
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.createShape([B)V+30
j  com.jme3.bullet.collision.shapes.MeshCollisionShape.<init>([Lcom/jme3/scene/Mesh;)V+35
j  com.rvandoosselaer.blocks.examples.PhysicsScene.simpleUpdate(F)V+121
j  com.jme3.app.SimpleApplication.update()V+82
j  com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop()V+22
j  com.jme3.system.lwjgl.LwjglDisplay.runLoop()V+104
j  com.jme3.system.lwjgl.LwjglAbstractDisplay.run()V+136
j  java.lang.Thread.run()V+11 java.base@11.0.7
v  ~StubRoutines::call_stub```
1 Like