assimp-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class MaterialCopier
5
+ PROPERTY_FLOAT = 0x1
6
+ PROPERTY_DOUBLE = 0x2
7
+ PROPERTY_STRING = 0x3
8
+ PROPERTY_INTEGER = 0x4
9
+ PROPERTY_BUFFER = 0x5
10
+
11
+ TEXTURE_TYPES = {
12
+ diffuse: 1, specular: 2, ambient: 3, emissive: 4, height: 5, normals: 6,
13
+ shininess: 7, opacity: 8, displacement: 9, lightmap: 10, reflection: 11,
14
+ base_color: 12, normal_camera: 13, emission_color: 14, metalness: 15,
15
+ roughness: 16, ao: 17, unknown: 18, sheen: 19, clearcoat: 20, transmission: 21
16
+ }.freeze
17
+ WRAP_MODES = { 0 => :wrap, 1 => :clamp, 2 => :mirror, 3 => :decal }.freeze
18
+
19
+ def initialize(native: Native, embedded_textures: [])
20
+ @native = native
21
+ @embedded_texture_indices = embedded_texture_indices(embedded_textures)
22
+ end
23
+
24
+ def copy(pointer)
25
+ properties = copy_properties(pointer)
26
+ textures = copy_textures(pointer)
27
+ shininess = scalar(properties, "$mat.shininess")
28
+ Material.new(
29
+ name: properties.fetch(["?mat.name", 0, 0], ""),
30
+ base_color: color(properties, "$clr.base") || color(properties, "$clr.diffuse") || [1.0, 1.0, 1.0, 1.0],
31
+ metallic: scalar(properties, "$mat.metallicFactor") || texture_default(textures, :metalness, 0.0),
32
+ roughness: scalar(properties, "$mat.roughnessFactor") ||
33
+ texture_default(textures, :roughness, roughness_from(shininess)),
34
+ shininess:,
35
+ opacity: scalar(properties, "$mat.opacity") || 1.0,
36
+ two_sided: (scalar(properties, "$mat.twosided") || 0).to_i != 0,
37
+ textures:,
38
+ properties:
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ def copy_properties(pointer)
45
+ material = Native::Material.new(pointer)
46
+ NativeReader.pointers(material[:properties], material[:num_properties]).each_with_object({}) do |property_pointer, result|
47
+ property = Native::MaterialProperty.new(property_pointer)
48
+ key = [property[:key].to_s.freeze, property[:semantic], property[:index]].freeze
49
+ result[key] = decode_property(property)
50
+ end.freeze
51
+ end
52
+
53
+ def decode_property(property)
54
+ pointer = property[:data]
55
+ length = property[:data_length]
56
+ case property[:type]
57
+ when PROPERTY_FLOAT then collapse(pointer.read_array_of_float(length / FFI.type_size(:float)))
58
+ when PROPERTY_DOUBLE then collapse(pointer.read_array_of_double(length / FFI.type_size(:double)))
59
+ when PROPERTY_STRING then Native::StringValue.new(pointer).to_s.freeze
60
+ when PROPERTY_INTEGER then collapse(pointer.read_array_of_int(length / FFI.type_size(:int)))
61
+ when PROPERTY_BUFFER then NativeReader.bytes(pointer, length)
62
+ else NativeReader.bytes(pointer, length)
63
+ end
64
+ end
65
+
66
+ def collapse(values)
67
+ return values.first if values.one?
68
+
69
+ values.freeze
70
+ end
71
+
72
+ def scalar(properties, key)
73
+ value = properties[[key, 0, 0]]
74
+ value.is_a?(Array) ? value.first : value
75
+ end
76
+
77
+ def color(properties, key)
78
+ value = properties[[key, 0, 0]]
79
+ return unless value
80
+
81
+ values = Array(value).map(&:to_f)
82
+ values << 1.0 if values.length == 3
83
+ values.first(4).freeze
84
+ end
85
+
86
+ def texture_default(textures, type, fallback)
87
+ textures.key?([type, 0]) ? 1.0 : fallback
88
+ end
89
+
90
+ def roughness_from(shininess)
91
+ return 1.0 unless shininess
92
+
93
+ Math.sqrt(2.0 / (shininess.to_f + 2.0)).clamp(0.0, 1.0)
94
+ end
95
+
96
+ def copy_textures(material_pointer)
97
+ TEXTURE_TYPES.each_with_object({}) do |(name, type), result|
98
+ @native.aiGetMaterialTextureCount(material_pointer, type).times do |index|
99
+ reference = copy_texture(material_pointer, type, index)
100
+ result[[name, index].freeze] = reference if reference
101
+ end
102
+ end.freeze
103
+ end
104
+
105
+ def copy_texture(material, type, index)
106
+ path = Native::StringValue.new
107
+ mapping = FFI::MemoryPointer.new(:int)
108
+ uv_index = FFI::MemoryPointer.new(:uint)
109
+ blend = FFI::MemoryPointer.new(:float)
110
+ operation = FFI::MemoryPointer.new(:int)
111
+ modes = FFI::MemoryPointer.new(:int, 2)
112
+ flags = FFI::MemoryPointer.new(:uint)
113
+ status = @native.aiGetMaterialTexture(
114
+ material, type, index, path, mapping, uv_index, blend, operation, modes, flags
115
+ )
116
+ return unless status.zero?
117
+
118
+ texture_path = path.to_s.freeze
119
+ TextureRef.new(
120
+ path: texture_path,
121
+ embedded_index: embedded_index(texture_path),
122
+ uv_index: uv_index.read_uint,
123
+ wrap: modes.read_array_of_int(2).map { |mode| WRAP_MODES.fetch(mode, :unknown) }.freeze
124
+ )
125
+ end
126
+
127
+ def embedded_texture_indices(textures)
128
+ textures.each_with_index.each_with_object({}) do |(texture, index), result|
129
+ next if texture.filename.empty?
130
+
131
+ normalized = texture.filename.tr("\\", "/")
132
+ result[texture.filename] = index
133
+ result[normalized] = index
134
+ result[File.basename(normalized)] = index
135
+ end.freeze
136
+ end
137
+
138
+ def embedded_index(path)
139
+ direct = /\A\*(\d+)\z/.match(path)
140
+ return Integer(direct[1]) if direct
141
+
142
+ normalized = path.tr("\\", "/")
143
+ @embedded_texture_indices[path] ||
144
+ @embedded_texture_indices[normalized] ||
145
+ @embedded_texture_indices[File.basename(normalized)]
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ Vec3 = Data.define(:x, :y, :z) do
5
+ def to_a
6
+ [x, y, z]
7
+ end
8
+ end
9
+
10
+ Quat = Data.define(:x, :y, :z, :w) do
11
+ def to_a
12
+ [x, y, z, w]
13
+ end
14
+ end
15
+
16
+ AABB = Data.define(:min, :max)
17
+
18
+ module Matrix
19
+ IDENTITY = [
20
+ 1.0, 0.0, 0.0, 0.0,
21
+ 0.0, 1.0, 0.0, 0.0,
22
+ 0.0, 0.0, 1.0, 0.0,
23
+ 0.0, 0.0, 0.0, 1.0
24
+ ].freeze
25
+
26
+ module_function
27
+
28
+ def multiply(left, right)
29
+ Array.new(16) do |offset|
30
+ column = offset / 4
31
+ row = offset % 4
32
+ 4.times.sum { |index| left[(index * 4) + row] * right[(column * 4) + index] }
33
+ end.freeze
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ Bone = Data.define(:name, :offset_matrix, :weights)
5
+
6
+ class Mesh
7
+ attr_reader :name, :material_index, :primitive, :positions, :normals, :tangents,
8
+ :uv_sets, :colors, :indices, :bones, :vertex_count, :aabb
9
+
10
+ def initialize(name:, material_index:, primitive:, positions:, normals:, tangents:, uv_sets:,
11
+ colors:, indices:, bones:, vertex_count:, aabb:)
12
+ @name = String(name).dup.freeze
13
+ @material_index = Integer(material_index)
14
+ @primitive = primitive.to_sym
15
+ @vertex_count = Integer(vertex_count)
16
+ @positions = packed(positions, @vertex_count * 12, :positions)
17
+ @normals = optional_packed(normals, @vertex_count * 12, :normals)
18
+ @tangents = optional_packed(tangents, @vertex_count * 16, :tangents)
19
+ @uv_sets = uv_sets.map.with_index { |data, index| packed(data, @vertex_count * 8, :"uv#{index}") }.freeze
20
+ @colors = optional_packed(colors, @vertex_count * 16, :colors)
21
+ @indices = packed_multiple(indices, 4, :indices)
22
+ @bones = bones.freeze
23
+ @aabb = aabb
24
+ freeze
25
+ end
26
+
27
+ def index_count
28
+ indices.bytesize / 4
29
+ end
30
+
31
+ private
32
+
33
+ def optional_packed(value, expected_size, field)
34
+ value && packed(value, expected_size, field)
35
+ end
36
+
37
+ def packed(value, expected_size, field)
38
+ data = String(value).dup.force_encoding(Encoding::BINARY).freeze
39
+ return data if data.bytesize == expected_size
40
+
41
+ raise ArgumentError, "#{field} has #{data.bytesize} bytes, expected #{expected_size}"
42
+ end
43
+
44
+ def packed_multiple(value, component_size, field)
45
+ data = String(value).dup.force_encoding(Encoding::BINARY).freeze
46
+ return data if (data.bytesize % component_size).zero?
47
+
48
+ raise ArgumentError, "#{field} byte size must be a multiple of #{component_size}"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Assimp
4
+ class MeshCopier
5
+ VECTOR_SIZE = Native::Vector3D.size
6
+ UV_BATCH_SIZE = 1024
7
+ PRIMITIVES = {
8
+ 0x1 => :points,
9
+ 0x2 => :lines,
10
+ 0x4 => :triangles,
11
+ 0x8 => :polygons
12
+ }.freeze
13
+
14
+ def initialize(native: Native)
15
+ @bone_type = native.version[1].zero? ? Native::LegacyBone : Native::Bone
16
+ end
17
+
18
+ def copy(pointer)
19
+ source = Native::Mesh.new(pointer)
20
+ vertex_count = source[:num_vertices]
21
+ positions = NativeReader.bytes(source[:vertices], VECTOR_SIZE * vertex_count)
22
+ normals = optional_vectors(source[:normals], vertex_count)
23
+
24
+ Mesh.new(
25
+ name: source[:name].to_s,
26
+ material_index: source[:material_index],
27
+ primitive: primitive(source[:primitive_types]),
28
+ positions:,
29
+ normals:,
30
+ tangents: copy_tangents(source, normals, vertex_count),
31
+ uv_sets: copy_uv_sets(source, vertex_count),
32
+ colors: optional_colors(source[:colors][0], vertex_count),
33
+ indices: copy_indices(source, vertex_count),
34
+ bones: copy_bones(source),
35
+ vertex_count:,
36
+ aabb: bounds(positions, vertex_count)
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def optional_vectors(pointer, count)
43
+ return nil if pointer.null?
44
+
45
+ NativeReader.bytes(pointer, VECTOR_SIZE * count)
46
+ end
47
+
48
+ def optional_colors(pointer, count)
49
+ return nil if pointer.null?
50
+
51
+ NativeReader.bytes(pointer, Native::Color4D.size * count)
52
+ end
53
+
54
+ def primitive(flags)
55
+ geometry_flags = flags & 0x0f
56
+ return PRIMITIVES.fetch(geometry_flags) if PRIMITIVES.key?(geometry_flags)
57
+ return :unknown if geometry_flags.zero?
58
+
59
+ :mixed
60
+ end
61
+
62
+ def copy_uv_sets(source, vertex_count)
63
+ (0...8).filter_map do |set_index|
64
+ pointer = source[:texture_coords][set_index]
65
+ next if pointer.null?
66
+
67
+ extract_uv(pointer, vertex_count)
68
+ end
69
+ end
70
+
71
+ def extract_uv(pointer, vertex_count)
72
+ output = String.new(capacity: vertex_count * 8, encoding: Encoding::BINARY)
73
+ (0...vertex_count).step(UV_BATCH_SIZE) do |start|
74
+ count = [UV_BATCH_SIZE, vertex_count - start].min
75
+ values = pointer.get_array_of_float(start * VECTOR_SIZE, count * 3)
76
+ output << values.each_slice(3).flat_map { |x, y, _z| [x, y] }.pack("e*")
77
+ end
78
+ output.freeze
79
+ end
80
+
81
+ def copy_tangents(source, normals, vertex_count)
82
+ tangents = source[:tangents]
83
+ bitangents = source[:bitangents]
84
+ return nil if tangents.null? || bitangents.null?
85
+
86
+ normal_values = normals&.unpack("e*")
87
+ tangent_values = tangents.read_array_of_float(vertex_count * 3)
88
+ bitangent_values = bitangents.read_array_of_float(vertex_count * 3)
89
+ values = vertex_count.times.flat_map do |index|
90
+ offset = index * 3
91
+ tangent = tangent_values.slice(offset, 3)
92
+ normal = normal_values&.slice(offset, 3)
93
+ bitangent = bitangent_values.slice(offset, 3)
94
+ tangent << handedness(normal, tangent, bitangent)
95
+ end
96
+ values.pack("e*").freeze
97
+ end
98
+
99
+ def handedness(normal, tangent, bitangent)
100
+ return 1.0 unless normal
101
+
102
+ cross_x = (normal[1] * tangent[2]) - (normal[2] * tangent[1])
103
+ cross_y = (normal[2] * tangent[0]) - (normal[0] * tangent[2])
104
+ cross_z = (normal[0] * tangent[1]) - (normal[1] * tangent[0])
105
+ dot = (cross_x * bitangent[0]) + (cross_y * bitangent[1]) + (cross_z * bitangent[2])
106
+ dot.negative? ? -1.0 : 1.0
107
+ end
108
+
109
+ def copy_indices(source, vertex_count)
110
+ values = source[:num_faces].times.flat_map do |face_index|
111
+ face = NativeReader.struct_at(source[:faces], Native::Face, face_index)
112
+ indices = face[:indices].read_array_of_uint(face[:num_indices])
113
+ invalid = indices.find { |index| index >= vertex_count }
114
+ raise ImportError, "mesh face references vertex #{invalid}, but has #{vertex_count} vertices" if invalid
115
+
116
+ indices
117
+ end
118
+ values.pack("L<*").freeze
119
+ end
120
+
121
+ def copy_bones(source)
122
+ NativeReader.pointers(source[:bones], source[:num_bones]).map do |pointer|
123
+ bone = @bone_type.new(pointer)
124
+ weights = bone[:num_weights].times.map do |index|
125
+ weight = NativeReader.struct_at(bone[:weights], Native::VertexWeight, index)
126
+ [weight[:vertex_id], weight[:weight]].freeze
127
+ end.freeze
128
+ Bone.new(
129
+ name: bone[:name].to_s.freeze,
130
+ offset_matrix: bone[:offset_matrix].to_column_major.freeze,
131
+ weights:
132
+ )
133
+ end.freeze
134
+ end
135
+
136
+ def bounds(positions, vertex_count)
137
+ return AABB.new(min: Vec3.new(0.0, 0.0, 0.0), max: Vec3.new(0.0, 0.0, 0.0)) if vertex_count.zero?
138
+
139
+ minimum = [Float::INFINITY, Float::INFINITY, Float::INFINITY]
140
+ maximum = [-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]
141
+ positions.unpack("e*").each_slice(3) do |values|
142
+ 3.times do |axis|
143
+ minimum[axis] = values[axis] if values[axis] < minimum[axis]
144
+ maximum[axis] = values[axis] if values[axis] > maximum[axis]
145
+ end
146
+ end
147
+ AABB.new(min: Vec3.new(*minimum), max: Vec3.new(*maximum))
148
+ end
149
+ end
150
+ end