File: mesh.lua

package info (click to toggle)
minetest-mod-meshport 0.2.3-1
  • links: PTS
  • area: main
  • in suites: sid, trixie
  • size: 776 kB
  • sloc: python: 40; makefile: 2
file content (340 lines) | stat: -rw-r--r-- 9,225 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
--[[
	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 S = meshport.S

--[[
	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", S("Ignoring texture modifers in material \"@1\".", mat))
			end

			matFile:write(string.format("map_Kd %s\n", meshport.texture_paths[texName]))
		else
			meshport.log(playerName, "warning",
					S("Could not find texture \"@1\". 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