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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/Rakefile +66 -0
- data/generator/generate.rb +297 -0
- data/generator/layout_probe.c +288 -0
- data/generator/verify_layout.rb +33 -0
- data/lib/assimp/animation.rb +27 -0
- data/lib/assimp/animation_copier.rb +50 -0
- data/lib/assimp/embedded_texture.rb +23 -0
- data/lib/assimp/errors.rb +9 -0
- data/lib/assimp/import_source.rb +54 -0
- data/lib/assimp/importer.rb +54 -0
- data/lib/assimp/material.rb +73 -0
- data/lib/assimp/material_copier.rb +148 -0
- data/lib/assimp/math.rb +36 -0
- data/lib/assimp/mesh.rb +51 -0
- data/lib/assimp/mesh_copier.rb +150 -0
- data/lib/assimp/native/generated.rb +475 -0
- data/lib/assimp/native/platform.rb +67 -0
- data/lib/assimp/native/types.rb +60 -0
- data/lib/assimp/native.rb +148 -0
- data/lib/assimp/native_reader.rb +25 -0
- data/lib/assimp/node.rb +29 -0
- data/lib/assimp/post_process.rb +35 -0
- data/lib/assimp/raw_scene.rb +97 -0
- data/lib/assimp/scene.rb +42 -0
- data/lib/assimp/scene_copier.rb +46 -0
- data/lib/assimp/texture_copier.rb +17 -0
- data/lib/assimp/version.rb +5 -0
- data/lib/assimp.rb +46 -0
- data/rakelib/native_package.rake +187 -0
- data/script/smoke_gem.rb +25 -0
- data/script/verify_release.rb +33 -0
- metadata +90 -0
|
@@ -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
|
data/lib/assimp/math.rb
ADDED
|
@@ -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
|
data/lib/assimp/mesh.rb
ADDED
|
@@ -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
|