pyvista-support icon indicating copy to clipboard operation
pyvista-support copied to clipboard

Plotting Minecraft blocks with textures efficiently

Open siyan-sylvia-li opened this issue 3 years ago • 7 comments

Hello! I have recently started using PyVista and I like it very much! Thank you for creating this tool!

I am trying to visualize Minecraft structures with PyVista so that people can view them outside of Minecraft. The structures will be created with NBT files.

Data Structures

NBT files contain a list of blocks and a palette for the types of blocks (e.g. water, wood plank, grass, etc). Each block has a 'pos' property for x y z coordinates and a 'state' property corresponding to the type of blocks. The number of blocks can be in the thousands, and there can be tens of types of blocks involved. I have attached an example NBT file here. example_data.zip

Specific Problem

In addition to plot these blocks, I need different textures with them as well. A "face image" is also attached in the zip file above. Basically I want a block of a certain type to have their corresponding face texture. Block_of_Emerald_JE4_BE3_face

How do I plot them? I currently add meshes sequentially, but that is being slow. Should I try to use an UnstructuredGrid?

    for type_block in cubes:
        for b in cubes[type_block]:`
            p.add_mesh(pv.Cube(center=np.asarray(b)), texture=pv.read_texture(texture_dict[type_block]))

Any help would be appreciated! Thank you again!

siyan-sylvia-li avatar Mar 05 '21 02:03 siyan-sylvia-li

I'd try to use an unstructured grid as this will mirror how minecraft displays textures. Effectively, and I'm simplifying here, minecraft only worries about the faces of a block that are external and "visible" and it will compute the shading of those blocks. When VTK displays a mesh, it extracts the external surface of a mesh and then displays that.

The only thing I'm not sure about is how to correctly map the texture onto the surface of each face. Can you provide a minimum working example? My kids (and sometimes I) play play minecraft, and I'd like to see a demo of this with VTK.

akaszynski avatar Mar 05 '21 02:03 akaszynski

Thank you for such a speedy reply! And sure thing! Let me find a smaller NBT file and move a couple of things around and attach it here! You would need to install numpy and nbtlib too.

siyan-sylvia-li avatar Mar 05 '21 02:03 siyan-sylvia-li

working_example.zip You can choose to either load the Oak Tree file (slow, took around 15 secs on my laptop) or the Gravel Path (very fast on my end) by commenting out lines in the python file.

siyan-sylvia-li avatar Mar 05 '21 02:03 siyan-sylvia-li

This was fun! Got the time down to 8 ms to generate the mesh and another 100 to render. Render time will likely stay fixed. Here's the code:

import pyvista as pv
import numpy as np
import nbtlib


# Copying from Djordje's code
def extract_nbt_blocks(load_file):
    nbt_struct = nbtlib.load(load_file)
    nbt_blocks = nbt_struct.root["blocks"]
    nbt_palette = nbt_struct.root["palette"]

    block_data = dict()
    for b in nbt_blocks:
        type_name = nbt_palette[b['state']]['Name'].replace('minecraft:', "")
        if type_name != 'air':
            if type_name not in block_data:
                block_data[type_name] = [b['pos']]
            else:
                block_data[type_name].append(b['pos'])
    return block_data


texture_dict = {'leaves2': 'TempBlockStorage/block_face_images/Dark_Oak_Leaves_BE2_face.png',
                'log2': 'TempBlockStorage/block_face_images/Dark_Oak_Log_(UD)_JE3_face.png',
                'gravel': 'TempBlockStorage/block_face_images/Gravel_JE5_BE4_face.png'}
# Loading Oak Tree (A HUGE structure, might take quite a while)
block_data = extract_nbt_blocks("NBTs/Extra_dark_oak.nbt")

# Loading Gravel Path (A lot faster)
# block_data = extract_nbt_blocks("NBTs/villages_gravel_path3x1x16.nbt")


###############################################################################
# Use glyphs to generate meshes for each block type

# create a basic (1 x 1 x 1) cube
cube = pv.Cube()

blocks = []
for block_type in block_data:

    # create a "point cloud" containing the centers of every cube
    mesh = pv.PolyData(np.array(block_data[block_type]))

    # use a glyph filter to create a cube at each point in polydata
    glyphs = mesh.glyph(geom=cube)
    glyphs.textures[block_type] = pv.read_texture(texture_dict[block_type])
    blocks.append(glyphs)


###############################################################################
# Plot

# Use 3 lights because it's a bit brighter
pl = pv.Plotter(lighting=None)
pl.background_color = 'w'
pl.enable_3_lights()

# we have to add block types one at a type since it seems we can only
# apply one texture per block type.  This may complicate things with complicated or mixed block types.
for mesh in blocks:
    pl.add_mesh(mesh)

# nice camera position
pl.camera_position = [(-62.243920852008635, 32.9620996859325, -20.346306010403953),
                      (11.0, 12.5, 12.0),
                      (0.23672597767457984, 0.9685471459959899, 0.07666314288520223)]

# this returns  camera position that you can use later for automated plots
# here, we also save a png screenshot
cpos = pl.show(screenshot='tree.png')

tree

akaszynski avatar Mar 05 '21 06:03 akaszynski

This is incredible! Thank you so much again!

siyan-sylvia-li avatar Mar 05 '21 07:03 siyan-sylvia-li

It would be great to get additional examples on our examples site. Do you have other nbts we could show? Perhaps something bigger?

akaszynski avatar Mar 05 '21 15:03 akaszynski

The larger NBT files often require having blocks in a certain orientation, such as stairs and vines, so we wouldn't be able to drape the same texture over all the faces ... I suppose that would mean trying to use a different type of geom for them?

I am also trying to allow interactive editing of the plot, where a human can click a button on the interface, then right click to add or delete blocks. I have looked at #239 and have decided to take a similar approach, by finding the closest point and then deleting the points corresponding to the cube the point is connected to, then overwriting the mesh. Please let me know if this is a desirable approach.

siyan-sylvia-li avatar Mar 06 '21 04:03 siyan-sylvia-li