File: parse_obj.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 (131 lines) | stat: -rw-r--r-- 4,322 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
--[[
	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/>.
]]

-- 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))

		if elementType == "v" then
			return "verts", vec
		else
			return "vert_norms", vec
		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


local function parse_face_element(elements, faceStr)
	-- Split the face element into strings containing the indices of elements associated with each vertex.
	local vertStrs = string.split(faceStr, " ")

	local face = {
		verts = {},
		tex_coords = {},
		vert_norms = {},
	}

	for i, vertStr in ipairs(vertStrs) do
		-- Split the string into indices for vertex, texture coordinate, and/or vertex normal elements.
		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")

		-- Set the position, texture coordinate, and vertex normal of the vertex.
		-- Note that vti or vni are allowed to be nil
		face.verts[i] = elements.verts[vi]
		face.tex_coords[i] = elements.tex_coords[vti]
		face.vert_norms[i] = elements.vert_norms[vni]
	end

	return face
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)
	local file = io.open(path, "r")

	local faces = meshport.Faces:new()
	local elements = {
		verts = {},
		tex_coords = {},
		vert_norms = {},
	}

	-- Tiles are assigned according to groups, in the order in which groups are defined.
	local groups = {}
	local currentTileIdx

	for line in file:lines() do
		-- elementStr may be an empty string, e.g. "g" with no group name.
		local elementType, elementStr = string.match(line, "^(%a+)%s*(.*)")

		if elementType == "v" or elementType == "vt" or elementType == "vn" then
			local dest, value = parse_vector_element(elementType, elementStr)
			table.insert(elements[dest], value)
		elseif elementType == "f" then
			-- If the face is not part of any group, use the placeholder group `0`.
			if not currentTileIdx then
				table.insert(groups, 0)
				currentTileIdx = #groups
			end

			-- Parse the face element.
			local face = parse_face_element(elements, elementStr)
			-- Assign materials according to the group.
			face.tile_idx = currentTileIdx
			faces:insert_face(face)
		elseif elementType == "g" then
			currentTileIdx = handle_group(groups, elementStr)
		end
	end

	return faces
end