Add everything

master
random-geek 2019-06-27 21:56:35 -07:00
parent 11110ea82f
commit e7fd2a1b4b
9 changed files with 1087 additions and 2 deletions

View File

@ -1,2 +1,67 @@
# meshport
Easily export areas in Minetest to meshes for 3D rendering.
# Meshport (Minetest Mesh Exporter)
![screenshot](screenshot.png)
Meshport is a mod which allows easy exporting of scenes from Minetest to a `.obj` file, complete with materials and textures. These models can be imported directly into Blender or another 3D program for rendering and animation.
This mod is still in the "alpha" phase; as such, many types of nodes are not yet able to be exported. See below for more details.
## Usage
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported, then use `/meshport` to export the mesh. The saved `.obj` and `.mtl` files will be located in the `meshport` folder of the world directory, within a subfolder.
### Importing into Blender
Once the model is exported, you should be able to import the `.obj` file with default settings. Make sure "Image Search" in the import settings is selected to ensure the textures are imported as well. Texture modifiers are ignored, so some materials will likely have to be fixed by hand.
#### Fixing interpolation
If you intend to render the scene, you will want to set the interpolation mode of all the image textures to "Closest" in order to keep the pixelated look. This can either be done manually or by running this script in Blender's text editor:
```python
import bpy
for mat in bpy.data.materials:
try:
nodes = mat.node_tree.nodes
for node in nodes:
if node.type == "TEX_IMAGE":
node.interpolation = "Closest"
except:
continue
```
#### Fixing vertex normals
Some mesh nodes may not have any vertex normals, which can lead to lighing problems. To fix this, what I have found to work is to first select the all the problematic nodes, either manually or by selecting by material in edit mode; then, mark the selected edges as sharp, and then average the normals by face area.
Additional tip: Use an HDRI sky texture (such as one from [here](https://hdrihaven.com)) for awesome-looking renders. ;)
## Supported features
At the moment, only the following node drawtypes are supported:
- Cubic drawtypes, including `normal`, `glasslike`, `allfaces`, and their variants (see below)
- `nodebox`
- `mesh` (only `.obj` meshes are exported)
Many special rendering features are not yet supported.
### A note on cubic nodes
Due to the differences between Minetest's rendering engine and 3D programs such as Blender, it is not possible to exactly replicate how certain cubic nodes are rendered in Minetest. Instead, to avoid duplicate faces, a face priority system is used as follows:
| Priority level | Drawtypes |
|----------------|----------------------------------------------------|
| 4 | `normal` |
| 3 | `glasslike` |
| 2 | `glasslike_framed` and `glasslike_framed_optional` |
| 1 | `allfaces` and `allfaces_optional` |
| 0 | All other nodes |
In places where two nodes of different drawtypes touch, only the face of the node with the higher priority drawtype will be drawn. For `allfaces` type nodes (such as leaves), interior faces will be drawn only when facing X+, Y+, or Z+ in the Minetest coordinate space.
## License
All code is licensed under the GNU LGPL v3.0.

189
api.lua Normal file
View File

@ -0,0 +1,189 @@
meshport.Faces = {}
function meshport.Faces:new()
local o = {
faces = {},
}
self.__index = self
setmetatable(o, self)
return o
end
function meshport.Faces:insert_face(face)
table.insert(self.faces, face)
end
function meshport.Faces:copy()
local newFaces = meshport.Faces:new()
-- Using `table.copy` on all of `self.faces` does not work here.
newFaces.faces = table.copy(self.faces)
-- for _, face in ipairs(self.faces) do
-- table.insert(newFaces.faces, table.copy(face))
-- end
return newFaces
end
function meshport.Faces:translate(vec)
for _, face in ipairs(self.faces) do
for i, vert in ipairs(face.verts) do
face.verts[i] = vector.add(vert, vec)
end
end
end
function meshport.Faces:rotate_by_facedir(facedir)
if facedir == 0 then
return
end
for _, face in ipairs(self.faces) do
-- Rotate vertices.
for i = 1, #face.verts do
face.verts[i] = meshport.rotate_vector_by_facedir(face.verts[i], facedir)
end
-- Rotate vertex normals.
for i = 1, #face.vert_norms do
face.vert_norms[i] = meshport.rotate_vector_by_facedir(face.vert_norms[i], facedir)
end
end
end
function meshport.Faces:apply_tiles(nodeDef)
local tile
for _, face in ipairs(self.faces) do
tile = meshport.get_tile(nodeDef.tiles, face.tile_idx)
face.texture = tile.name or tile
end
end
meshport.Mesh = {}
function meshport.Mesh:new()
local o = {
verts = {},
vert_norms = {},
tex_coords = {},
faces = {},
}
setmetatable(o, self)
self.__index = self
return o
end
function meshport.Mesh:insert_face(face)
local indices = {
verts = {},
vert_norms = {},
tex_coords = {},
}
local elementStr, vec
-- Add vertices to mesh.
for i, vert in ipairs(face.verts) do
-- Invert Z axis to comply with Blender's coordinate system.
vec = meshport.clean_vector({x = vert.x, y = vert.y, z = -vert.z})
elementStr = string.format("v %f %f %f\n", vec.x, vec.y, vec.z)
indices.verts[i] = meshport.find_or_insert(self.verts, elementStr)
end
-- Add texture coordinates (UV map).
for i, texCoord in ipairs(face.tex_coords) do
elementStr = string.format("vt %f %f\n", texCoord.x, texCoord.y)
indices.tex_coords[i] = meshport.find_or_insert(self.tex_coords, elementStr)
end
-- Add vertex normals.
for i, vertNorm in ipairs(face.vert_norms) do
-- Invert Z axis.
vec = meshport.clean_vector({x = vertNorm.x, y = vertNorm.y, z = -vertNorm.z})
elementStr = string.format("vn %f %f %f\n", vec.x, vec.y, vec.z)
indices.vert_norms[i] = meshport.find_or_insert(self.vert_norms, elementStr)
end
-- Add faces to mesh.
local vertStrs = {}
local vertList = {}
for i = 1, #indices.verts do
vertList = table.insert(vertStrs, table.concat({
indices.verts[i],
-- If there is a vertex normal but not a texture coordinate, insert a blank string here.
indices.tex_coords[i] or (indices.vert_norms[i] and ""),
indices.vert_norms[i],
}, "/"))
end
self.faces[face.texture] = self.faces[face.texture] or {}
table.insert(self.faces[face.texture], string.format("f %s\n", table.concat(vertStrs, " ")))
end
function meshport.Mesh:write_obj(path)
local objFile = io.open(path .. DIR_DELIM .. "/model.obj", "w")
objFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n")
objFile:write("mtllib materials.mtl\n")
-- Write vertices.
for _, vert in ipairs(self.verts) do
objFile:write(vert)
end
-- Write texture coordinates.
for _, texCoord in ipairs(self.tex_coords) do
objFile:write(texCoord)
end
-- Write vertex normals.
for _, vertNorm in ipairs(self.vert_norms) do
objFile:write(vertNorm)
end
-- Write faces, sorted in order of material.
for mat, faces in pairs(self.faces) do
objFile:write(string.format("usemtl %s\n", mat))
for _, face in ipairs(faces) do
objFile:write(face)
end
end
objFile:close()
end
function meshport.Mesh:write_mtl(path, playerName)
local textures = meshport.get_asset_paths("textures")
local matFile = io.open(path .. "/materials.mtl", "w")
matFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n\n")
-- Write material information.
for mat, _ in pairs(self.faces) do
matFile:write(string.format("newmtl %s\n", mat))
-- Attempt to get the base texture, ignoring texture modifiers.
local texName = string.match(mat, "[%w%s%-_%.]+%.png") or mat
if textures[texName] then
if texName ~= mat then
meshport.print(playerName, "warning", string.format("Ignoring texture modifers in material %q.", mat))
end
matFile:write(string.format("map_Kd %s\n\n", textures[texName]))
else
meshport.print(playerName, "warning",
string.format("Could not find texture %q. Using a dummy material instead.", texName))
matFile:write(string.format("Kd %f %f %f\n\n", math.random(), math.random(), math.random()))
end
matFile:write("\n\n")
end
matFile:close()
end

175
export.lua Normal file
View File

@ -0,0 +1,175 @@
meshport.nodebox_cache = {}
meshport.mesh_cache = {}
meshport.cube_face_priority = {
allfaces = 1,
glasslike_framed = 2,
glasslike = 3,
normal = 4,
}
function meshport.create_cube_node(nodeDrawtype, nodeTiles, pos, facedir, neighbors)
local sideFaces = {
{{x = 0.5, y = 0.5, z =-0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x =-0.5, y = 0.5, z =-0.5}}, -- Y+
{{x = 0.5, y =-0.5, z = 0.5}, {x = 0.5, y =-0.5, z =-0.5}, {x =-0.5, y =-0.5, z =-0.5}, {x =-0.5, y =-0.5, z = 0.5}}, -- Y-
{{x = 0.5, y =-0.5, z = 0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x = 0.5, y = 0.5, z =-0.5}, {x = 0.5, y =-0.5, z =-0.5}}, -- X+
{{x =-0.5, y =-0.5, z =-0.5}, {x =-0.5, y = 0.5, z =-0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x =-0.5, y =-0.5, z = 0.5}}, -- X-
{{x =-0.5, y =-0.5, z = 0.5}, {x =-0.5, y = 0.5, z = 0.5}, {x = 0.5, y = 0.5, z = 0.5}, {x = 0.5, y =-0.5, z = 0.5}}, -- Z+
{{x = 0.5, y =-0.5, z =-0.5}, {x = 0.5, y = 0.5, z =-0.5}, {x =-0.5, y = 0.5, z =-0.5}, {x =-0.5, y =-0.5, z =-0.5}}, -- Z-
}
local texCoords = {{x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}}
local faces = meshport.Faces:new()
-- For glasslike_framed nodes, only the first tile is used.
local tileIdx = nodeDrawtype == "glasslike_framed" and 1 or nil
local neighborDrawtype, vertNorm
for i = 1, 6 do
neighborDrawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(neighbors[i]).drawtype)
if meshport.cube_face_priority[nodeDrawtype] > (meshport.cube_face_priority[neighborDrawtype] or 0)
-- For allfaces nodes (such are leaves), interior faces are drawn only when facing X+, Y+, or Z+.
or (nodeDrawtype == "allfaces" and neighborDrawtype == "allfaces" and i % 2 == 1) then
vertNorm = meshport.neighbor_dirs[i]
faces:insert_face(meshport.prepare_cuboid_face({
verts = sideFaces[i],
tex_coords = texCoords,
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
tile_idx = tileIdx,
}, nodeTiles, pos, facedir, i))
end
end
return faces
end
function meshport.create_nodebox_node(nodeName, pos, facedir, param2, neighbors)
local nodeDef = minetest.registered_nodes[nodeName]
if not meshport.nodebox_cache[nodeName] then
meshport.nodebox_cache[nodeName] = meshport.prepare_nodebox(nodeDef.node_box)
end
local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, facedir, param2, neighbors)
if meshport.nodebox_cache[nodeName].type ~= "connected" then
boxes:rotate_by_facedir(facedir)
end
return boxes:to_faces(nodeDef.tiles, pos, facedir)
end
function meshport.create_mesh_node(nodeDef, playerName)
local meshName = nodeDef.mesh
if not meshName then
return
end
if not meshport.mesh_cache[meshName] then
-- Get the paths of all .obj meshes.
if not meshport.obj_paths then
meshport.obj_paths = meshport.get_asset_paths("models", ".obj")
end
if not meshport.obj_paths[meshName] then
if string.lower(string.sub(meshName, -4)) ~= ".obj" then
meshport.print(playerName, "warning", string.format("Mesh %q is not supported.", meshName))
else
meshport.print(playerName, "warning", string.format("Mesh %q could not be found.", meshName))
end
-- Cache a blank faces object so the player isn't warned again.
meshport.mesh_cache[meshName] = meshport.Faces:new()
else
meshport.mesh_cache[meshName] = meshport.parse_obj(meshport.obj_paths[meshName])
end
end
return meshport.mesh_cache[meshName]:copy()
end
function meshport.create_node(idx, area, content, param2, playerName)
if content[idx] == minetest.CONTENT_AIR or content[idx] == minetest.CONTENT_IGNORE then
return
end
local nodeDef = meshport.get_def_from_id(content[idx])
if not nodeDef.drawtype or nodeDef.drawtype == "airlike" then
return
end
local nodeDrawtype = meshport.get_aliased_drawtype(nodeDef.drawtype)
local facedir = meshport.get_facedir(param2[idx], nodeDef.paramtype2)
local isCubicType = meshport.cube_face_priority[nodeDrawtype] or nodeDrawtype == "nodebox"
local neighbors, faces
if isCubicType then
neighbors = meshport.get_node_neighbors(content, area, idx)
end
if meshport.cube_face_priority[nodeDrawtype] then
faces = meshport.create_cube_node(nodeDrawtype, nodeDef.tiles, area:position(idx), facedir, neighbors)
elseif nodeDrawtype == "nodebox" then
faces = meshport.create_nodebox_node(
minetest.get_name_from_content_id(content[idx]), area:position(idx), facedir, param2[idx], neighbors)
elseif nodeDrawtype == "mesh" then
faces = meshport.create_mesh_node(nodeDef, playerName)
end
if not faces then
return
end
if not isCubicType then
faces:rotate_by_facedir(facedir)
end
faces:apply_tiles(nodeDef)
return faces
end
function meshport.create_mesh(playerName, p1, p2)
meshport.print(playerName, "info", "Generating mesh...")
p1, p2 = vector.sort(p1, p2)
local vm = minetest.get_voxel_manip()
-- Add one node of padding to area so we can read neighbor blocks.
local vp1, vp2 = vm:read_from_map(vector.subtract(p1, 1), vector.add(p2, 1))
local content = vm:get_data()
local param2 = vm:get_param2_data()
-- Create a VoxelArea for converting from flat array indices to position vectors.
local vArea = VoxelArea:new{MinEdge = vp1, MaxEdge = vp2}
local mesh = meshport.Mesh:new()
local faces
-- Loop through all positions in the desired area.
for idx in vArea:iterp(p1, p2) do
-- Generate a mesh for the node.
faces = meshport.create_node(idx, vArea, content, param2, playerName)
if faces then
-- Move the node to its proper position in the mesh.
faces:translate(vector.add(vector.subtract(vArea:position(idx), p1), 0.5))
for _, face in ipairs(faces.faces) do
-- Add each face to our final mesh.
mesh:insert_face(face)
end
end
end
-- Create path for exported mesh.
local path = string.format("%s%smeshport%s%s_%s",
minetest.get_worldpath(), DIR_DELIM, DIR_DELIM, playerName, os.date("%Y-%m-%d_%H-%M-%S"))
minetest.mkdir(path)
mesh:write_obj(path)
mesh:write_mtl(path, playerName)
meshport.print(playerName, "info", "Finished.")
end

299
helpers.lua Normal file
View File

@ -0,0 +1,299 @@
meshport.neighbor_dirs = {
{x = 0, y = 1, z = 0}, -- Y+
{x = 0, y = -1, z = 0}, -- Y-
{x = 1, y = 0, z = 0}, -- X+
{x = -1, y = 0, z = 0}, -- X-
{x = 0, y = 0, z = 1}, -- Z+
{x = 0, y = 0, z = -1}, -- Z-
}
meshport.facedir_to_tile_indices = {
[0] =
{1, 2, 3, 4, 5, 6},
{1, 2, 5, 6, 4, 3},
{1, 2, 4, 3, 6, 5},
{1, 2, 6, 5, 3, 4},
{6, 5, 3, 4, 1, 2},
{3, 4, 5, 6, 1, 2},
{5, 6, 4, 3, 1, 2},
{4, 3, 6, 5, 1, 2},
{5, 6, 3, 4, 2, 1},
{4, 3, 5, 6, 2, 1},
{6, 5, 4, 3, 2, 1},
{3, 4, 6, 5, 2, 1},
{4, 3, 1, 2, 5, 6},
{6, 5, 1, 2, 4, 3},
{3, 4, 1, 2, 6, 5},
{5, 6, 1, 2, 3, 4},
{3, 4, 2, 1, 5, 6},
{5, 6, 2, 1, 4, 3},
{4, 3, 2, 1, 6, 5},
{6, 5, 2, 1, 3, 4},
{2, 1, 4, 3, 5, 6},
{2, 1, 6, 5, 4, 3},
{2, 1, 3, 4, 6, 5},
{2, 1, 5, 6, 3, 4},
}
meshport.facedir_to_tile_rotations = {
[0] =
{0, 0, 0, 0, 0, 0},
{3, 1, 0, 0, 0, 0},
{2, 2, 0, 0, 0, 0},
{1, 3, 0, 0, 0, 0},
{0, 2, 3, 1, 2, 0},
{0, 2, 3, 1, 1, 1},
{0, 2, 3, 1, 0, 2},
{0, 2, 3, 1, 3, 3},
{2, 0, 1, 3, 2, 0},
{2, 0, 1, 3, 3, 3},
{2, 0, 1, 3, 0, 2},
{2, 0, 1, 3, 1, 1},
{3, 3, 3, 3, 1, 3},
{3, 3, 2, 0, 1, 3},
{3, 3, 1, 1, 1, 3},
{3, 3, 0, 2, 1, 3},
{1, 1, 1, 1, 3, 1},
{1, 1, 2, 0, 3, 1},
{1, 1, 3, 3, 3, 1},
{1, 1, 0, 2, 3, 1},
{2, 2, 2, 2, 2, 2},
{3, 1, 2, 2, 2, 2},
{0, 0, 2, 2, 2, 2},
{1, 3, 2, 2, 2, 2},
}
meshport.wallmounted_to_facedir = {[0] = 20, 0, 17, 15, 8, 6}
meshport.drawtype_aliases = {
allfaces_optional = "allfaces",
glasslike_framed_optional = "glasslike_framed",
}
function meshport.print(name, level, s)
local message
if level == "info" then
message = minetest.colorize("#00EF00", s)
elseif level == "warning" then
message = minetest.colorize("#EFEF00", "Warning: " .. s)
elseif level == "error" then
message = minetest.colorize("#EF0000", "Error: " .. s)
end
minetest.chat_send_player(name, "[meshport] " .. message)
end
function meshport.find_or_insert(list, value)
local idx = table.indexof(list, value)
-- If the element does not exist, create it.
if idx < 0 then
table.insert(list, value)
idx = #list
end
-- Return the index of the element.
return idx
end
function meshport.clean_vector(vec)
-- Prevents an issue involving negative zero values, which are not handled properly by `string.format`.
return {
x = vec.x == 0 and 0 or vec.x,
y = vec.y == 0 and 0 or vec.y,
z = vec.z == 0 and 0 or vec.z,
}
end
function meshport.rotate_vector_by_facedir(vec, facedir)
local v = vector.new(vec)
local rotY = facedir % 4
local rotSide = (facedir - rotY) / 4
-- Rotate the vector. Values of 0 for either `rotY` or `rotSide` do not change the vector.
if rotY == 1 then
v.x, v.z = v.z, -v.x -- 90 degrees clockwise
elseif rotY == 2 then
v.x, v.z = -v.x, -v.z -- 180 degrees clockwise
elseif rotY == 3 then
v.x, v.z = -v.z, v.x -- 270 degrees clockwise
end
if rotSide == 1 then
v.y, v.z = -v.z, v.y -- Facing Z+
elseif rotSide == 2 then
v.y, v.z = v.z, -v.y -- Facing Z-
elseif rotSide == 3 then
v.x, v.y = v.y, -v.x -- Facing X+
elseif rotSide == 4 then
v.x, v.y = -v.y, v.x -- Facing X-
elseif rotSide == 5 then
v.x, v.y = -v.x, -v.y -- Facing Y-
end
return v
end
function meshport.rotate_texture_coordinate(texCoord, rot)
local vt = table.copy(texCoord)
-- Rotate the vector. Values of components range from 0 to 1, so adding 1 when inverting is necessary.
if rot == 1 then
vt.x, vt.y = vt.y, 1 - vt.x -- 90 degrees clockwise
elseif rot == 2 then
vt.x, vt.y = 1 - vt.x, 1 - vt.y -- 180 degrees clockwise
elseif rot == 3 then
vt.x, vt.y = 1 - vt.y, vt.x -- 270 degrees clockwise
end
return vt
end
function meshport.rotate_texture_coordinates(texCoords, rot)
if rot == 0 then
return texCoords
end
local newTexCoords = {}
for _, texCoord in ipairs(texCoords) do
table.insert(newTexCoords, meshport.rotate_texture_coordinate(texCoord, rot))
end
return newTexCoords
end
function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scale)
-- Get the offset of the tile relative to the lower left corner of the texture.
local texPos = {}
if sideIdx == 1 then
texPos.x = pos.x % 16 % scale
texPos.y = pos.z % 16 % scale
elseif sideIdx == 2 then
texPos.x = pos.x % 16 % scale
texPos.y = scale - pos.z % 16 % scale - 1
elseif sideIdx == 3 then
texPos.x = pos.z % 16 % scale
texPos.y = pos.y % 16 % scale
elseif sideIdx == 4 then
texPos.x = scale - pos.z % 16 % scale - 1
texPos.y = pos.y % 16 % scale
elseif sideIdx == 5 then
texPos.x = scale - pos.x % 16 % scale - 1
texPos.y = pos.y % 16 % scale
elseif sideIdx == 6 then
texPos.x = pos.x % 16 % scale
texPos.y = pos.y % 16 % scale
end
-- Scale and move the texture coordinates.
local newTexCoords = {}
for _, texCoord in ipairs(texCoords) do
table.insert(newTexCoords, {
x = (texCoord.x + texPos.x) / scale,
y = (texCoord.y + texPos.y) / scale,
})
end
return newTexCoords
end
function meshport.prepare_cuboid_face(face, tiles, pos, facedir, sideIdx)
-- If the tile index has not been set manually, assign a tile to the face based on the facedir value.
face.tile_idx = face.tile_idx or meshport.facedir_to_tile_indices[facedir][sideIdx]
local tile = meshport.get_tile(tiles, face.tile_idx)
if tile.align_style == "world" or tile.align_style == "user" then
-- For scaled, world-aligned tiles, scale and reposition the texture coordinates as needed.
if tile.scale and tile.scale ~= 1 then
face.tex_coords = meshport.scale_global_texture_coordinates(face.tex_coords, pos, sideIdx, tile.scale)
end
else
-- If the tile isn't world-aligned, rotate it according to the facedir.
face.tex_coords = meshport.rotate_texture_coordinates(face.tex_coords,
meshport.facedir_to_tile_rotations[facedir][sideIdx])
end
return face
end
function meshport.get_def_from_id(contentId)
return minetest.registered_nodes[minetest.get_name_from_content_id(contentId)] or {}
end
function meshport.get_aliased_drawtype(drawtype)
return meshport.drawtype_aliases[drawtype or ""] or drawtype
end
function meshport.get_facedir(param2, type)
if type == "facedir" or type == "colorfacedir" then
-- For colorfacedir, only the first 5 bits are needed.
return param2 % 32
elseif type == "wallmounted" or type == "colorwallmounted" then
-- For colorwallmounted, only the first 3 bits are needed. If the wallmounted direction is invalid, return 0.
return meshport.wallmounted_to_facedir[param2 % 8] or 0
else
return 0
end
end
function meshport.get_node_neighbors(array, area, idx)
-- Get the node's absolute position from the flat array index.
local pos = area:position(idx)
local neighbors = {}
-- Get the content/param2 value for each neighboring node.
for i = 1, 6 do
neighbors[i] = array[area:indexp(vector.add(pos, meshport.neighbor_dirs[i]))]
end
return neighbors
end
function meshport.node_connects_to(nodeName, connectsTo)
-- If `connectsTo` is a string or nil, turn it into a table for iteration.
if type(connectsTo) ~= "table" then
connectsTo = {connectsTo}
end
for _, connectName in ipairs(connectsTo) do
if connectName == nodeName
or string.sub(connectName, 1, 6) == "group:"
and minetest.get_item_group(nodeName, string.sub(connectName, 7)) ~= 0 then
return true
end
end
return false
end
function meshport.get_tile(tiles, n)
if type(tiles) == "table" then
return tiles[n] or tiles[#tiles]
else
return "unknown"
end
end
function meshport.get_asset_paths(assetFolderName, extension)
local modAssetPath
local assets = {}
-- Iterate through each enabled mod.
for _, modName in ipairs(minetest.get_modnames()) do
modAssetPath = minetest.get_modpath(modName) .. DIR_DELIM .. assetFolderName
-- Iterate through all the files in the requested folder of the mod.
for _, fileName in ipairs(minetest.get_dir_list(modAssetPath, false)) do
-- Add files to the table. If an extendion is specified, only add files with that extension.
if not extension or string.lower(string.sub(fileName, -string.len(extension))) == extension then
assets[fileName] = modAssetPath .. DIR_DELIM .. fileName
end
end
end
return assets
end

68
init.lua Normal file
View File

@ -0,0 +1,68 @@
meshport = {
p1 = {},
p2 = {},
}
modpath = minetest.get_modpath("meshport")
dofile(modpath .. "/helpers.lua")
dofile(modpath .. "/api.lua")
dofile(modpath .. "/parse_obj.lua")
dofile(modpath .. "/nodebox.lua")
dofile(modpath .. "/export.lua")
minetest.register_privilege("meshport", "Can save meshes with meshport.")
minetest.register_on_leaveplayer(function(player, timed_out)
local name = player:get_player_name()
meshport.p1[name] = nil
meshport.p2[name] = nil
end)
for i = 1, 2 do
minetest.register_chatcommand("mesh" .. i, {
params = "[pos]",
description = string.format(
"Set position %i for meshport. Player's position is used if no other position is specified.", i),
privs = {meshport = true},
func = function(name, param)
local pos
if param == "" then
pos = minetest.get_player_by_name(name):get_pos()
else
pos = minetest.string_to_pos(param)
end
if not pos then
meshport.print(name, "error", "Not a valid position.")
return
end
pos = vector.round(pos)
if i == 1 then
meshport.p1[name] = pos
elseif i == 2 then
meshport.p2[name] = pos
end
meshport.print(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
end,
})
end
minetest.register_chatcommand("meshport", {
params = "",
description = "Save a mesh of the selected area.",
privs = {meshport = true},
func = function(name, param)
if not meshport.p1[name] or not meshport.p2[name] then
meshport.print(name, "error", "No area selected. Use /mesh1 and /mesh2 to select an area.")
return
end
meshport.create_mesh(name, meshport.p1[name], meshport.p2[name])
end,
})

2
mod.conf Normal file
View File

@ -0,0 +1,2 @@
name = meshport
descrption = Easily export areas in Minetest to meshes for 3D rendering.

194
nodebox.lua Normal file
View File

@ -0,0 +1,194 @@
meshport.side_box_names = {
"top", -- Y+
"bottom", -- Y-
"right", -- X+
"left", -- X-
"back", -- Z+
"front", -- Z-
}
function meshport.sort_box(box)
return {
math.min(box[1], box[4]),
math.min(box[2], box[5]),
math.min(box[3], box[6]),
math.max(box[1], box[4]),
math.max(box[2], box[5]),
math.max(box[3], box[6]),
}
end
meshport.Boxes = {}
function meshport.Boxes:new(boxes)
local o = {}
if type(boxes) ~= "table" or type(boxes[1]) == "number" then
o.boxes = {boxes}
else
o.boxes = boxes
end
setmetatable(o, self)
self.__index = self
return o
end
function meshport.Boxes:insert_all(boxes)
for _, box in ipairs(boxes.boxes) do
table.insert(self.boxes, table.copy(box))
end
end
function meshport.Boxes:transform(func)
local a, b
for i, box in ipairs(self.boxes) do
a = func(vector.new(box[1], box[2], box[3]))
b = func(vector.new(box[4], box[5], box[6]))
self.boxes[i] = {a.x, a.y, a.z, b.x, b.y, b.z}
end
end
function meshport.Boxes:rotate_by_facedir(facedir)
local a, b
for i, box in ipairs(self.boxes) do
a = meshport.rotate_vector_by_facedir(vector.new(box[1], box[2], box[3]), facedir)
b = meshport.rotate_vector_by_facedir(vector.new(box[4], box[5], box[6]), facedir)
self.boxes[i] = {a.x, a.y, a.z, b.x, b.y, b.z}
end
end
function meshport.Boxes:get_leveled(level)
local newBoxes = meshport.Boxes:new(table.copy(self.boxes))
for i, box in ipairs(newBoxes.boxes) do
box = meshport.sort_box(box)
box[5] = level / 64 - 0.5
newBoxes.boxes[i] = box
end
return newBoxes
end
function meshport.Boxes:to_faces(nodeTiles, pos, facedir)
local faces = meshport.Faces:new()
for _, b in ipairs(self.boxes) do
b = meshport.sort_box(b)
local sideFaces = {
{{x = b[4], y = b[5], z = b[3]}, {x = b[4], y = b[5], z = b[6]}, {x = b[1], y = b[5], z = b[6]}, {x = b[1], y = b[5], z = b[3]}}, -- Y+
{{x = b[4], y = b[2], z = b[6]}, {x = b[4], y = b[2], z = b[3]}, {x = b[1], y = b[2], z = b[3]}, {x = b[1], y = b[2], z = b[6]}}, -- Y-
{{x = b[4], y = b[2], z = b[6]}, {x = b[4], y = b[5], z = b[6]}, {x = b[4], y = b[5], z = b[3]}, {x = b[4], y = b[2], z = b[3]}}, -- X+
{{x = b[1], y = b[2], z = b[3]}, {x = b[1], y = b[5], z = b[3]}, {x = b[1], y = b[5], z = b[6]}, {x = b[1], y = b[2], z = b[6]}}, -- X-
{{x = b[1], y = b[2], z = b[6]}, {x = b[1], y = b[5], z = b[6]}, {x = b[4], y = b[5], z = b[6]}, {x = b[4], y = b[2], z = b[6]}}, -- Z+
{{x = b[4], y = b[2], z = b[3]}, {x = b[4], y = b[5], z = b[3]}, {x = b[1], y = b[5], z = b[3]}, {x = b[1], y = b[2], z = b[3]}}, -- Z-
}
local sideTexCoords = {
{{x = b[4], y = b[3]}, {x = b[4], y = b[6]}, {x = b[1], y = b[6]}, {x = b[1], y = b[3]}}, -- Y+
{{x = b[4], y =-b[6]}, {x = b[4], y =-b[3]}, {x = b[1], y =-b[3]}, {x = b[1], y =-b[6]}}, -- Y-
{{x = b[6], y = b[2]}, {x = b[6], y = b[5]}, {x = b[3], y = b[5]}, {x = b[3], y = b[2]}}, -- X+
{{x =-b[3], y = b[2]}, {x =-b[3], y = b[5]}, {x =-b[6], y = b[5]}, {x =-b[6], y = b[2]}}, -- X-
{{x =-b[1], y = b[2]}, {x =-b[1], y = b[5]}, {x =-b[4], y = b[5]}, {x =-b[4], y = b[2]}}, -- Z+
{{x = b[4], y = b[2]}, {x = b[4], y = b[5]}, {x = b[1], y = b[5]}, {x = b[1], y = b[2]}}, -- Z-
}
local tileIdx, vertNorm
for i = 1, 6 do
-- Fix offset texture coordinates.
for v = 1, 4 do
sideTexCoords[i][v] = {x = sideTexCoords[i][v].x + 0.5, y = sideTexCoords[i][v].y + 0.5}
end
vertNorm = meshport.neighbor_dirs[i]
faces:insert_face(meshport.prepare_cuboid_face({
verts = sideFaces[i],
tex_coords = sideTexCoords[i],
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
}, nodeTiles, pos, facedir, i))
end
end
return faces
end
function meshport.prepare_nodebox(nodebox)
local prepNodebox = {}
prepNodebox.type = nodebox.type
if nodebox.type == "regular" then
prepNodebox.fixed = meshport.Boxes:new({-0.5, -0.5, -0.5, 0.5, 0.5, 0.5})
elseif nodebox.type == "fixed" or nodebox.type == "leveled" then
prepNodebox.fixed = meshport.Boxes:new(nodebox.fixed)
elseif nodebox.type == "connected" then
prepNodebox.fixed = meshport.Boxes:new(nodebox.fixed)
prepNodebox.connected = {}
prepNodebox.disconnected = {}
for i, name in ipairs(meshport.side_box_names) do
prepNodebox.connected[i] = meshport.Boxes:new(nodebox["connect_" .. name])
prepNodebox.disconnected[i] = meshport.Boxes:new(nodebox["disconnected_" .. name])
end
prepNodebox.disconnected_all = meshport.Boxes:new(nodebox.disconnected)
prepNodebox.disconnected_sides = meshport.Boxes:new(nodebox.disconnected_sides)
elseif nodebox.type == "wallmounted" then
prepNodebox.wall_bottom = meshport.Boxes:new(nodebox.wall_bottom)
prepNodebox.wall_top = meshport.Boxes:new(nodebox.wall_top)
prepNodebox.wall_side = meshport.Boxes:new(nodebox.wall_side)
-- Rotate the boxes so they are in the correct orientation after rotation by facedir.
prepNodebox.wall_top:transform(function(v) return {x = -v.x, y = -v.y, z = v.z} end)
prepNodebox.wall_side:transform(function(v) return {x = -v.z, y = v.x, z = v.y} end)
end
return prepNodebox
end
function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors)
local boxes = meshport.Boxes:new()
if prepNodebox.fixed then
if prepNodebox.type == "leveled" then
boxes:insert_all(prepNodebox.fixed:get_leveled(
nodeDef.paramtype2 == "leveled" and param2 or nodeDef.leveled or 0))
else
boxes:insert_all(prepNodebox.fixed)
end
end
if prepNodebox.type == "connected" then
local neighborName
for i = 1, 6 do
neighborName = minetest.get_name_from_content_id(neighbors[i])
if meshport.node_connects_to(neighborName, nodeDef.connects_to) then
boxes:insert_all(prepNodebox.connected[i])
else
boxes:insert_all(prepNodebox.disconnected[i])
end
end
elseif prepNodebox.type == "wallmounted" then
if nodeDef.paramtype2 == "wallmounted" or nodeDef.paramtype2 == "colorwallmounted" then
if facedir == 20 then
boxes:insert_all(prepNodebox.wall_top)
elseif facedir == 0 then
boxes:insert_all(prepNodebox.wall_bottom)
else
boxes:insert_all(prepNodebox.wall_side)
end
else
boxes:insert_all(prepNodebox.wall_top)
end
end
return boxes
end

93
parse_obj.lua Normal file
View File

@ -0,0 +1,93 @@
function meshport.parse_vector_element(elementStr)
local elementType
local vec = {}
-- Get the element type and vector. `vec.z` will be left `nil` for two-dimensional vectors.
elementType, vec.x, vec.y, vec.z = string.match(elementStr, "^(%a+)%s([%d%.%-]+)%s([%d%.%-]+)%s?([%d%.%-]*)")
for k, v in pairs(vec) do
vec[k] = tonumber(v)
end
-- Return the element type and value.
if elementType == "v" then
-- Invert X axis to match the Minetest coordinate system.
vec.x = -vec.x
return "verts", vec
elseif elementType == "vt" then
return "tex_coords", vec
elseif elementType == "vn" then
vec.x = -vec.x
return "vert_norms", vec
end
end
function meshport.parse_face_element(elements, elementStr)
-- Split the face element into strings containing the indices of elements associated with each vertex.
local vertStrs = string.split(string.match(elementStr, "^f%s([%d/%s]+)"), " ")
local elementIndices
local face = {
verts = {},
tex_coords = {},
vert_norms = {},
}
for i, vertStr in ipairs(vertStrs) do
-- Split the string into a table of indices for position, texture coordinate, and/or vertex normal elements.
elementIndices = string.split(vertStr, "/", true)
for k, v in pairs(elementIndices) do
elementIndices[k] = tonumber(v)
end
-- Set the position, texture coordinate, and vertex normal of the face. `or 0` prevents a nil index error.
face.verts[i] = elements.verts[elementIndices[1] or 0]
face.tex_coords[i] = elements.tex_coords[elementIndices[2] or 0]
face.vert_norms[i] = elements.vert_norms[elementIndices[3] or 0]
end
return face
end
function meshport.parse_obj(path)
local faces = meshport.Faces:new()
local file = io.open(path, "r")
local elements = {
verts = {},
tex_coords = {},
vert_norms = {},
}
local groups = {}
local curGroup
local elementType
for line in file:lines() do
elementType = string.sub(line, 1, 1)
if elementType == "v" then
-- Parse the vector element. Used for "v", "vt", and "vn".
local type, value = meshport.parse_vector_element(line)
table.insert(elements[type], value)
elseif elementType == "f" then
-- If the face is not part of any group, use the placeholder group `0`.
if not curGroup then
table.insert(groups, 0)
curGroup = table.indexof(groups, 0)
end
-- Parse the face element.
local face = meshport.parse_face_element(elements, line)
-- Assign materials according to the group.
face.tile_idx = curGroup
faces:insert_face(face)
elseif elementType == "g" then
-- If this group has not been used yet, then add it to the list.
curGroup = meshport.find_or_insert(groups, string.match(line, "^g%s(.+)"))
end
end
return faces
end

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB