Add numerous features, refactor extensively
This commit is contained in:
parent
67d08f760a
commit
ff62e98edc
15
.luacheckrc
15
.luacheckrc
@ -2,14 +2,17 @@ unused_args = false
|
|||||||
allow_defined_top = true
|
allow_defined_top = true
|
||||||
max_line_length = 999
|
max_line_length = 999
|
||||||
|
|
||||||
|
globals = {
|
||||||
|
"meshport",
|
||||||
|
}
|
||||||
|
|
||||||
read_globals = {
|
read_globals = {
|
||||||
string = {fields = {"split", "trim"}},
|
string = {fields = {"split", "trim"}},
|
||||||
table = {fields = {"copy", "getn", "indexof"}},
|
table = {fields = {"copy", "getn", "indexof"}},
|
||||||
|
|
||||||
"minetest", "DIR_DELIM",
|
"minetest",
|
||||||
"vector", "VoxelArea",
|
"DIR_DELIM",
|
||||||
}
|
"PseudoRandom",
|
||||||
|
"vector",
|
||||||
ignore = {
|
"VoxelArea",
|
||||||
"vertList",
|
|
||||||
}
|
}
|
||||||
|
82
README.md
82
README.md
@ -5,52 +5,90 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Meshport is a mod which allows easy exporting of scenes from Minetest to `.obj` files, complete with materials and textures. These models can be imported directly into Blender or another 3D program for rendering and animation.
|
Meshport is a mod which allows easy exporting of scenes from Minetest to `.obj`
|
||||||
|
files, 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.
|
This mod is still in the beta phase; certain texturing features and node
|
||||||
|
drawtypes are not yet supported.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported, then use `/meshport [filename]` to export the mesh (filename is optional). The saved `.obj` and `.mtl` files will be located in the `meshport` folder of the world directory, within a subfolder.
|
Use `/mesh1` and `/mesh2` to set the corners of the area you want exported,
|
||||||
|
then use `/meshport [filename]` to export the mesh (filename is optional).
|
||||||
|
|
||||||
|
Folders containing exported meshes, including `.obj` and `.mtl` files, are
|
||||||
|
saved in the `meshport` folder of the world directory.
|
||||||
|
|
||||||
### Importing into Blender
|
### 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.
|
Once the model is exported, you can import the `.obj` file into Blender with
|
||||||
|
default settings. Make sure "Image Search" in the import settings is selected
|
||||||
|
to ensure textures from the `.mtl` file are imported as well.
|
||||||
|
|
||||||
#### Fixing materials
|
#### Fixing materials
|
||||||
|
|
||||||
Blender's packaged material assigned to OBJ textures are not effective or easy to use. By default, textures will also appear blurry and lack alpha. The `materials.py` script is included in the mod to simplify the materials, change interpolation, and add transparency. Open the script in Blender's text editor and run the script with the mesh selected.
|
Upon importing the file, Blender assigns basic materials to the model which are
|
||||||
|
inaccurate and not very usable. By default, these materials appear blurry and
|
||||||
|
lack transparency. The `materials.py` script is included in the mod to fix
|
||||||
|
these issues. Open the script in Blender's text editor and run the script with
|
||||||
|
the mesh selected.
|
||||||
|
|
||||||
#### Fixing vertex normals
|
Meshport does not handle texture modifiers or node coloring, so some materials
|
||||||
|
will probably still need to be fixed by hand after running the script.
|
||||||
|
|
||||||
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.
|
#### Other fixes
|
||||||
|
|
||||||
Additional tip: Use an HDRI sky texture (such as one from [here](https://hdrihaven.com)) for awesome-looking renders. ;)
|
Some mesh nodes may not have any vertex normals, which can lead to lighting
|
||||||
|
problems. To fix this, what I have found to work is to select the all the
|
||||||
|
problematic nodes (either manually or by selecting by material in edit mode),
|
||||||
|
mark the selected edges as sharp, and average the normals by face area.
|
||||||
|
|
||||||
|
Some animated textures may also appear incorrect. Meshport tries to scale
|
||||||
|
texture coordinates of animated textures to fit within one frame, but some
|
||||||
|
nodes (especially flowing liquids) can exceed this boundary. If this is an
|
||||||
|
issue, switch to a non-animated texture and scale up the affected UV maps to
|
||||||
|
match the new texture.
|
||||||
|
|
||||||
|
Additional tip: Use an HDRI sky texture (such as one from [here][1]) for
|
||||||
|
awesome-looking renders. ;)
|
||||||
|
|
||||||
|
[1]: https://hdrihaven.com
|
||||||
|
|
||||||
## Supported features
|
## Supported features
|
||||||
|
|
||||||
At the moment, only the following node drawtypes are supported:
|
The following node drawtypes are currently supported:
|
||||||
|
|
||||||
- Cubic drawtypes, including `normal`, `glasslike`, `allfaces`, and their variants (see below)
|
- Cubic drawtypes, including `normal`, `allfaces`, `glasslike`, and their
|
||||||
|
variants (see below)
|
||||||
|
- `glasslike_framed`
|
||||||
|
- `liquid` and `flowingliquid`
|
||||||
- `nodebox`
|
- `nodebox`
|
||||||
- `mesh` (only `.obj` meshes are exported)
|
- `mesh` (only `.obj` meshes are exported)
|
||||||
|
- `plantlike` and `plantlike_rooted`
|
||||||
|
|
||||||
Many special rendering features are not yet supported.
|
Meshport also supports many of Minetest's relevant features, including:
|
||||||
|
|
||||||
### A note on cubic nodes
|
- Most `paramtype2`s (note that color is ignored for colored types)
|
||||||
|
- `visual_scale`
|
||||||
|
- World-aligned textures
|
||||||
|
- Animated textures (only one frame is used)
|
||||||
|
|
||||||
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:
|
Some special rendering features are unsupported, including texture modifiers,
|
||||||
|
overlay textures, and node coloring.
|
||||||
|
|
||||||
| Priority level | Drawtypes |
|
### Notes on cubic nodes
|
||||||
|----------------|----------------------------------------------------|
|
|
||||||
| 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.
|
Drawtypes `allfaces_optional` and `glasslike_framed_optional` are output the
|
||||||
|
same as `allfaces` and `glasslike`, respectively.
|
||||||
|
|
||||||
|
Due to the differences between Minetest's rendering engine and 3D programs such
|
||||||
|
as Blender, it is impossible to exactly replicate how certain cubic nodes are
|
||||||
|
rendered in Minetest. Instead, Meshport aims for a compromise between accuracy
|
||||||
|
and simplicity of geometry. In certain cases where two cubic nodes are
|
||||||
|
touching, one face may be offset slightly to avoid duplicate faces while still
|
||||||
|
allowing both faces to be visible.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All code is licensed under the GNU LGPL v3.0.
|
Meshport is licensed under the GNU LGPL v3.0.
|
||||||
|
189
api.lua
189
api.lua
@ -1,189 +0,0 @@
|
|||||||
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
|
|
761
export.lua
761
export.lua
@ -1,67 +1,564 @@
|
|||||||
meshport.nodebox_cache = {}
|
--[[
|
||||||
meshport.mesh_cache = {}
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
|
Minetest: Copyright (C) 2010-2021 celeron55, Perttu Ahola <celeron55@gmail.com>
|
||||||
|
|
||||||
meshport.cube_face_priority = {
|
This file is part of Meshport.
|
||||||
|
|
||||||
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
|
-- Much of the mesh generation code in this file is derived from Minetest's
|
||||||
|
-- MapblockMeshGenerator class. See minetest/src/client/content_mapblock.cpp.
|
||||||
|
|
||||||
|
--[[
|
||||||
|
THE CUBIC NODE PRIORITY SYSTEM
|
||||||
|
|
||||||
|
For each face on each cubic node, Meshport decides whether or not to draw
|
||||||
|
that face based on a combination of the current node's drawtype (show in
|
||||||
|
the top row of the table below), the neighboring node's drawtype (shown in
|
||||||
|
the leftmost column), the direction of the face, and both nodes'
|
||||||
|
visual_scale.
|
||||||
|
|
||||||
|
A "YES" combination means the face is drawn, "no" means the face is not
|
||||||
|
drawn, and "Offset" means the face is drawn, but slightly inset to avoid
|
||||||
|
duplication of faces.
|
||||||
|
|
||||||
|
| This node => | allfaces (1) | glasslike | liquid | normal (2) |
|
||||||
|
|--------------:|:------------:|:---------:|:-------:|:----------:|
|
||||||
|
| air/non-cubic | YES | YES | YES (3) | YES |
|
||||||
|
| allfaces | (4) | YES | YES | YES |
|
||||||
|
| glasslike | Offset | (5) | YES | YES |
|
||||||
|
| liquid | Offset | Offset | no | YES |
|
||||||
|
| normal (2) | no | no | no | no |
|
||||||
|
|
||||||
|
1. Allfaces faces are always drawn if `visual_scale` is not 1.
|
||||||
|
2. The base of `plantlike_rooted` is treated as a normal node.
|
||||||
|
3. Liquid faces are not drawn bordering a corresponding flowing liquid.
|
||||||
|
4. Only drawn if facing X+, Y+, or Z+, or if either node's `visual_scale`
|
||||||
|
is not 1.
|
||||||
|
5. Only drawn if the nodes are different. X-, Z-, and Y- faces are offset.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local CUBIC_FACE_PRIORITY = {
|
||||||
allfaces = 1,
|
allfaces = 1,
|
||||||
glasslike_framed = 2,
|
glasslike = 2,
|
||||||
glasslike = 3,
|
liquid = 3,
|
||||||
normal = 4,
|
normal = 4,
|
||||||
|
plantlike_rooted = 4, -- base of plantlike_rooted is equivalent to `normal`.
|
||||||
}
|
}
|
||||||
|
|
||||||
function meshport.create_cube_node(nodeDrawtype, nodeTiles, pos, facedir, neighbors)
|
local vec = vector.new -- Makes defining tables of vertices a little less painful.
|
||||||
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 CUBIC_SIDE_FACES = {
|
||||||
|
{vec(-0.5, 0.5, -0.5), vec( 0.5, 0.5, -0.5), vec( 0.5, 0.5, 0.5), vec(-0.5, 0.5, 0.5)}, -- Y+
|
||||||
|
{vec(-0.5, -0.5, 0.5), vec( 0.5, -0.5, 0.5), vec( 0.5, -0.5, -0.5), vec(-0.5, -0.5, -0.5)}, -- Y-
|
||||||
|
{vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5, 0.5), vec( 0.5, 0.5, 0.5), vec( 0.5, 0.5, -0.5)}, -- X+
|
||||||
|
{vec(-0.5, -0.5, 0.5), vec(-0.5, -0.5, -0.5), vec(-0.5, 0.5, -0.5), vec(-0.5, 0.5, 0.5)}, -- X-
|
||||||
|
{vec( 0.5, -0.5, 0.5), vec(-0.5, -0.5, 0.5), vec(-0.5, 0.5, 0.5), vec( 0.5, 0.5, 0.5)}, -- Z+
|
||||||
|
{vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5, 0.5, -0.5), vec(-0.5, 0.5, -0.5)}, -- Z-
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- For normal, plantlike_rooted, and liquid drawtypes
|
||||||
|
local function create_cubic_node(pos, content, param2, nodeDef, drawtype, neighbors)
|
||||||
|
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
|
||||||
|
local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
|
||||||
|
-- If the current node is a liquid, get the flowing version of it.
|
||||||
|
local flowingLiquid = drawtype == "liquid"
|
||||||
|
and meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing) or nil
|
||||||
|
|
||||||
local faces = meshport.Faces:new()
|
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
|
for i = 1, 6 do
|
||||||
neighborDrawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(neighbors[i]).drawtype)
|
local drawFace
|
||||||
|
|
||||||
if meshport.cube_face_priority[nodeDrawtype] > (meshport.cube_face_priority[neighborDrawtype] or 0)
|
if neighbors[i] == minetest.CONTENT_AIR then
|
||||||
-- For allfaces nodes (such are leaves), interior faces are drawn only when facing X+, Y+, or Z+.
|
drawFace = true
|
||||||
or (nodeDrawtype == "allfaces" and neighborDrawtype == "allfaces" and i % 2 == 1) then
|
elseif neighbors[i] == minetest.CONTENT_IGNORE
|
||||||
vertNorm = meshport.neighbor_dirs[i]
|
-- Don't draw faces between identical nodes
|
||||||
|
or neighbors[i] == content
|
||||||
|
-- Don't draw liquid faces bordering a corresponding flowing liquid
|
||||||
|
or neighbors[i] == flowingLiquid then
|
||||||
|
drawFace = false
|
||||||
|
else
|
||||||
|
local neighborDef = meshport.get_def_from_id(neighbors[i])
|
||||||
|
local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
|
||||||
|
drawFace = selfPriority > (CUBIC_FACE_PRIORITY[neighborDrawtype] or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
if drawFace then
|
||||||
|
local norm = meshport.NEIGHBOR_DIRS[i]
|
||||||
|
|
||||||
faces:insert_face(meshport.prepare_cuboid_face({
|
faces:insert_face(meshport.prepare_cuboid_face({
|
||||||
verts = sideFaces[i],
|
verts = table.copy(CUBIC_SIDE_FACES[i]),
|
||||||
tex_coords = texCoords,
|
vert_norms = {norm, norm, norm, norm},
|
||||||
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
|
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
|
||||||
tile_idx = tileIdx,
|
}, nodeDef.tiles, pos, facedir, i))
|
||||||
}, nodeTiles, pos, facedir, i))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return faces
|
return faces
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.create_nodebox_node(nodeName, pos, facedir, param2, neighbors)
|
|
||||||
|
-- For allfaces and glasslike drawtypes, and equivalent variants.
|
||||||
|
local function create_special_cubic_node(pos, content, nodeDef, drawtype, neighbors)
|
||||||
|
local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
|
||||||
|
local isAllfaces = drawtype == "allfaces"
|
||||||
|
local allfacesScale = isAllfaces and nodeDef.visual_scale or 1
|
||||||
|
|
||||||
|
local faces = meshport.Faces:new()
|
||||||
|
|
||||||
|
for i = 1, 6 do
|
||||||
|
local drawFace
|
||||||
|
local inset = false
|
||||||
|
|
||||||
|
if allfacesScale ~= 1 or neighbors[i] == minetest.CONTENT_AIR or neighbors[i] == minetest.CONTENT_IGNORE then
|
||||||
|
drawFace = true
|
||||||
|
elseif neighbors[i] == content then
|
||||||
|
drawFace = isAllfaces and i % 2 == 1
|
||||||
|
else
|
||||||
|
local neighborDef = meshport.get_def_from_id(neighbors[i])
|
||||||
|
local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
|
||||||
|
local neighborPriority = CUBIC_FACE_PRIORITY[neighborDrawtype] or 0
|
||||||
|
|
||||||
|
if neighborPriority < selfPriority then
|
||||||
|
drawFace = true
|
||||||
|
elseif neighborPriority >= 4 then
|
||||||
|
-- Don't draw faces bordering normal nodes.
|
||||||
|
drawFace = false
|
||||||
|
elseif neighborPriority > selfPriority then
|
||||||
|
drawFace = true
|
||||||
|
inset = true
|
||||||
|
elseif isAllfaces then -- neighborPriority == selfPriority
|
||||||
|
drawFace = i % 2 == 1 or neighborDef.visual_scale ~= 1
|
||||||
|
else -- neighborPriority == selfPriority
|
||||||
|
drawFace = true
|
||||||
|
inset = i % 2 == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if drawFace then
|
||||||
|
local verts = table.copy(CUBIC_SIDE_FACES[i])
|
||||||
|
|
||||||
|
if inset then
|
||||||
|
local offset = vector.multiply(meshport.NEIGHBOR_DIRS[i], -0.003)
|
||||||
|
for j, vert in ipairs(verts) do
|
||||||
|
verts[j] = vector.add(vert, offset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local norm = meshport.NEIGHBOR_DIRS[i]
|
||||||
|
faces:insert_face(meshport.prepare_cuboid_face({
|
||||||
|
verts = verts,
|
||||||
|
vert_norms = {norm, norm, norm, norm},
|
||||||
|
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
|
||||||
|
tile_idx = 1, -- Only the first tile is used.
|
||||||
|
}, nodeDef.tiles, pos, 0, i))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
faces:scale(allfacesScale)
|
||||||
|
return faces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local GLASSLIKE_FRAMED_CONSTANTS = (function()
|
||||||
|
local a = 0.5
|
||||||
|
local g = 0.5 - 0.003
|
||||||
|
local b = 0.876 * 0.5
|
||||||
|
|
||||||
|
return {
|
||||||
|
G = g,
|
||||||
|
B = b,
|
||||||
|
FRAME_EDGES = {
|
||||||
|
{ b, b, -a, a, a, a}, -- Y+ / X+
|
||||||
|
{-a, b, -a, -b, a, a}, -- Y+ / X-
|
||||||
|
{ b, -a, -a, a, -b, a}, -- Y- / X+
|
||||||
|
{-a, -a, -a, -b, -b, a}, -- Y- / X-
|
||||||
|
{ b, -a, b, a, a, a}, -- X+ / Z+
|
||||||
|
{ b, -a, -a, a, a, -b}, -- X+ / Z-
|
||||||
|
{-a, -a, b, -b, a, a}, -- X- / Z+
|
||||||
|
{-a, -a, -a, -b, a, -b}, -- X- / Z-
|
||||||
|
{-a, b, b, a, a, a}, -- Z+ / Y+
|
||||||
|
{-a, -a, b, a, -b, a}, -- Z+ / Y-
|
||||||
|
{-a, b, -a, a, a, -b}, -- Z- / Y+
|
||||||
|
{-a, -a, -a, a, -b, -b}, -- Z- / Y-
|
||||||
|
},
|
||||||
|
GLASS_FACES = {
|
||||||
|
{vec(-a, g, -a), vec( a, g, -a), vec( a, g, a), vec(-a, g, a)}, -- Y+
|
||||||
|
{vec(-a, -g, a), vec( a, -g, a), vec( a, -g, -a), vec(-a, -g, -a)}, -- Y-
|
||||||
|
{vec( g, -a, -a), vec( g, -a, a), vec( g, a, a), vec( g, a, -a)}, -- X+
|
||||||
|
{vec(-g, -a, a), vec(-g, -a, -a), vec(-g, a, -a), vec(-g, a, a)}, -- X-
|
||||||
|
{vec( a, -a, g), vec(-a, -a, g), vec(-a, a, g), vec( a, a, g)}, -- Z+
|
||||||
|
{vec(-a, -a, -g), vec( a, -a, -g), vec( a, a, -g), vec(-a, a, -g)}, -- Z-
|
||||||
|
},
|
||||||
|
EDGE_NEIGHBORS = {
|
||||||
|
{1, 3, 8}, {1, 4, 7}, {2, 3, 16}, {2, 4, 15},
|
||||||
|
{3, 5, 12}, {3, 6, 14}, {4, 5, 11}, {4, 6, 13},
|
||||||
|
{5, 1, 9}, {5, 2, 17}, {6, 1, 10}, {6, 2, 18},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end)()
|
||||||
|
|
||||||
|
|
||||||
|
local function create_glasslike_framed_node(pos, param2, nodeDef, area, vContent)
|
||||||
|
local idx = area:indexp(pos)
|
||||||
|
local llParam2 = nodeDef.paramtype2 == "glasslikeliquidlevel" and param2 or 0
|
||||||
|
local hMerge = llParam2 < 128 -- !(param2 & 128)
|
||||||
|
local vMerge = llParam2 % 128 < 64 -- !(param2 & 64)
|
||||||
|
local intLevel = llParam2 % 64
|
||||||
|
|
||||||
|
-- Localize constants
|
||||||
|
local G, B, FRAME_EDGES, GLASS_FACES, EDGE_NEIGHBORS = (function(c)
|
||||||
|
return c.G, c.B, c.FRAME_EDGES, c.GLASS_FACES, c.EDGE_NEIGHBORS
|
||||||
|
end)(GLASSLIKE_FRAMED_CONSTANTS)
|
||||||
|
|
||||||
|
local neighbors = {
|
||||||
|
false, false, false, false, false, false, false, false, false,
|
||||||
|
false, false, false, false, false, false, false, false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if hMerge or vMerge then
|
||||||
|
for i = 1, 18 do
|
||||||
|
local dir = meshport.NEIGHBOR_DIRS[i]
|
||||||
|
if (hMerge or (dir.x == 0 and dir.z == 0)) and (vMerge or dir.y == 0) then
|
||||||
|
local nIdx = area:indexp(vector.add(pos, dir))
|
||||||
|
neighbors[i] = vContent[nIdx] == vContent[idx]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local boxes = meshport.Boxes:new()
|
||||||
|
|
||||||
|
for i = 1, 12 do
|
||||||
|
local edgeVisible
|
||||||
|
local touching = EDGE_NEIGHBORS[i]
|
||||||
|
|
||||||
|
if neighbors[touching[3]] then
|
||||||
|
edgeVisible = not (neighbors[touching[1]] and neighbors[touching[2]])
|
||||||
|
else
|
||||||
|
edgeVisible = neighbors[touching[1]] == neighbors[touching[2]]
|
||||||
|
end
|
||||||
|
|
||||||
|
if edgeVisible then
|
||||||
|
boxes:insert_box(FRAME_EDGES[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local faces = boxes:to_faces(nodeDef, pos, 0, 1)
|
||||||
|
|
||||||
|
for i = 1, 6 do
|
||||||
|
if not neighbors[i] then
|
||||||
|
local norm = meshport.NEIGHBOR_DIRS[i]
|
||||||
|
|
||||||
|
faces:insert_face({
|
||||||
|
verts = table.copy(GLASS_FACES[i]),
|
||||||
|
vert_norms = {norm, norm, norm, norm},
|
||||||
|
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
|
||||||
|
tile_idx = 2,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if intLevel > 0 and nodeDef.special_tiles and nodeDef.special_tiles[1] then
|
||||||
|
local level = intLevel / 63 * 2 - 1
|
||||||
|
local liquidBoxes = meshport.Boxes:new()
|
||||||
|
liquidBoxes:insert_box({
|
||||||
|
-(neighbors[4] and G or B),
|
||||||
|
-(neighbors[2] and G or B),
|
||||||
|
-(neighbors[6] and G or B),
|
||||||
|
(neighbors[3] and G or B),
|
||||||
|
(neighbors[1] and G or B) * level,
|
||||||
|
(neighbors[5] and G or B)
|
||||||
|
})
|
||||||
|
faces:insert_all(liquidBoxes:to_faces(nodeDef, pos, 0, 1, true))
|
||||||
|
end
|
||||||
|
|
||||||
|
return faces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local FLOWING_LIQUID_CONSTANTS = {
|
||||||
|
SIDE_DIRS = {vec(1, 0, 0), vec(-1, 0, 0), vec(0, 0, 1), vec(0, 0, -1)},
|
||||||
|
SIDE_CORNERS = {
|
||||||
|
{{x = 1, z = 1}, {x = 1, z = 0}}, -- X+
|
||||||
|
{{x = 0, z = 0}, {x = 0, z = 1}}, -- X-
|
||||||
|
{{x = 0, z = 1}, {x = 1, z = 1}}, -- Z+
|
||||||
|
{{x = 1, z = 0}, {x = 0, z = 0}}, -- Z-
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
|
||||||
|
local cSource = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_source)
|
||||||
|
local cFlowing = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing)
|
||||||
|
local range = math.min(math.max(meshport.get_def_from_id(cFlowing).liquid_range or 8, 1), 8)
|
||||||
|
|
||||||
|
--[[ Step 1: Gather neighbor data ]]
|
||||||
|
local neighbors = {[-1] = {}, [0] = {}, [1] = {}}
|
||||||
|
|
||||||
|
for dz = -1, 1 do
|
||||||
|
for dx = -1, 1 do
|
||||||
|
local nPos = vector.add(pos, vector.new(dx, 0, dz))
|
||||||
|
local nIdx = area:indexp(nPos)
|
||||||
|
|
||||||
|
neighbors[dz][dx] = {
|
||||||
|
content = vContent[nIdx],
|
||||||
|
level = -0.5,
|
||||||
|
is_same_liquid = false,
|
||||||
|
top_is_same_liquid = false,
|
||||||
|
}
|
||||||
|
local nData = neighbors[dz][dx]
|
||||||
|
|
||||||
|
if vContent[nIdx] ~= minetest.CONTENT_IGNORE then
|
||||||
|
if vContent[nIdx] == cSource then
|
||||||
|
nData.is_same_liquid = true
|
||||||
|
nData.level = 0.5
|
||||||
|
elseif vContent[nIdx] == cFlowing then
|
||||||
|
nData.is_same_liquid = true
|
||||||
|
local intLevel = math.max(vParam2[nIdx] % 8 - 8 + range, 0)
|
||||||
|
nData.level = -0.5 + (intLevel + 0.5) / range
|
||||||
|
end
|
||||||
|
|
||||||
|
local tPos = vector.add(nPos, vector.new(0, 1, 0))
|
||||||
|
local tIdx = area:indexp(tPos)
|
||||||
|
if vContent[tIdx] == cSource or vContent[tIdx] == cFlowing then
|
||||||
|
nData.top_is_same_liquid = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[ Step 2: Determine level at each corner ]]
|
||||||
|
local cornerLevels = {[0] = {[0] = 0, 0}, {[0] = 0, 0}}
|
||||||
|
|
||||||
|
local function get_corner_level(cx, cz)
|
||||||
|
local sum = 0
|
||||||
|
local count = 0
|
||||||
|
local airCount = 0
|
||||||
|
|
||||||
|
for dz = -1, 0 do
|
||||||
|
for dx = -1, 0 do
|
||||||
|
local nData = neighbors[cz + dz][cx + dx]
|
||||||
|
|
||||||
|
if nData.top_is_same_liquid or nData.content == cSource then
|
||||||
|
return 0.5
|
||||||
|
elseif nData.content == cFlowing then
|
||||||
|
sum = sum + nData.level
|
||||||
|
count = count + 1
|
||||||
|
elseif nData.content == minetest.CONTENT_AIR then
|
||||||
|
airCount = airCount + 1
|
||||||
|
|
||||||
|
if airCount >= 2 then
|
||||||
|
return -0.5 + 0.02
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if count > 0 then
|
||||||
|
return sum / count
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
for cz = 0, 1 do
|
||||||
|
for cx = 0, 1 do
|
||||||
|
cornerLevels[cz][cx] = get_corner_level(cx, cz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[ Step 3: Actually create the liquid mesh ]]
|
||||||
|
local faces = meshport.Faces:new()
|
||||||
|
|
||||||
|
-- Localize constants
|
||||||
|
local SIDE_DIRS, SIDE_CORNERS = (function(c)
|
||||||
|
return c.SIDE_DIRS, c.SIDE_CORNERS
|
||||||
|
end)(FLOWING_LIQUID_CONSTANTS)
|
||||||
|
|
||||||
|
-- Add side faces
|
||||||
|
local sideVerts = {
|
||||||
|
{vec( 0.5, 0.5, 0.5), vec( 0.5, 0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5, 0.5)}, -- X+
|
||||||
|
{vec(-0.5, 0.5, -0.5), vec(-0.5, 0.5, 0.5), vec(-0.5, -0.5, 0.5), vec(-0.5, -0.5, -0.5)}, -- X-
|
||||||
|
{vec(-0.5, 0.5, 0.5), vec( 0.5, 0.5, 0.5), vec( 0.5, -0.5, 0.5), vec(-0.5, -0.5, 0.5)}, -- Z+
|
||||||
|
{vec( 0.5, 0.5, -0.5), vec(-0.5, 0.5, -0.5), vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5)}, -- Z-
|
||||||
|
}
|
||||||
|
|
||||||
|
local function need_side(dir)
|
||||||
|
local neighbor = neighbors[dir.z][dir.x]
|
||||||
|
if neighbor.is_same_liquid
|
||||||
|
and (not neighbors[0][0].top_is_same_liquid or neighbor.top_is_same_liquid) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local nContent = neighbors[dir.z][dir.x].content
|
||||||
|
local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(nContent).drawtype)
|
||||||
|
if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
|
||||||
|
return false -- Don't draw bordering normal nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 4 do
|
||||||
|
local dir = SIDE_DIRS[i]
|
||||||
|
|
||||||
|
if need_side(dir) then
|
||||||
|
local verts = sideVerts[i]
|
||||||
|
local sideTexCoords = {{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}, {x = 1, y = 0}}
|
||||||
|
|
||||||
|
if not neighbors[0][0].top_is_same_liquid then -- If there's liquid above, default to a full block.
|
||||||
|
local corners = SIDE_CORNERS[i]
|
||||||
|
|
||||||
|
for j = 1, 2 do
|
||||||
|
local corner = cornerLevels[corners[j].z][corners[j].x]
|
||||||
|
verts[j].y = corner
|
||||||
|
sideTexCoords[j].y = corner + 0.5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
faces:insert_face({
|
||||||
|
verts = verts,
|
||||||
|
vert_norms = {dir, dir, dir, dir},
|
||||||
|
tex_coords = sideTexCoords,
|
||||||
|
tile_idx = 2,
|
||||||
|
use_special_tiles = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add top faces
|
||||||
|
if not neighbors[0][0].top_is_same_liquid then -- Check node above the current node
|
||||||
|
local verts = {
|
||||||
|
vec( 0.5, cornerLevels[0][1], -0.5),
|
||||||
|
vec( 0.5, cornerLevels[1][1], 0.5),
|
||||||
|
vec(-0.5, cornerLevels[1][0], 0.5),
|
||||||
|
vec(-0.5, cornerLevels[0][0], -0.5),
|
||||||
|
}
|
||||||
|
|
||||||
|
local norm1 = vector.normalize(vector.cross(
|
||||||
|
vector.subtract(verts[1], verts[2]),
|
||||||
|
vector.subtract(verts[3], verts[2])
|
||||||
|
))
|
||||||
|
local norm2 = vector.normalize(vector.cross(
|
||||||
|
vector.subtract(verts[3], verts[4]),
|
||||||
|
vector.subtract(verts[1], verts[4])
|
||||||
|
))
|
||||||
|
|
||||||
|
local dz = (cornerLevels[0][0] + cornerLevels[0][1]) -
|
||||||
|
(cornerLevels[1][0] + cornerLevels[1][1])
|
||||||
|
local dx = (cornerLevels[0][0] + cornerLevels[1][0]) -
|
||||||
|
(cornerLevels[0][1] + cornerLevels[1][1])
|
||||||
|
local textureAngle = -math.atan2(dz, dx)
|
||||||
|
|
||||||
|
-- Get texture coordinate offset based on position.
|
||||||
|
local tx, ty = pos.z, -pos.x
|
||||||
|
-- Rotate offset around (0, 0) by textureAngle.
|
||||||
|
-- Then isolate the fractional part, since the texture is tiled anyway.
|
||||||
|
local sinTA = math.sin(textureAngle)
|
||||||
|
local cosTA = math.cos(textureAngle)
|
||||||
|
local textureOffset = {
|
||||||
|
x = (tx * cosTA - ty * sinTA) % 1,
|
||||||
|
y = (tx * sinTA + ty * cosTA) % 1
|
||||||
|
}
|
||||||
|
|
||||||
|
faces:insert_face({
|
||||||
|
verts = {verts[1], verts[2], verts[3]},
|
||||||
|
vert_norms = {norm1, norm1, norm1},
|
||||||
|
tex_coords = meshport.translate_texture_coordinates(
|
||||||
|
meshport.rotate_texture_coordinates_rad(
|
||||||
|
{{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}},
|
||||||
|
textureAngle
|
||||||
|
),
|
||||||
|
textureOffset
|
||||||
|
),
|
||||||
|
tile_idx = 1,
|
||||||
|
use_special_tiles = true,
|
||||||
|
})
|
||||||
|
faces:insert_face({
|
||||||
|
verts = {verts[3], verts[4], verts[1]},
|
||||||
|
vert_norms = {norm2, norm2, norm2},
|
||||||
|
tex_coords = meshport.translate_texture_coordinates(
|
||||||
|
meshport.rotate_texture_coordinates_rad(
|
||||||
|
{{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}},
|
||||||
|
textureAngle
|
||||||
|
),
|
||||||
|
textureOffset
|
||||||
|
),
|
||||||
|
tile_idx = 1,
|
||||||
|
use_special_tiles = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add bottom face
|
||||||
|
local function need_liquid_bottom()
|
||||||
|
local bContent = vContent[area:indexp(vector.add(pos, vector.new(0, -1, 0)))]
|
||||||
|
if bContent == cSource or bContent == cFlowing then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(bContent).drawtype)
|
||||||
|
if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
|
||||||
|
return false -- Don't draw bordering normal nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if need_liquid_bottom() then
|
||||||
|
local norm = vector.new(0, -1, 0)
|
||||||
|
|
||||||
|
faces:insert_face({
|
||||||
|
verts = {
|
||||||
|
vec(-0.5, -0.5, 0.5),
|
||||||
|
vec( 0.5, -0.5, 0.5),
|
||||||
|
vec( 0.5, -0.5, -0.5),
|
||||||
|
vec(-0.5, -0.5, -0.5),
|
||||||
|
},
|
||||||
|
vert_norms = {norm, norm, norm, norm},
|
||||||
|
tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
|
||||||
|
tile_idx = 1,
|
||||||
|
use_special_tiles = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return faces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function create_nodebox_node(pos, content, param2, neighbors)
|
||||||
|
local nodeName = minetest.get_name_from_content_id(content)
|
||||||
local nodeDef = minetest.registered_nodes[nodeName]
|
local nodeDef = minetest.registered_nodes[nodeName]
|
||||||
|
|
||||||
if not meshport.nodebox_cache[nodeName] then
|
if not meshport.nodebox_cache[nodeName] then
|
||||||
meshport.nodebox_cache[nodeName] = meshport.prepare_nodebox(nodeDef.node_box)
|
meshport.nodebox_cache[nodeName] = meshport.prepare_nodebox(nodeDef.node_box)
|
||||||
end
|
end
|
||||||
|
|
||||||
local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, facedir, param2, neighbors)
|
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
|
||||||
|
local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, param2, facedir, neighbors)
|
||||||
|
|
||||||
if meshport.nodebox_cache[nodeName].type ~= "connected" then
|
if meshport.nodebox_cache[nodeName].type ~= "connected" then
|
||||||
boxes:rotate_by_facedir(facedir)
|
boxes:rotate_by_facedir(facedir)
|
||||||
end
|
end
|
||||||
|
|
||||||
return boxes:to_faces(nodeDef.tiles, pos, facedir)
|
return boxes:to_faces(nodeDef, pos, facedir)
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.create_mesh_node(nodeDef, playerName)
|
|
||||||
|
local function create_mesh_node(nodeDef, param2, playerName)
|
||||||
local meshName = nodeDef.mesh
|
local meshName = nodeDef.mesh
|
||||||
|
|
||||||
if not meshName then
|
if not meshName then
|
||||||
@ -76,97 +573,227 @@ function meshport.create_mesh_node(nodeDef, playerName)
|
|||||||
|
|
||||||
if not meshport.obj_paths[meshName] then
|
if not meshport.obj_paths[meshName] then
|
||||||
if string.lower(string.sub(meshName, -4)) ~= ".obj" then
|
if string.lower(string.sub(meshName, -4)) ~= ".obj" then
|
||||||
meshport.print(playerName, "warning", string.format("Mesh %q is not supported.", meshName))
|
meshport.log(playerName, "warning", string.format("Mesh %q is not supported.", meshName))
|
||||||
else
|
else
|
||||||
meshport.print(playerName, "warning", string.format("Mesh %q could not be found.", meshName))
|
meshport.log(playerName, "warning", string.format("Mesh %q could not be found.", meshName))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Cache a blank faces object so the player isn't warned again.
|
-- Cache a blank faces object so the player isn't warned again.
|
||||||
meshport.mesh_cache[meshName] = meshport.Faces:new()
|
meshport.mesh_cache[meshName] = meshport.Faces:new()
|
||||||
else
|
else
|
||||||
meshport.mesh_cache[meshName] = meshport.parse_obj(meshport.obj_paths[meshName])
|
-- TODO: pcall this in case of failure
|
||||||
|
local meshFaces = meshport.parse_obj(meshport.obj_paths[meshName])
|
||||||
|
meshFaces:scale(nodeDef.visual_scale)
|
||||||
|
meshport.mesh_cache[meshName] = meshFaces
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return meshport.mesh_cache[meshName]:copy()
|
local faces = meshport.mesh_cache[meshName]:copy()
|
||||||
|
|
||||||
|
local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
|
||||||
|
faces:rotate_by_facedir(facedir)
|
||||||
|
|
||||||
|
local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
|
||||||
|
faces:rotate_xz_degrees(rotation)
|
||||||
|
|
||||||
|
return faces
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.create_node(idx, area, content, param2, playerName)
|
|
||||||
if content[idx] == minetest.CONTENT_AIR or content[idx] == minetest.CONTENT_IGNORE then
|
local function create_plantlike_node(pos, param2, nodeDef)
|
||||||
|
local style = 0
|
||||||
|
local height = 1.0
|
||||||
|
local scale = 0.5 * nodeDef.visual_scale
|
||||||
|
local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
|
||||||
|
local offset = vector.new(0, 0, 0)
|
||||||
|
local randomOffsetY = false
|
||||||
|
local faceNum = 0
|
||||||
|
|
||||||
|
if nodeDef.paramtype2 == "meshoptions" then
|
||||||
|
style = param2 % 8
|
||||||
|
|
||||||
|
if param2 % 16 >= 8 then -- param2 % 8
|
||||||
|
-- TODO: Use MT's seed generators
|
||||||
|
local seed = (pos.x % 0xFF) * 0x100 + (pos.z % 0xFF) + (pos.y % 0xFF) * 0x10000
|
||||||
|
local rng = PseudoRandom(seed)
|
||||||
|
offset.x = (rng:next() % 16 / 16) * 0.29 - 0.145
|
||||||
|
offset.z = (rng:next() % 16 / 16) * 0.29 - 0.145
|
||||||
|
end
|
||||||
|
|
||||||
|
if param2 % 32 >= 16 then -- param2 & 16
|
||||||
|
scale = scale * 1.41421
|
||||||
|
end
|
||||||
|
|
||||||
|
if param2 % 64 >= 32 then -- param2 & 32
|
||||||
|
randomOffsetY = true
|
||||||
|
end
|
||||||
|
elseif nodeDef.paramtype2 == "leveled" then
|
||||||
|
height = param2 / 16
|
||||||
|
end
|
||||||
|
|
||||||
|
local function create_plantlike_quad(faceRotation, topOffset, bottomOffset)
|
||||||
|
local faces = meshport.Faces:new()
|
||||||
|
local plantHeight = 2.0 * scale * height
|
||||||
|
local norm = vector.normalize(vector.new(0, bottomOffset - topOffset, plantHeight))
|
||||||
|
|
||||||
|
faces:insert_face({
|
||||||
|
verts = {
|
||||||
|
vec(-scale, -0.5 + plantHeight, topOffset),
|
||||||
|
vec( scale, -0.5 + plantHeight, topOffset),
|
||||||
|
vec( scale, -0.5, bottomOffset),
|
||||||
|
vec(-scale, -0.5, bottomOffset),
|
||||||
|
},
|
||||||
|
tex_coords = {{x = 0, y = height}, {x = 1, y = height}, {x = 1, y = 0}, {x = 0, y = 0}},
|
||||||
|
vert_norms = {norm, norm, norm, norm},
|
||||||
|
tile_idx = 0,
|
||||||
|
use_special_tiles = nodeDef.drawtype == "plantlike_rooted"
|
||||||
|
})
|
||||||
|
|
||||||
|
if randomOffsetY then
|
||||||
|
local seed = faceNum + (pos.x % 0xFF) * 0x10000 + (pos.z % 0xFF) * 0x100 + (pos.y % 0xFF) * 0x1000000
|
||||||
|
local yRng = PseudoRandom(seed)
|
||||||
|
faces:translate(vector.new(0, (yRng:next() % 16) / 16.0 * -0.125, 0))
|
||||||
|
faceNum = faceNum + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
faces:rotate_xz_degrees(faceRotation + rotation)
|
||||||
|
return faces
|
||||||
|
end
|
||||||
|
|
||||||
|
local faces = meshport.Faces:new()
|
||||||
|
|
||||||
|
if style == 0 then
|
||||||
|
faces:insert_all(create_plantlike_quad(46, 0, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(-44, 0, 0))
|
||||||
|
elseif style == 1 then
|
||||||
|
faces:insert_all(create_plantlike_quad(91, 0, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(1, 0, 0))
|
||||||
|
elseif style == 2 then
|
||||||
|
faces:insert_all(create_plantlike_quad(121, 0, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(241, 0, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(1, 0, 0))
|
||||||
|
elseif style == 3 then
|
||||||
|
faces:insert_all(create_plantlike_quad(1, 0.25, 0.25))
|
||||||
|
faces:insert_all(create_plantlike_quad(91, 0.25, 0.25))
|
||||||
|
faces:insert_all(create_plantlike_quad(181, 0.25, 0.25))
|
||||||
|
faces:insert_all(create_plantlike_quad(271, 0.25, 0.25))
|
||||||
|
elseif style == 4 then
|
||||||
|
faces:insert_all(create_plantlike_quad(1, -0.5, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(91, -0.5, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(181, -0.5, 0))
|
||||||
|
faces:insert_all(create_plantlike_quad(271, -0.5, 0))
|
||||||
|
end
|
||||||
|
|
||||||
|
faces:translate(offset)
|
||||||
|
return faces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function create_node(idx, area, vContent, vParam2, playerName)
|
||||||
|
if vContent[idx] == minetest.CONTENT_AIR
|
||||||
|
or vContent[idx] == minetest.CONTENT_IGNORE
|
||||||
|
or vContent[idx] == minetest.CONTENT_UNKNOWN then -- TODO: Export unknown nodes?
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local nodeDef = meshport.get_def_from_id(content[idx])
|
local nodeDef = meshport.get_def_from_id(vContent[idx])
|
||||||
|
if nodeDef.drawtype == "airlike" then
|
||||||
if not nodeDef.drawtype or nodeDef.drawtype == "airlike" then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local pos = area:position(idx)
|
||||||
local nodeDrawtype = meshport.get_aliased_drawtype(nodeDef.drawtype)
|
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
|
local neighbors, faces
|
||||||
|
|
||||||
if isCubicType then
|
if CUBIC_FACE_PRIORITY[nodeDrawtype] or nodeDrawtype == "nodebox" then
|
||||||
neighbors = meshport.get_node_neighbors(content, area, idx)
|
neighbors = meshport.get_node_neighbors(vContent, area, idx)
|
||||||
end
|
end
|
||||||
|
|
||||||
if meshport.cube_face_priority[nodeDrawtype] then
|
if (CUBIC_FACE_PRIORITY[nodeDrawtype] or 0) >= 3 then -- liquid, normal, plantlike_rooted
|
||||||
faces = meshport.create_cube_node(nodeDrawtype, nodeDef.tiles, area:position(idx), facedir, neighbors)
|
faces = create_cubic_node(pos, vContent[idx], vParam2[idx], nodeDef, nodeDrawtype, neighbors)
|
||||||
|
|
||||||
|
if nodeDrawtype == "plantlike_rooted" then
|
||||||
|
local plantPos = vector.add(pos, vector.new(0, 1, 0))
|
||||||
|
local plantFaces = create_plantlike_node(nodeDef, plantPos, vParam2[idx])
|
||||||
|
plantFaces:translate(vector.new(0, 1, 0))
|
||||||
|
faces:insert_all(plantFaces)
|
||||||
|
end
|
||||||
|
elseif CUBIC_FACE_PRIORITY[nodeDrawtype] then -- Any other cubic nodes (allfaces, glasslike)
|
||||||
|
faces = create_special_cubic_node(pos, vContent[idx], nodeDef, nodeDrawtype, neighbors)
|
||||||
|
elseif nodeDrawtype == "glasslike_framed" then
|
||||||
|
faces = create_glasslike_framed_node(pos, vParam2[idx], nodeDef, area, vContent)
|
||||||
|
elseif nodeDrawtype == "flowingliquid" then
|
||||||
|
faces = create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
|
||||||
elseif nodeDrawtype == "nodebox" then
|
elseif nodeDrawtype == "nodebox" then
|
||||||
faces = meshport.create_nodebox_node(
|
faces = create_nodebox_node(pos, vContent[idx], vParam2[idx], neighbors)
|
||||||
minetest.get_name_from_content_id(content[idx]), area:position(idx), facedir, param2[idx], neighbors)
|
|
||||||
elseif nodeDrawtype == "mesh" then
|
elseif nodeDrawtype == "mesh" then
|
||||||
faces = meshport.create_mesh_node(nodeDef, playerName)
|
faces = create_mesh_node(nodeDef, vParam2[idx], playerName)
|
||||||
|
elseif nodeDrawtype == "plantlike" then
|
||||||
|
faces = create_plantlike_node(pos, vParam2[idx], nodeDef)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not faces then
|
if not faces then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not isCubicType then
|
|
||||||
faces:rotate_by_facedir(facedir)
|
|
||||||
end
|
|
||||||
|
|
||||||
faces:apply_tiles(nodeDef)
|
faces:apply_tiles(nodeDef)
|
||||||
return faces
|
return faces
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function initialize_resources()
|
||||||
|
meshport.texture_paths = meshport.get_asset_paths("textures")
|
||||||
|
meshport.texture_dimension_cache = {}
|
||||||
|
-- meshport.obj_paths is only loaded if needed
|
||||||
|
meshport.nodebox_cache = {}
|
||||||
|
meshport.mesh_cache = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function cleanup_resources()
|
||||||
|
meshport.texture_paths = nil
|
||||||
|
meshport.texture_dimension_cache = nil
|
||||||
|
meshport.obj_paths = nil
|
||||||
|
meshport.nodebox_cache = nil
|
||||||
|
meshport.mesh_cache = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.create_mesh(playerName, p1, p2, path)
|
function meshport.create_mesh(playerName, p1, p2, path)
|
||||||
meshport.print(playerName, "info", "Generating mesh...")
|
meshport.log(playerName, "info", "Generating mesh...")
|
||||||
|
initialize_resources()
|
||||||
|
|
||||||
p1, p2 = vector.sort(p1, p2)
|
p1, p2 = vector.sort(p1, p2)
|
||||||
local vm = minetest.get_voxel_manip()
|
local vm = minetest.get_voxel_manip()
|
||||||
|
|
||||||
-- Add one node of padding to area so we can read neighbor blocks.
|
-- 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 vp1, vp2 = vm:read_from_map(vector.subtract(p1, 1), vector.add(p2, 1))
|
||||||
local content = vm:get_data()
|
local vContent = vm:get_data()
|
||||||
local param2 = vm:get_param2_data()
|
local vParam2 = vm:get_param2_data()
|
||||||
|
|
||||||
-- Create a VoxelArea for converting from flat array indices to position vectors.
|
-- Create a VoxelArea for converting from flat array indices to position vectors.
|
||||||
local vArea = VoxelArea:new{MinEdge = vp1, MaxEdge = vp2}
|
local vArea = VoxelArea:new{MinEdge = vp1, MaxEdge = vp2}
|
||||||
|
local meshOrigin = vector.subtract(p1, 0.5)
|
||||||
local mesh = meshport.Mesh:new()
|
local mesh = meshport.Mesh:new()
|
||||||
local faces
|
|
||||||
|
|
||||||
-- Loop through all positions in the desired area.
|
-- Loop through all positions in the desired area.
|
||||||
for idx in vArea:iterp(p1, p2) do
|
for idx in vArea:iterp(p1, p2) do
|
||||||
-- Generate a mesh for the node.
|
-- Generate a mesh for the node.
|
||||||
faces = meshport.create_node(idx, vArea, content, param2, playerName)
|
local faces = create_node(idx, vArea, vContent, vParam2, playerName)
|
||||||
|
|
||||||
if faces then
|
if faces then
|
||||||
-- Move the node to its proper position in the mesh.
|
-- Move the node to its proper position.
|
||||||
faces:translate(vector.add(vector.subtract(vArea:position(idx), p1), 0.5))
|
faces:translate(vector.subtract(vArea:position(idx), meshOrigin))
|
||||||
|
|
||||||
for _, face in ipairs(faces.faces) do
|
-- Add faces to our final mesh.
|
||||||
-- Add each face to our final mesh.
|
mesh:insert_faces(faces)
|
||||||
mesh:insert_face(face)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
minetest.mkdir(path)
|
minetest.mkdir(path)
|
||||||
|
|
||||||
mesh:write_obj(path)
|
mesh:write_obj(path)
|
||||||
mesh:write_mtl(path, playerName)
|
mesh:write_mtl(path, playerName)
|
||||||
|
|
||||||
meshport.print(playerName, "info", "Finished. Saved to " .. path)
|
cleanup_resources()
|
||||||
|
meshport.log(playerName, "info", "Finished. Saved to " .. path)
|
||||||
end
|
end
|
||||||
|
57
init.lua
57
init.lua
@ -1,11 +1,29 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
|
|
||||||
|
This file is part of Meshport.
|
||||||
|
|
||||||
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
meshport = {
|
meshport = {
|
||||||
p1 = {},
|
player_data = {},
|
||||||
p2 = {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modpath = minetest.get_modpath("meshport")
|
modpath = minetest.get_modpath("meshport")
|
||||||
dofile(modpath .. "/helpers.lua")
|
dofile(modpath .. "/utils.lua")
|
||||||
dofile(modpath .. "/api.lua")
|
dofile(modpath .. "/mesh.lua")
|
||||||
dofile(modpath .. "/parse_obj.lua")
|
dofile(modpath .. "/parse_obj.lua")
|
||||||
dofile(modpath .. "/nodebox.lua")
|
dofile(modpath .. "/nodebox.lua")
|
||||||
dofile(modpath .. "/export.lua")
|
dofile(modpath .. "/export.lua")
|
||||||
@ -14,8 +32,7 @@ minetest.register_privilege("meshport", "Can save meshes with meshport.")
|
|||||||
|
|
||||||
minetest.register_on_leaveplayer(function(player, timed_out)
|
minetest.register_on_leaveplayer(function(player, timed_out)
|
||||||
local name = player:get_player_name()
|
local name = player:get_player_name()
|
||||||
meshport.p1[name] = nil
|
meshport.player_data[name] = nil
|
||||||
meshport.p2[name] = nil
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
for i = 1, 2 do
|
for i = 1, 2 do
|
||||||
@ -35,19 +52,23 @@ for i = 1, 2 do
|
|||||||
end
|
end
|
||||||
|
|
||||||
if not pos then
|
if not pos then
|
||||||
meshport.print(name, "error", "Not a valid position.")
|
meshport.log(name, "error", "Not a valid position.")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
pos = vector.round(pos)
|
pos = vector.round(pos)
|
||||||
|
|
||||||
if i == 1 then
|
if not meshport.player_data[name] then
|
||||||
meshport.p1[name] = pos
|
meshport.player_data[name] = {}
|
||||||
elseif i == 2 then
|
|
||||||
meshport.p2[name] = pos
|
|
||||||
end
|
end
|
||||||
|
|
||||||
meshport.print(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
|
if i == 1 then
|
||||||
|
meshport.player_data[name].p1 = pos
|
||||||
|
elseif i == 2 then
|
||||||
|
meshport.player_data[name].p2 = pos
|
||||||
|
end
|
||||||
|
|
||||||
|
meshport.log(name, "info", string.format("Position %i set to %s.", i, minetest.pos_to_string(pos)))
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@ -58,13 +79,15 @@ minetest.register_chatcommand("meshport", {
|
|||||||
privs = {meshport = true},
|
privs = {meshport = true},
|
||||||
|
|
||||||
func = function(name, filename)
|
func = function(name, filename)
|
||||||
if not meshport.p1[name] or not meshport.p2[name] then
|
local playerData = meshport.player_data[name] or {}
|
||||||
meshport.print(name, "error", "No area selected. Use /mesh1 and /mesh2 to select an area.")
|
|
||||||
|
if not playerData.p1 or not playerData.p2 then
|
||||||
|
meshport.log(name, "error", "No area selected. Use /mesh1 and /mesh2 to select an area.")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if filename:find("[^%w-_]") then
|
if filename:find("[^%w-_]") then
|
||||||
meshport.print(name, "error", "Invalid name supplied. Please use valid characters ([A-Z][a-z][0-9][-_]).")
|
meshport.log(name, "error", "Invalid name supplied. Please use valid characters ([A-Z][a-z][0-9][-_]).")
|
||||||
return
|
return
|
||||||
elseif filename == "" then
|
elseif filename == "" then
|
||||||
filename = os.date("%Y-%m-%d_%H-%M-%S")
|
filename = os.date("%Y-%m-%d_%H-%M-%S")
|
||||||
@ -74,12 +97,12 @@ minetest.register_chatcommand("meshport", {
|
|||||||
local folderName = name .. "_" .. filename
|
local folderName = name .. "_" .. filename
|
||||||
|
|
||||||
if table.indexof(minetest.get_dir_list(mpPath, true), folderName) > 0 then
|
if table.indexof(minetest.get_dir_list(mpPath, true), folderName) > 0 then
|
||||||
meshport.print(name, "error",
|
meshport.log(name, "error",
|
||||||
string.format("Folder %q already exists. Try using a different name.", folderName))
|
string.format("Folder %q already exists. Try using a different name.", folderName))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local path = mpPath .. DIR_DELIM .. folderName
|
local path = mpPath .. DIR_DELIM .. folderName
|
||||||
meshport.create_mesh(name, meshport.p1[name], meshport.p2[name], path)
|
meshport.create_mesh(name, playerData.p1, playerData.p2, path)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
338
mesh.lua
Normal file
338
mesh.lua
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
|
|
||||||
|
This file is part of Meshport.
|
||||||
|
|
||||||
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
|
--[[
|
||||||
|
A buffer of faces.
|
||||||
|
|
||||||
|
Faces are expected to be in this format:
|
||||||
|
{
|
||||||
|
verts = table, -- list of vertices (as vectors)
|
||||||
|
vert_norms = table, -- list of vertex normals (as vectors)
|
||||||
|
tex_coords = table, -- list of texture coordinates, e.g. {x = 0.5, y = 1}
|
||||||
|
tile_idx = int, -- index of tile to use
|
||||||
|
use_special_tiles = bool, -- if true, use tiles from special_tiles field of nodedef
|
||||||
|
texture = string, -- name of actual texture to use)
|
||||||
|
}
|
||||||
|
|
||||||
|
Note to self/contributors--to avoid weird bugs, please follow these rules
|
||||||
|
regarding table fields:
|
||||||
|
|
||||||
|
1. Each table of vertices, vertex normals, or texture coordinates should be
|
||||||
|
unique to its parent face. That is, multiple faces or Faces objects
|
||||||
|
should not share references to the same table.
|
||||||
|
2. Values within these tables are allowed to be duplicated. For example, one
|
||||||
|
face can reference the same vertex normal four times, and other faces or
|
||||||
|
Faces objects can also reference the same vertex normal.
|
||||||
|
]]
|
||||||
|
meshport.Faces = {}
|
||||||
|
|
||||||
|
function meshport.Faces:new()
|
||||||
|
local o = { -- TODO: Separate tables for vertices/indices.
|
||||||
|
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:insert_all(faces)
|
||||||
|
for _, face in ipairs(faces.faces) do
|
||||||
|
table.insert(self.faces, face)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Faces:copy()
|
||||||
|
local newFaces = meshport.Faces:new()
|
||||||
|
newFaces.faces = table.copy(self.faces)
|
||||||
|
return newFaces
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Faces:translate(vec)
|
||||||
|
if vec.x == 0 and vec.y == 0 and vec.z == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
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:scale(scale)
|
||||||
|
if scale == 1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, face in ipairs(self.faces) do
|
||||||
|
for i, vert in ipairs(face.verts) do
|
||||||
|
face.verts[i] = vector.multiply(vert, scale)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Faces:rotate_by_facedir(facedir)
|
||||||
|
if facedir == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, face in ipairs(self.faces) do
|
||||||
|
for i, vert in ipairs(face.verts) do
|
||||||
|
face.verts[i] = meshport.rotate_vector_by_facedir(vert, facedir)
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, norm in ipairs(face.vert_norms) do
|
||||||
|
face.vert_norms[i] = meshport.rotate_vector_by_facedir(norm, facedir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Faces:rotate_xz_degrees(degrees)
|
||||||
|
if degrees == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local rad = math.rad(degrees)
|
||||||
|
local sinRad = math.sin(rad)
|
||||||
|
local cosRad = math.cos(rad)
|
||||||
|
|
||||||
|
for _, face in ipairs(self.faces) do
|
||||||
|
for i, vert in ipairs(face.verts) do
|
||||||
|
face.verts[i] = vector.new(
|
||||||
|
vert.x * cosRad - vert.z * sinRad,
|
||||||
|
vert.y,
|
||||||
|
vert.x * sinRad + vert.z * cosRad
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, norm in ipairs(face.vert_norms) do
|
||||||
|
face.vert_norms[i] = vector.new(
|
||||||
|
norm.x * cosRad - norm.z * sinRad,
|
||||||
|
norm.y,
|
||||||
|
norm.x * sinRad + norm.z * cosRad
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Faces:apply_tiles(nodeDef)
|
||||||
|
for _, face in ipairs(self.faces) do
|
||||||
|
local tiles
|
||||||
|
if face.use_special_tiles then
|
||||||
|
tiles = nodeDef.special_tiles
|
||||||
|
else
|
||||||
|
tiles = nodeDef.tiles
|
||||||
|
end
|
||||||
|
|
||||||
|
local tile = meshport.get_tile(tiles, face.tile_idx)
|
||||||
|
face.texture = tile.name or tile
|
||||||
|
|
||||||
|
-- If an animated texture is used, scale texture coordinates so only the first image is used.
|
||||||
|
local animation = tile.animation
|
||||||
|
if type(animation) == "table" then
|
||||||
|
local xScale, yScale = 1, 1
|
||||||
|
|
||||||
|
if animation.type == "vertical_frames" then
|
||||||
|
local texW, texH = meshport.get_texture_dimensions(tile.name)
|
||||||
|
if texW and texH then
|
||||||
|
xScale = (animation.aspect_w or 16) / texW
|
||||||
|
yScale = (animation.aspect_h or 16) / texH
|
||||||
|
end
|
||||||
|
elseif animation.type == "sheet_2d" then
|
||||||
|
xScale = 1 / (animation.frames_w or 1)
|
||||||
|
yScale = 1 / (animation.frames_h or 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if xScale ~= 1 or yScale ~= 1 then
|
||||||
|
for i, coord in ipairs(face.tex_coords) do
|
||||||
|
face.tex_coords[i] = {x = coord.x * xScale, y = coord.y * yScale}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function clean_vector(vec)
|
||||||
|
-- Prevents an issue involving negative zero values, which are not handled properly by `string.format`.
|
||||||
|
return vector.new(
|
||||||
|
vec.x == 0 and 0 or vec.x,
|
||||||
|
vec.y == 0 and 0 or vec.y,
|
||||||
|
vec.z == 0 and 0 or vec.z
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function bimap_find_or_insert(forward, reverse, item)
|
||||||
|
local idx = reverse[item]
|
||||||
|
if not idx then
|
||||||
|
idx = #forward + 1
|
||||||
|
forward[idx] = item
|
||||||
|
reverse[item] = idx
|
||||||
|
end
|
||||||
|
return idx
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Stores a mesh in a form which is easily convertible to an .OBJ file.
|
||||||
|
meshport.Mesh = {}
|
||||||
|
|
||||||
|
function meshport.Mesh:new()
|
||||||
|
local o = {
|
||||||
|
-- Using two tables for elements makes insert_face() significantly faster.
|
||||||
|
-- verts[1] = "0 -1 0"
|
||||||
|
-- verts_reverse["0 -1 0"] = 1
|
||||||
|
-- etc...
|
||||||
|
verts = {},
|
||||||
|
verts_reverse = {},
|
||||||
|
vert_norms = {},
|
||||||
|
vert_norms_reverse = {},
|
||||||
|
tex_coords = {},
|
||||||
|
tex_coords_reverse = {},
|
||||||
|
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 = clean_vector(vector.new(vert.x, vert.y, -vert.z))
|
||||||
|
elementStr = string.format("%f %f %f", vec.x, vec.y, vec.z)
|
||||||
|
indices.verts[i] = bimap_find_or_insert(self.verts, self.verts_reverse, elementStr)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add texture coordinates (UV map).
|
||||||
|
for i, texCoord in ipairs(face.tex_coords) do
|
||||||
|
elementStr = string.format("%f %f", texCoord.x, texCoord.y)
|
||||||
|
indices.tex_coords[i] = bimap_find_or_insert(self.tex_coords, self.tex_coords_reverse, elementStr)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add vertex normals.
|
||||||
|
for i, vertNorm in ipairs(face.vert_norms) do
|
||||||
|
-- Invert Z axis to comply with Blender's coordinate system.
|
||||||
|
vec = clean_vector(vector.new(vertNorm.x, vertNorm.y, -vertNorm.z))
|
||||||
|
elementStr = string.format("%f %f %f", vec.x, vec.y, vec.z)
|
||||||
|
indices.vert_norms[i] = bimap_find_or_insert(self.vert_norms, self.vert_norms_reverse, elementStr)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add faces to mesh.
|
||||||
|
if not self.faces[face.texture] then
|
||||||
|
self.faces[face.texture] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local vertStrs = {}
|
||||||
|
|
||||||
|
for i = 1, #indices.verts do
|
||||||
|
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
|
||||||
|
|
||||||
|
table.insert(self.faces[face.texture], table.concat(vertStrs, " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Mesh:insert_faces(faces)
|
||||||
|
for _, face in ipairs(faces.faces) do
|
||||||
|
self:insert_face(face)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Mesh:write_obj(path)
|
||||||
|
local objFile = io.open(path .. "/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(string.format("v %s\n", vert))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write texture coordinates.
|
||||||
|
for _, texCoord in ipairs(self.tex_coords) do
|
||||||
|
objFile:write(string.format("vt %s\n", texCoord))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Write vertex normals.
|
||||||
|
for _, vertNorm in ipairs(self.vert_norms) do
|
||||||
|
objFile:write(string.format("vn %s\n", 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(string.format("f %s\n", face))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
objFile:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
function meshport.Mesh:write_mtl(path, playerName)
|
||||||
|
local matFile = io.open(path .. "/materials.mtl", "w")
|
||||||
|
|
||||||
|
matFile:write("# Created using meshport (https://github.com/random-geek/meshport).\n")
|
||||||
|
|
||||||
|
-- Write material information.
|
||||||
|
for mat, _ in pairs(self.faces) do
|
||||||
|
matFile:write(string.format("\nnewmtl %s\n", mat))
|
||||||
|
|
||||||
|
-- Attempt to get the base texture, ignoring texture modifiers.
|
||||||
|
local texName = string.match(mat, "[%w%s%-_%.]+%.png") or mat
|
||||||
|
|
||||||
|
if meshport.texture_paths[texName] then
|
||||||
|
if texName ~= mat then
|
||||||
|
meshport.log(playerName, "warning", string.format("Ignoring texture modifers in material %q.", mat))
|
||||||
|
end
|
||||||
|
|
||||||
|
matFile:write(string.format("map_Kd %s\n", meshport.texture_paths[texName]))
|
||||||
|
else
|
||||||
|
meshport.log(playerName, "warning",
|
||||||
|
string.format("Could not find texture %q. Using a dummy material instead.", texName))
|
||||||
|
matFile:write(string.format("Kd %f %f %f\n", math.random(), math.random(), math.random()))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
matFile:close()
|
||||||
|
end
|
144
nodebox.lua
144
nodebox.lua
@ -1,4 +1,24 @@
|
|||||||
meshport.side_box_names = {
|
--[[
|
||||||
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
|
|
||||||
|
This file is part of Meshport.
|
||||||
|
|
||||||
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
|
|
||||||
|
local SIDE_BOX_NAMES = {
|
||||||
"top", -- Y+
|
"top", -- Y+
|
||||||
"bottom", -- Y-
|
"bottom", -- Y-
|
||||||
"right", -- X+
|
"right", -- X+
|
||||||
@ -7,7 +27,8 @@ meshport.side_box_names = {
|
|||||||
"front", -- Z-
|
"front", -- Z-
|
||||||
}
|
}
|
||||||
|
|
||||||
function meshport.sort_box(box)
|
|
||||||
|
local function sort_box(box)
|
||||||
return {
|
return {
|
||||||
math.min(box[1], box[4]),
|
math.min(box[1], box[4]),
|
||||||
math.min(box[2], box[5]),
|
math.min(box[2], box[5]),
|
||||||
@ -18,15 +39,41 @@ function meshport.sort_box(box)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function 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
|
||||||
|
|
||||||
|
|
||||||
|
-- A list of node boxes, in the format used by Minetest:
|
||||||
|
-- {a.x, a.y, a.z, b.x, b.y, b.z}
|
||||||
|
-- Individual boxes inside the `boxes` array are not mutated.
|
||||||
meshport.Boxes = {}
|
meshport.Boxes = {}
|
||||||
|
|
||||||
function meshport.Boxes:new(boxes)
|
function meshport.Boxes:new(boxes)
|
||||||
local o = {}
|
local o = {}
|
||||||
|
|
||||||
if type(boxes) ~= "table" or type(boxes[1]) == "number" then
|
if type(boxes) == "table" and type(boxes[1]) == "table" then
|
||||||
o.boxes = {boxes}
|
-- Copy boxes individually to avoid mutating the argument.
|
||||||
|
o.boxes = {}
|
||||||
|
for i, box in ipairs(boxes) do
|
||||||
|
o.boxes[i] = box
|
||||||
|
end
|
||||||
else
|
else
|
||||||
o.boxes = boxes
|
o.boxes = {boxes}
|
||||||
end
|
end
|
||||||
|
|
||||||
setmetatable(o, self)
|
setmetatable(o, self)
|
||||||
@ -34,9 +81,13 @@ function meshport.Boxes:new(boxes)
|
|||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function meshport.Boxes:insert_box(box)
|
||||||
|
table.insert(self.boxes, box)
|
||||||
|
end
|
||||||
|
|
||||||
function meshport.Boxes:insert_all(boxes)
|
function meshport.Boxes:insert_all(boxes)
|
||||||
for _, box in ipairs(boxes.boxes) do
|
for _, box in ipairs(boxes.boxes) do
|
||||||
table.insert(self.boxes, table.copy(box))
|
table.insert(self.boxes, box)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -63,62 +114,66 @@ function meshport.Boxes:rotate_by_facedir(facedir)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function meshport.Boxes:get_leveled(level)
|
function meshport.Boxes:get_leveled(level)
|
||||||
local newBoxes = meshport.Boxes:new(table.copy(self.boxes))
|
local newBoxes = meshport.Boxes:new()
|
||||||
|
|
||||||
for i, box in ipairs(newBoxes.boxes) do
|
for i, box in ipairs(self.boxes) do
|
||||||
box = meshport.sort_box(box)
|
local newBox = sort_box(box)
|
||||||
box[5] = level / 64 - 0.5
|
newBox[5] = level / 64 - 0.5
|
||||||
newBoxes.boxes[i] = box
|
newBoxes.boxes[i] = newBox
|
||||||
end
|
end
|
||||||
|
|
||||||
return newBoxes
|
return newBoxes
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.Boxes:to_faces(nodeTiles, pos, facedir)
|
function meshport.Boxes:to_faces(nodeDef, pos, facedir, tileIdx, useSpecial)
|
||||||
|
local tiles = useSpecial and nodeDef.special_tiles or nodeDef.tiles
|
||||||
|
local vec = vector.new
|
||||||
|
|
||||||
local faces = meshport.Faces:new()
|
local faces = meshport.Faces:new()
|
||||||
|
|
||||||
for _, b in ipairs(self.boxes) do
|
for _, box in ipairs(self.boxes) do
|
||||||
b = meshport.sort_box(b)
|
local b = sort_box(box)
|
||||||
|
|
||||||
local sideFaces = {
|
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+
|
{vec(b[1], b[5], b[3]), vec(b[4], b[5], b[3]), vec(b[4], b[5], b[6]), vec(b[1], b[5], b[6])}, -- 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-
|
{vec(b[1], b[2], b[6]), vec(b[4], b[2], b[6]), vec(b[4], b[2], b[3]), vec(b[1], b[2], b[3])}, -- 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+
|
{vec(b[4], b[2], b[3]), vec(b[4], b[2], b[6]), vec(b[4], b[5], b[6]), vec(b[4], b[5], 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-
|
{vec(b[1], b[2], b[6]), vec(b[1], b[2], b[3]), vec(b[1], b[5], b[3]), vec(b[1], b[5], 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+
|
{vec(b[4], b[2], b[6]), vec(b[1], b[2], b[6]), vec(b[1], b[5], b[6]), vec(b[4], b[5], 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-
|
{vec(b[1], b[2], b[3]), vec(b[4], b[2], b[3]), vec(b[4], b[5], b[3]), vec(b[1], b[5], b[3])}, -- Z-
|
||||||
}
|
}
|
||||||
|
|
||||||
local sideTexCoords = {
|
local t = {}
|
||||||
{{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 vertNorm
|
|
||||||
|
|
||||||
for i = 1, 6 do
|
for i = 1, 6 do
|
||||||
-- Fix offset texture coordinates.
|
t[i] = b[i] + 0.5 -- Texture coordinates range from 0 to 1
|
||||||
for v = 1, 4 do
|
|
||||||
sideTexCoords[i][v] = {x = sideTexCoords[i][v].x + 0.5, y = sideTexCoords[i][v].y + 0.5}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
vertNorm = meshport.neighbor_dirs[i]
|
local sideTexCoords = {
|
||||||
|
{{x = t[1], y = t[3]}, {x = t[4], y = t[3]}, {x = t[4], y = t[6]}, {x = t[1], y = t[6]}}, -- Y+
|
||||||
|
{{x = t[1], y =1-t[6]}, {x = t[4], y =1-t[6]}, {x = t[4], y =1-t[3]}, {x = t[1], y =1-t[3]}}, -- Y-
|
||||||
|
{{x = t[3], y = t[2]}, {x = t[6], y = t[2]}, {x = t[6], y = t[5]}, {x = t[3], y = t[5]}}, -- X+
|
||||||
|
{{x =1-t[6], y = t[2]}, {x =1-t[3], y = t[2]}, {x =1-t[3], y = t[5]}, {x =1-t[6], y = t[5]}}, -- X-
|
||||||
|
{{x =1-t[4], y = t[2]}, {x =1-t[1], y = t[2]}, {x =1-t[1], y = t[5]}, {x =1-t[4], y = t[5]}}, -- Z+
|
||||||
|
{{x = t[1], y = t[2]}, {x = t[4], y = t[2]}, {x = t[4], y = t[5]}, {x = t[1], y = t[5]}}, -- Z-
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 1, 6 do
|
||||||
|
local norm = meshport.NEIGHBOR_DIRS[i]
|
||||||
|
|
||||||
faces:insert_face(meshport.prepare_cuboid_face({
|
faces:insert_face(meshport.prepare_cuboid_face({
|
||||||
verts = sideFaces[i],
|
verts = sideFaces[i],
|
||||||
tex_coords = sideTexCoords[i],
|
tex_coords = sideTexCoords[i],
|
||||||
vert_norms = {vertNorm, vertNorm, vertNorm, vertNorm},
|
vert_norms = {norm, norm, norm, norm},
|
||||||
}, nodeTiles, pos, facedir, i))
|
tile_idx = tileIdx,
|
||||||
|
use_special_tiles = useSpecial,
|
||||||
|
}, tiles, pos, facedir, i))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return faces
|
return faces
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.prepare_nodebox(nodebox)
|
function meshport.prepare_nodebox(nodebox)
|
||||||
local prepNodebox = {}
|
local prepNodebox = {}
|
||||||
prepNodebox.type = nodebox.type
|
prepNodebox.type = nodebox.type
|
||||||
@ -132,7 +187,7 @@ function meshport.prepare_nodebox(nodebox)
|
|||||||
prepNodebox.connected = {}
|
prepNodebox.connected = {}
|
||||||
prepNodebox.disconnected = {}
|
prepNodebox.disconnected = {}
|
||||||
|
|
||||||
for i, name in ipairs(meshport.side_box_names) do
|
for i, name in ipairs(SIDE_BOX_NAMES) do
|
||||||
prepNodebox.connected[i] = meshport.Boxes:new(nodebox["connect_" .. name])
|
prepNodebox.connected[i] = meshport.Boxes:new(nodebox["connect_" .. name])
|
||||||
prepNodebox.disconnected[i] = meshport.Boxes:new(nodebox["disconnected_" .. name])
|
prepNodebox.disconnected[i] = meshport.Boxes:new(nodebox["disconnected_" .. name])
|
||||||
end
|
end
|
||||||
@ -145,20 +200,21 @@ function meshport.prepare_nodebox(nodebox)
|
|||||||
prepNodebox.wall_side = meshport.Boxes:new(nodebox.wall_side)
|
prepNodebox.wall_side = meshport.Boxes:new(nodebox.wall_side)
|
||||||
|
|
||||||
-- Rotate the boxes so they are in the correct orientation after rotation by facedir.
|
-- 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_top:transform(function(v) return vector.new(-v.x, -v.y, v.z) end)
|
||||||
prepNodebox.wall_side:transform(function(v) return {x = -v.z, y = v.x, z = v.y} end)
|
prepNodebox.wall_side:transform(function(v) return vector.new(-v.z, v.x, v.y) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return prepNodebox
|
return prepNodebox
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors)
|
|
||||||
|
function meshport.collect_boxes(prepNodebox, nodeDef, param2, facedir, neighbors)
|
||||||
local boxes = meshport.Boxes:new()
|
local boxes = meshport.Boxes:new()
|
||||||
|
|
||||||
if prepNodebox.fixed then
|
if prepNodebox.fixed then
|
||||||
if prepNodebox.type == "leveled" then
|
if prepNodebox.type == "leveled" then
|
||||||
boxes:insert_all(prepNodebox.fixed:get_leveled(
|
local level = nodeDef.paramtype2 == "leveled" and param2 or nodeDef.leveled or 0
|
||||||
nodeDef.paramtype2 == "leveled" and param2 or nodeDef.leveled or 0))
|
boxes:insert_all(prepNodebox.fixed:get_leveled(level))
|
||||||
else
|
else
|
||||||
boxes:insert_all(prepNodebox.fixed)
|
boxes:insert_all(prepNodebox.fixed)
|
||||||
end
|
end
|
||||||
@ -170,7 +226,7 @@ function meshport.collect_boxes(prepNodebox, nodeDef, facedir, param2, neighbors
|
|||||||
for i = 1, 6 do
|
for i = 1, 6 do
|
||||||
neighborName = minetest.get_name_from_content_id(neighbors[i])
|
neighborName = minetest.get_name_from_content_id(neighbors[i])
|
||||||
|
|
||||||
if meshport.node_connects_to(neighborName, nodeDef.connects_to) then
|
if node_connects_to(neighborName, nodeDef.connects_to) then
|
||||||
boxes:insert_all(prepNodebox.connected[i])
|
boxes:insert_all(prepNodebox.connected[i])
|
||||||
else
|
else
|
||||||
boxes:insert_all(prepNodebox.disconnected[i])
|
boxes:insert_all(prepNodebox.disconnected[i])
|
||||||
|
122
parse_obj.lua
122
parse_obj.lua
@ -1,31 +1,50 @@
|
|||||||
function meshport.parse_vector_element(elementStr)
|
--[[
|
||||||
local elementType
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
local vec = {}
|
|
||||||
|
|
||||||
-- Get the element type and vector. `vec.z` will be left `nil` for two-dimensional vectors.
|
This file is part of Meshport.
|
||||||
elementType, vec.x, vec.y, vec.z = string.match(elementStr, "^(%a+)%s([%d%.%-]+)%s([%d%.%-]+)%s?([%d%.%-]*)")
|
|
||||||
|
|
||||||
for k, v in pairs(vec) do
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
vec[k] = tonumber(v)
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
end
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
|
-- See the OBJ file specification: http://www.martinreddy.net/gfx/3d/OBJ.spec
|
||||||
|
-- Also, the Irrlicht implementation: irrlicht/source/Irrlicht/COBJMeshFileLoader.cpp
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_vector_element(elementType, elementStr)
|
||||||
|
if elementType == "v" or elementType == "vn" then
|
||||||
|
-- Note that there may be an optional weight value after z, which is ignored.
|
||||||
|
local xs, ys, zs = string.match(elementStr, "^([%d%.%-]+)%s+([%d%.%-]+)%s+([%d%.%-]+)")
|
||||||
|
-- The X axis of vectors is inverted to match the Minetest coordinate system.
|
||||||
|
local vec = vector.new(-tonumber(xs), tonumber(ys), tonumber(zs))
|
||||||
|
|
||||||
-- Return the element type and value.
|
|
||||||
if elementType == "v" then
|
if elementType == "v" then
|
||||||
-- Invert X axis to match the Minetest coordinate system.
|
|
||||||
vec.x = -vec.x
|
|
||||||
return "verts", vec
|
return "verts", vec
|
||||||
elseif elementType == "vt" then
|
else
|
||||||
return "tex_coords", vec
|
|
||||||
elseif elementType == "vn" then
|
|
||||||
vec.x = -vec.x
|
|
||||||
return "vert_norms", vec
|
return "vert_norms", vec
|
||||||
end
|
end
|
||||||
|
elseif elementType == "vt" then
|
||||||
|
local xs, ys = string.match(elementStr, "^([%d%.%-]+)%s+([%d%.%-]+)")
|
||||||
|
local coords = {x = tonumber(xs), y = tonumber(ys)}
|
||||||
|
assert(coords.x and coords.y, "Invalid texture coordinate element")
|
||||||
|
return "tex_coords", coords
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.parse_face_element(elements, elementStr)
|
|
||||||
|
local function parse_face_element(elements, faceStr)
|
||||||
-- Split the face element into strings containing the indices of elements associated with each vertex.
|
-- 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 vertStrs = string.split(faceStr, " ")
|
||||||
local elementIndices
|
|
||||||
|
|
||||||
local face = {
|
local face = {
|
||||||
verts = {},
|
verts = {},
|
||||||
@ -34,58 +53,77 @@ function meshport.parse_face_element(elements, elementStr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, vertStr in ipairs(vertStrs) do
|
for i, vertStr in ipairs(vertStrs) do
|
||||||
-- Split the string into a table of indices for position, texture coordinate, and/or vertex normal elements.
|
-- Split the string into indices for vertex, texture coordinate, and/or vertex normal elements.
|
||||||
elementIndices = string.split(vertStr, "/", true)
|
local vs, vts, vns = string.match(vertStr, "^(%d*)/?(%d*)/?(%d*)$")
|
||||||
|
local vi, vti, vni = tonumber(vs), tonumber(vts), tonumber(vns)
|
||||||
|
assert(vi, "Invalid face element")
|
||||||
|
|
||||||
for k, v in pairs(elementIndices) do
|
-- Set the position, texture coordinate, and vertex normal of the vertex.
|
||||||
elementIndices[k] = tonumber(v)
|
-- Note that vti or vni are allowed to be nil
|
||||||
end
|
face.verts[i] = elements.verts[vi]
|
||||||
|
face.tex_coords[i] = elements.tex_coords[vti]
|
||||||
-- Set the position, texture coordinate, and vertex normal of the face. `or 0` prevents a nil index error.
|
face.vert_norms[i] = elements.vert_norms[vni]
|
||||||
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
|
end
|
||||||
|
|
||||||
return face
|
return face
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function handle_group(groups, elementStr)
|
||||||
|
-- Note: Minetest ignores usemtl; see `OBJ_LOADER_IGNORE_MATERIAL_FILES`.
|
||||||
|
-- The format allows multiple group names; get only the first one.
|
||||||
|
local groupName = string.match(elementStr, "^(%S+)")
|
||||||
|
if not groupName then
|
||||||
|
-- "default" is the default group name if no name is specified.
|
||||||
|
groupName = "default"
|
||||||
|
end
|
||||||
|
local groupIdx = table.indexof(groups, groupName)
|
||||||
|
|
||||||
|
-- If this group has not been used yet, add it to the list.
|
||||||
|
if groupIdx < 0 then
|
||||||
|
table.insert(groups, groupName)
|
||||||
|
groupIdx = #groups
|
||||||
|
end
|
||||||
|
|
||||||
|
return groupIdx
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.parse_obj(path)
|
function meshport.parse_obj(path)
|
||||||
local faces = meshport.Faces:new()
|
|
||||||
local file = io.open(path, "r")
|
local file = io.open(path, "r")
|
||||||
|
|
||||||
|
local faces = meshport.Faces:new()
|
||||||
local elements = {
|
local elements = {
|
||||||
verts = {},
|
verts = {},
|
||||||
tex_coords = {},
|
tex_coords = {},
|
||||||
vert_norms = {},
|
vert_norms = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- Tiles are assigned according to groups, in the order in which groups are defined.
|
||||||
local groups = {}
|
local groups = {}
|
||||||
local curGroup
|
local currentTileIdx
|
||||||
local elementType
|
|
||||||
|
|
||||||
for line in file:lines() do
|
for line in file:lines() do
|
||||||
elementType = string.sub(line, 1, 1)
|
-- elementStr may be an empty string, e.g. "g" with no group name.
|
||||||
|
local elementType, elementStr = string.match(line, "^(%a+)%s*(.*)")
|
||||||
|
|
||||||
if elementType == "v" then
|
if elementType == "v" or elementType == "vt" or elementType == "vn" then
|
||||||
-- Parse the vector element. Used for "v", "vt", and "vn".
|
local dest, value = parse_vector_element(elementType, elementStr)
|
||||||
local type, value = meshport.parse_vector_element(line)
|
table.insert(elements[dest], value)
|
||||||
table.insert(elements[type], value)
|
|
||||||
elseif elementType == "f" then
|
elseif elementType == "f" then
|
||||||
-- If the face is not part of any group, use the placeholder group `0`.
|
-- If the face is not part of any group, use the placeholder group `0`.
|
||||||
if not curGroup then
|
if not currentTileIdx then
|
||||||
table.insert(groups, 0)
|
table.insert(groups, 0)
|
||||||
curGroup = table.indexof(groups, 0)
|
currentTileIdx = #groups
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse the face element.
|
-- Parse the face element.
|
||||||
local face = meshport.parse_face_element(elements, line)
|
local face = parse_face_element(elements, elementStr)
|
||||||
-- Assign materials according to the group.
|
-- Assign materials according to the group.
|
||||||
face.tile_idx = curGroup
|
face.tile_idx = currentTileIdx
|
||||||
faces:insert_face(face)
|
faces:insert_face(face)
|
||||||
elseif elementType == "g" then
|
elseif elementType == "g" then
|
||||||
-- If this group has not been used yet, then add it to the list.
|
currentTileIdx = handle_group(groups, elementStr)
|
||||||
curGroup = meshport.find_or_insert(groups, string.match(line, "^g%s(.+)"))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,13 +1,47 @@
|
|||||||
meshport.neighbor_dirs = {
|
--[[
|
||||||
{x = 0, y = 1, z = 0}, -- Y+
|
Copyright (C) 2021 random-geek (https://github.com/random-geek)
|
||||||
{x = 0, y = -1, z = 0}, -- Y-
|
|
||||||
{x = 1, y = 0, z = 0}, -- X+
|
This file is part of Meshport.
|
||||||
{x = -1, y = 0, z = 0}, -- X-
|
|
||||||
{x = 0, y = 0, z = 1}, -- Z+
|
Meshport is free software: you can redistribute it and/or modify it under
|
||||||
{x = 0, y = 0, z = -1}, -- Z-
|
the terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public License
|
||||||
|
along with Meshport. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
]]
|
||||||
|
|
||||||
|
meshport.NEIGHBOR_DIRS = {
|
||||||
|
-- face neighbors
|
||||||
|
vector.new( 0, 1, 0), -- 1
|
||||||
|
vector.new( 0,-1, 0),
|
||||||
|
vector.new( 1, 0, 0),
|
||||||
|
vector.new(-1, 0, 0),
|
||||||
|
vector.new( 0, 0, 1),
|
||||||
|
vector.new( 0, 0,-1),
|
||||||
|
|
||||||
|
-- edge neighbors
|
||||||
|
vector.new(-1, 1, 0), -- 7
|
||||||
|
vector.new( 1, 1, 0),
|
||||||
|
vector.new( 0, 1, 1),
|
||||||
|
vector.new( 0, 1,-1),
|
||||||
|
vector.new(-1, 0, 1),
|
||||||
|
vector.new( 1, 0, 1),
|
||||||
|
vector.new(-1, 0,-1),
|
||||||
|
vector.new( 1, 0,-1),
|
||||||
|
vector.new(-1,-1, 0),
|
||||||
|
vector.new( 1,-1, 0),
|
||||||
|
vector.new( 0,-1, 1),
|
||||||
|
vector.new( 0,-1,-1),
|
||||||
}
|
}
|
||||||
|
|
||||||
meshport.facedir_to_tile_indices = {
|
local FACEDIR_TO_TILE_INDICES = {
|
||||||
[0] =
|
[0] =
|
||||||
{1, 2, 3, 4, 5, 6},
|
{1, 2, 3, 4, 5, 6},
|
||||||
{1, 2, 5, 6, 4, 3},
|
{1, 2, 5, 6, 4, 3},
|
||||||
@ -35,42 +69,43 @@ meshport.facedir_to_tile_indices = {
|
|||||||
{2, 1, 5, 6, 3, 4},
|
{2, 1, 5, 6, 3, 4},
|
||||||
}
|
}
|
||||||
|
|
||||||
meshport.facedir_to_tile_rotations = {
|
local FACEDIR_TO_TILE_ROTATIONS = {
|
||||||
[0] =
|
[0] =
|
||||||
{0, 0, 0, 0, 0, 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},
|
{1, 3, 0, 0, 0, 0},
|
||||||
{0, 2, 3, 1, 2, 0},
|
{2, 2, 0, 0, 0, 0},
|
||||||
{0, 2, 3, 1, 1, 1},
|
{3, 1, 0, 0, 0, 0},
|
||||||
{0, 2, 3, 1, 0, 2},
|
{0, 2, 1, 3, 2, 0},
|
||||||
{0, 2, 3, 1, 3, 3},
|
{0, 2, 1, 3, 3, 3},
|
||||||
{2, 0, 1, 3, 2, 0},
|
{0, 2, 1, 3, 0, 2},
|
||||||
{2, 0, 1, 3, 3, 3},
|
{0, 2, 1, 3, 1, 1},
|
||||||
{2, 0, 1, 3, 0, 2},
|
{2, 0, 3, 1, 2, 0},
|
||||||
{2, 0, 1, 3, 1, 1},
|
{2, 0, 3, 1, 1, 1},
|
||||||
{3, 3, 3, 3, 1, 3},
|
{2, 0, 3, 1, 0, 2},
|
||||||
{3, 3, 2, 0, 1, 3},
|
{2, 0, 3, 1, 3, 3},
|
||||||
{3, 3, 1, 1, 1, 3},
|
|
||||||
{3, 3, 0, 2, 1, 3},
|
|
||||||
{1, 1, 1, 1, 3, 1},
|
{1, 1, 1, 1, 3, 1},
|
||||||
{1, 1, 2, 0, 3, 1},
|
{1, 1, 2, 0, 3, 1},
|
||||||
{1, 1, 3, 3, 3, 1},
|
{1, 1, 3, 3, 3, 1},
|
||||||
{1, 1, 0, 2, 3, 1},
|
{1, 1, 0, 2, 3, 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},
|
||||||
{2, 2, 2, 2, 2, 2},
|
{2, 2, 2, 2, 2, 2},
|
||||||
{3, 1, 2, 2, 2, 2},
|
|
||||||
{0, 0, 2, 2, 2, 2},
|
|
||||||
{1, 3, 2, 2, 2, 2},
|
{1, 3, 2, 2, 2, 2},
|
||||||
|
{0, 0, 2, 2, 2, 2},
|
||||||
|
{3, 1, 2, 2, 2, 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
meshport.wallmounted_to_facedir = {[0] = 20, 0, 17, 15, 8, 6}
|
local WALLMOUNTED_TO_FACEDIR = {[0] = 20, 0, 17, 15, 8, 6}
|
||||||
|
|
||||||
meshport.drawtype_aliases = {
|
local DRAWTYPE_ALIASES = {
|
||||||
allfaces_optional = "allfaces",
|
allfaces_optional = "allfaces",
|
||||||
glasslike_framed_optional = "glasslike_framed",
|
glasslike_framed_optional = "glasslike",
|
||||||
}
|
}
|
||||||
|
|
||||||
function meshport.print(name, level, s)
|
|
||||||
|
function meshport.log(name, level, s)
|
||||||
local message
|
local message
|
||||||
|
|
||||||
if level == "info" then
|
if level == "info" then
|
||||||
@ -84,27 +119,6 @@ function meshport.print(name, level, s)
|
|||||||
minetest.chat_send_player(name, "[meshport] " .. message)
|
minetest.chat_send_player(name, "[meshport] " .. message)
|
||||||
end
|
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)
|
function meshport.rotate_vector_by_facedir(vec, facedir)
|
||||||
local v = vector.new(vec)
|
local v = vector.new(vec)
|
||||||
@ -135,36 +149,68 @@ function meshport.rotate_vector_by_facedir(vec, facedir)
|
|||||||
return v
|
return v
|
||||||
end
|
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.
|
function meshport.translate_texture_coordinates(texCoords, offset)
|
||||||
if rot == 1 then
|
if offset.x == 0 and offset.y == 0 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
|
return texCoords
|
||||||
end
|
end
|
||||||
|
|
||||||
local newTexCoords = {}
|
local newTexCoords = {}
|
||||||
|
|
||||||
for _, texCoord in ipairs(texCoords) do
|
for _, tc in ipairs(texCoords) do
|
||||||
table.insert(newTexCoords, meshport.rotate_texture_coordinate(texCoord, rot))
|
table.insert(newTexCoords, {x = tc.x + offset.x, y = tc.y + offset.y})
|
||||||
end
|
end
|
||||||
|
|
||||||
return newTexCoords
|
return newTexCoords
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scale)
|
|
||||||
|
function meshport.rotate_texture_coordinates_rad(texCoords, rad)
|
||||||
|
if rad == 0 then
|
||||||
|
return texCoords
|
||||||
|
end
|
||||||
|
|
||||||
|
local sinRad = math.sin(rad)
|
||||||
|
local cosRad = math.cos(rad)
|
||||||
|
local newTexCoords = {}
|
||||||
|
|
||||||
|
for _, texCoord in ipairs(texCoords) do
|
||||||
|
-- Coordinates are rotated around (0.5, 0.5).
|
||||||
|
local x = texCoord.x - 0.5
|
||||||
|
local y = texCoord.y - 0.5
|
||||||
|
table.insert(newTexCoords, {
|
||||||
|
x = x * cosRad - y * sinRad + 0.5,
|
||||||
|
y = x * sinRad + y * cosRad + 0.5
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return newTexCoords
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function rotate_texture_coordinates(texCoords, rot)
|
||||||
|
if rot == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, tc in ipairs(texCoords) do
|
||||||
|
local x, y
|
||||||
|
|
||||||
|
-- Rotate the vector. Values of components range from 0 to 1, so adding 1 when inverting is necessary.
|
||||||
|
if rot == 1 then
|
||||||
|
x, y = 1 - tc.y, tc.x -- 90 degrees counterclockwise
|
||||||
|
elseif rot == 2 then
|
||||||
|
x, y = 1 - tc.x, 1 - tc.y -- 180 degrees counterclockwise
|
||||||
|
elseif rot == 3 then
|
||||||
|
x, y = tc.y, 1 - tc.x -- 270 degrees counterclockwise
|
||||||
|
end
|
||||||
|
|
||||||
|
texCoords[i] = {x = x, y = y}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function scale_global_texture_coordinates(texCoords, pos, sideIdx, scale)
|
||||||
-- Get the offset of the tile relative to the lower left corner of the texture.
|
-- Get the offset of the tile relative to the lower left corner of the texture.
|
||||||
local texPos = {}
|
local texPos = {}
|
||||||
|
|
||||||
@ -189,57 +235,77 @@ function meshport.scale_global_texture_coordinates(texCoords, pos, sideIdx, scal
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Scale and move the texture coordinates.
|
-- Scale and move the texture coordinates.
|
||||||
local newTexCoords = {}
|
for i, texCoord in ipairs(texCoords) do
|
||||||
|
texCoords[i] = {
|
||||||
for _, texCoord in ipairs(texCoords) do
|
|
||||||
table.insert(newTexCoords, {
|
|
||||||
x = (texCoord.x + texPos.x) / scale,
|
x = (texCoord.x + texPos.x) / scale,
|
||||||
y = (texCoord.y + texPos.y) / scale,
|
y = (texCoord.y + texPos.y) / scale,
|
||||||
})
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return newTexCoords
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- WARNING: This function mutates tables!
|
||||||
|
-- Please follow the table rules used by Faces.
|
||||||
function meshport.prepare_cuboid_face(face, tiles, pos, facedir, sideIdx)
|
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.
|
-- 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]
|
face.tile_idx = face.tile_idx or FACEDIR_TO_TILE_INDICES[facedir][sideIdx]
|
||||||
local tile = meshport.get_tile(tiles, face.tile_idx)
|
local tile = meshport.get_tile(tiles, face.tile_idx)
|
||||||
|
|
||||||
if tile.align_style == "world" or tile.align_style == "user" then
|
if tile.align_style == "world" or tile.align_style == "user" then
|
||||||
-- For scaled, world-aligned tiles, scale and reposition the texture coordinates as needed.
|
-- For scaled, world-aligned tiles, scale and reposition the texture coordinates as needed.
|
||||||
if tile.scale and tile.scale ~= 1 then
|
if tile.scale and tile.scale ~= 1 then
|
||||||
face.tex_coords = meshport.scale_global_texture_coordinates(face.tex_coords, pos, sideIdx, tile.scale)
|
scale_global_texture_coordinates(face.tex_coords, pos, sideIdx, tile.scale)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- If the tile isn't world-aligned, rotate it according to the facedir.
|
-- If the tile isn't world-aligned, rotate it according to the facedir.
|
||||||
face.tex_coords = meshport.rotate_texture_coordinates(face.tex_coords,
|
rotate_texture_coordinates(face.tex_coords, FACEDIR_TO_TILE_ROTATIONS[facedir][sideIdx])
|
||||||
meshport.facedir_to_tile_rotations[facedir][sideIdx])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return face
|
return face
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function meshport.get_content_id_or_nil(nodeName)
|
||||||
|
if minetest.registered_nodes[nodeName] then
|
||||||
|
return minetest.get_content_id(nodeName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.get_def_from_id(contentId)
|
function meshport.get_def_from_id(contentId)
|
||||||
return minetest.registered_nodes[minetest.get_name_from_content_id(contentId)] or {}
|
return minetest.registered_nodes[minetest.get_name_from_content_id(contentId)] or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.get_aliased_drawtype(drawtype)
|
function meshport.get_aliased_drawtype(drawtype)
|
||||||
return meshport.drawtype_aliases[drawtype or ""] or drawtype
|
return DRAWTYPE_ALIASES[drawtype or ""] or drawtype
|
||||||
end
|
end
|
||||||
|
|
||||||
function meshport.get_facedir(param2, type)
|
|
||||||
|
function meshport.get_facedir(type, param2)
|
||||||
if type == "facedir" or type == "colorfacedir" then
|
if type == "facedir" or type == "colorfacedir" then
|
||||||
-- For colorfacedir, only the first 5 bits are needed.
|
-- For colorfacedir, only the first 5 bits are needed.
|
||||||
return param2 % 32
|
return param2 % 32
|
||||||
elseif type == "wallmounted" or type == "colorwallmounted" then
|
elseif type == "wallmounted" or type == "colorwallmounted" then
|
||||||
-- For colorwallmounted, only the first 3 bits are needed. If the wallmounted direction is invalid, return 0.
|
-- 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
|
return WALLMOUNTED_TO_FACEDIR[param2 % 8] or 0
|
||||||
else
|
else
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function meshport.get_degrotate(type, param2)
|
||||||
|
if type == "degrotate" then
|
||||||
|
return 1.5 * (param2 % 240)
|
||||||
|
elseif type == "colordegrotate" then
|
||||||
|
return 15 * ((param2 % 32) % 24)
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.get_node_neighbors(array, area, idx)
|
function meshport.get_node_neighbors(array, area, idx)
|
||||||
-- Get the node's absolute position from the flat array index.
|
-- Get the node's absolute position from the flat array index.
|
||||||
local pos = area:position(idx)
|
local pos = area:position(idx)
|
||||||
@ -247,37 +313,65 @@ function meshport.get_node_neighbors(array, area, idx)
|
|||||||
|
|
||||||
-- Get the content/param2 value for each neighboring node.
|
-- Get the content/param2 value for each neighboring node.
|
||||||
for i = 1, 6 do
|
for i = 1, 6 do
|
||||||
neighbors[i] = array[area:indexp(vector.add(pos, meshport.neighbor_dirs[i]))]
|
neighbors[i] = array[area:indexp(vector.add(pos, meshport.NEIGHBOR_DIRS[i]))]
|
||||||
end
|
end
|
||||||
|
|
||||||
return neighbors
|
return neighbors
|
||||||
end
|
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)
|
function meshport.get_tile(tiles, n)
|
||||||
if type(tiles) == "table" then
|
if type(tiles) == "table" and #tiles > 0 then
|
||||||
return tiles[n] or tiles[#tiles]
|
return tiles[n] or tiles[#tiles]
|
||||||
else
|
else
|
||||||
return "unknown"
|
return "unknown"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function get_png_dimensions(path)
|
||||||
|
-- Luckily, reading the dimensions of a PNG file is a trivial task
|
||||||
|
local file = io.open(path, "rb")
|
||||||
|
if not file then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file:seek("set", 1)
|
||||||
|
if file:read(3) ~= "PNG" then -- Verify it's a PNG file
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file:seek("set", 16)
|
||||||
|
|
||||||
|
local function read_u32(b)
|
||||||
|
return (b:byte(1) * 0x1000000 +
|
||||||
|
b:byte(2) * 0x10000 +
|
||||||
|
b:byte(3) * 0x100 +
|
||||||
|
b:byte(4))
|
||||||
|
end
|
||||||
|
|
||||||
|
local w = read_u32(file:read(4))
|
||||||
|
local h = read_u32(file:read(4))
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
return w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- In case of failure, this should return nil, nil
|
||||||
|
function meshport.get_texture_dimensions(textureName)
|
||||||
|
local dims = meshport.texture_dimension_cache[textureName]
|
||||||
|
if dims then
|
||||||
|
return dims[1], dims[2]
|
||||||
|
end
|
||||||
|
|
||||||
|
local path = meshport.texture_paths[textureName]
|
||||||
|
if path then
|
||||||
|
local w, h = get_png_dimensions(path)
|
||||||
|
meshport.texture_dimension_cache[path] = {w, h} -- Will be an empty table if the file isn't found.
|
||||||
|
return w, h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
function meshport.get_asset_paths(assetFolderName, extension)
|
function meshport.get_asset_paths(assetFolderName, extension)
|
||||||
local modAssetPath
|
local modAssetPath
|
||||||
local assets = {}
|
local assets = {}
|
||||||
@ -288,7 +382,7 @@ function meshport.get_asset_paths(assetFolderName, extension)
|
|||||||
|
|
||||||
-- Iterate through all the files in the requested folder of the mod.
|
-- Iterate through all the files in the requested folder of the mod.
|
||||||
for _, fileName in ipairs(minetest.get_dir_list(modAssetPath, false)) do
|
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.
|
-- Add files to the table. If an extension is specified, only add files with that extension.
|
||||||
if not extension or string.lower(string.sub(fileName, -string.len(extension))) == extension then
|
if not extension or string.lower(string.sub(fileName, -string.len(extension))) == extension then
|
||||||
assets[fileName] = modAssetPath .. DIR_DELIM .. fileName
|
assets[fileName] = modAssetPath .. DIR_DELIM .. fileName
|
||||||
end
|
end
|
Loading…
x
Reference in New Issue
Block a user