rgltf 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 +270 -0
- data/Rakefile +10 -0
- data/benchmark/accessors.rb +26 -0
- data/benchmark/compare.rb +28 -0
- data/benchmark/sample_assets.rb +61 -0
- data/examples/README.md +23 -0
- data/examples/build_triangle.rb +40 -0
- data/examples/convert.rb +24 -0
- data/examples/inspect.rb +27 -0
- data/lib/rgltf/accessor_reader.rb +142 -0
- data/lib/rgltf/buffer_resolver.rb +95 -0
- data/lib/rgltf/builder/accessor_data.rb +40 -0
- data/lib/rgltf/builder/accessors.rb +130 -0
- data/lib/rgltf/builder/component_builders.rb +89 -0
- data/lib/rgltf/builder/document_properties.rb +32 -0
- data/lib/rgltf/builder/materials.rb +132 -0
- data/lib/rgltf/builder.rb +176 -0
- data/lib/rgltf/document.rb +159 -0
- data/lib/rgltf/errors.rb +10 -0
- data/lib/rgltf/extension.rb +79 -0
- data/lib/rgltf/extensions/khr_lights_punctual.rb +101 -0
- data/lib/rgltf/extensions/khr_materials_emissive_strength.rb +29 -0
- data/lib/rgltf/extensions/khr_materials_unlit.rb +19 -0
- data/lib/rgltf/extensions/khr_texture_transform.rb +42 -0
- data/lib/rgltf/glb.rb +71 -0
- data/lib/rgltf/properties/accessor.rb +142 -0
- data/lib/rgltf/properties/animation.rb +96 -0
- data/lib/rgltf/properties/asset.rb +15 -0
- data/lib/rgltf/properties/base.rb +45 -0
- data/lib/rgltf/properties/buffer.rb +50 -0
- data/lib/rgltf/properties/buffer_view.rb +18 -0
- data/lib/rgltf/properties/camera.rb +50 -0
- data/lib/rgltf/properties/image.rb +48 -0
- data/lib/rgltf/properties/material.rb +95 -0
- data/lib/rgltf/properties/mesh.rb +67 -0
- data/lib/rgltf/properties/node.rb +135 -0
- data/lib/rgltf/properties/sampler.rb +35 -0
- data/lib/rgltf/properties/scene.rb +14 -0
- data/lib/rgltf/properties/skin.rb +25 -0
- data/lib/rgltf/properties/texture.rb +19 -0
- data/lib/rgltf/properties.rb +17 -0
- data/lib/rgltf/validation/accessors.rb +205 -0
- data/lib/rgltf/validation/animations.rb +184 -0
- data/lib/rgltf/validation/buffers.rb +95 -0
- data/lib/rgltf/validation/cameras.rb +76 -0
- data/lib/rgltf/validation/context.rb +133 -0
- data/lib/rgltf/validation/images_materials.rb +159 -0
- data/lib/rgltf/validation/meshes.rb +214 -0
- data/lib/rgltf/validation/root_asset_extensions.rb +134 -0
- data/lib/rgltf/validation/scenes_nodes.rb +141 -0
- data/lib/rgltf/validation/skins.rb +94 -0
- data/lib/rgltf/validator.rb +53 -0
- data/lib/rgltf/version.rb +5 -0
- data/lib/rgltf/writer/buffer_merger.rb +72 -0
- data/lib/rgltf/writer/default_omitter.rb +51 -0
- data/lib/rgltf/writer/extension_serializer.rb +64 -0
- data/lib/rgltf/writer.rb +95 -0
- data/lib/rgltf.rb +133 -0
- data/tasks/sample_assets.rake +115 -0
- metadata +104 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Accessor < Properties::Base
|
|
5
|
+
COMPONENT_TYPES = AccessorReader::COMPONENT_TYPES
|
|
6
|
+
ELEMENT_COUNTS = AccessorReader::ELEMENT_COUNTS
|
|
7
|
+
|
|
8
|
+
attr_reader :buffer_view, :byte_offset, :component_type, :normalized,
|
|
9
|
+
:count, :type, :min, :max, :sparse
|
|
10
|
+
|
|
11
|
+
def initialize(json, document:, index:)
|
|
12
|
+
super(json, document:, index:, owner_type: :accessor)
|
|
13
|
+
@buffer_view = optional_reference(document, :buffer_views, json['bufferView'], "accessors/#{index}/bufferView")
|
|
14
|
+
@byte_offset = json.fetch('byteOffset', 0)
|
|
15
|
+
@component_type = COMPONENT_TYPES.fetch(json.fetch('componentType'))[:sym]
|
|
16
|
+
@normalized = json.fetch('normalized', false)
|
|
17
|
+
@count = json.fetch('count')
|
|
18
|
+
@type = json.fetch('type')
|
|
19
|
+
ELEMENT_COUNTS.fetch(@type)
|
|
20
|
+
@min = json['min']
|
|
21
|
+
@max = json['max']
|
|
22
|
+
@sparse = json['sparse'] && Sparse.new(json['sparse'], document:, accessor_index: index)
|
|
23
|
+
rescue KeyError => e
|
|
24
|
+
raise FormatError, "invalid accessor #{index}: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def packed
|
|
28
|
+
@packed ||= AccessorReader.packed(self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def vertex_format
|
|
32
|
+
component_count = ELEMENT_COUNTS.fetch(type)
|
|
33
|
+
return nil unless component_count.between?(1, 4)
|
|
34
|
+
|
|
35
|
+
prefix = if normalized
|
|
36
|
+
{ i8: 'snorm8', u8: 'unorm8', i16: 'snorm16', u16: 'unorm16' }[component_type]
|
|
37
|
+
else
|
|
38
|
+
{ i8: 'sint8', u8: 'uint8', i16: 'sint16', u16: 'uint16', u32: 'uint32',
|
|
39
|
+
f32: 'float32' }[component_type]
|
|
40
|
+
end
|
|
41
|
+
return nil unless prefix
|
|
42
|
+
|
|
43
|
+
return nil if %i[i8 u8 i16 u16].include?(component_type) && ![2, 4].include?(component_count)
|
|
44
|
+
|
|
45
|
+
component_count == 1 ? prefix.to_sym : :"#{prefix}x#{component_count}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_a
|
|
49
|
+
AccessorReader.unpack(self, packed)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def each_element
|
|
53
|
+
return enum_for(:each_element) unless block_given?
|
|
54
|
+
|
|
55
|
+
to_a.each do |element|
|
|
56
|
+
element.is_a?(Array) ? yield(*element) : yield(element)
|
|
57
|
+
end
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def elements
|
|
62
|
+
enum_for(:each_element)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def packed_as_u32
|
|
66
|
+
unless type == 'SCALAR' && %i[u8 u16 u32].include?(component_type)
|
|
67
|
+
raise UnsupportedError, 'packed_as_u32 requires a SCALAR unsigned integer accessor'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@packed_as_u32 ||= begin
|
|
71
|
+
directive = AccessorReader::COMPONENTS_BY_SYMBOL.fetch(component_type).fetch(:pack)
|
|
72
|
+
packed.unpack("#{directive}*").pack('L<*').freeze
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def release!
|
|
77
|
+
@packed = nil
|
|
78
|
+
@packed_as_u32 = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def nested_properties
|
|
82
|
+
sparse ? [sparse] : []
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def optional_reference(document, collection, value, path)
|
|
88
|
+
value.nil? ? nil : document.fetch_reference(collection, value, path)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Accessor
|
|
93
|
+
class Sparse < Properties::Base
|
|
94
|
+
attr_reader :count, :indices, :values
|
|
95
|
+
|
|
96
|
+
def initialize(json, document:, accessor_index:)
|
|
97
|
+
super(json, document:, owner_type: :accessor_sparse)
|
|
98
|
+
@count = json.fetch('count')
|
|
99
|
+
@indices = Indices.new(json.fetch('indices'), document:, accessor_index:)
|
|
100
|
+
@values = Values.new(json.fetch('values'), document:, accessor_index:)
|
|
101
|
+
rescue KeyError => e
|
|
102
|
+
raise FormatError, "invalid sparse accessor #{accessor_index}: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def nested_properties
|
|
106
|
+
[indices, values]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Indices < Properties::Base
|
|
110
|
+
attr_reader :buffer_view, :byte_offset, :component_type
|
|
111
|
+
|
|
112
|
+
def initialize(json, document:, accessor_index:)
|
|
113
|
+
super(json, document:, owner_type: :accessor_sparse_indices)
|
|
114
|
+
@buffer_view = document.fetch_reference(
|
|
115
|
+
:buffer_views,
|
|
116
|
+
json.fetch('bufferView'),
|
|
117
|
+
"accessors/#{accessor_index}/sparse/indices/bufferView"
|
|
118
|
+
)
|
|
119
|
+
@byte_offset = json.fetch('byteOffset', 0)
|
|
120
|
+
type = AccessorReader::COMPONENT_TYPES.fetch(json.fetch('componentType'))[:sym]
|
|
121
|
+
raise FormatError, 'sparse indices must be unsigned integers' unless %i[u8 u16 u32].include?(type)
|
|
122
|
+
|
|
123
|
+
@component_type = type
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Values < Properties::Base
|
|
128
|
+
attr_reader :buffer_view, :byte_offset
|
|
129
|
+
|
|
130
|
+
def initialize(json, document:, accessor_index:)
|
|
131
|
+
super(json, document:, owner_type: :accessor_sparse_values)
|
|
132
|
+
@buffer_view = document.fetch_reference(
|
|
133
|
+
:buffer_views,
|
|
134
|
+
json.fetch('bufferView'),
|
|
135
|
+
"accessors/#{accessor_index}/sparse/values/bufferView"
|
|
136
|
+
)
|
|
137
|
+
@byte_offset = json.fetch('byteOffset', 0)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Animation < Properties::Base
|
|
5
|
+
attr_reader :channels, :samplers
|
|
6
|
+
|
|
7
|
+
def initialize(json, document:, index:)
|
|
8
|
+
super(json, document:, index:, owner_type: :animation)
|
|
9
|
+
@samplers = json.fetch('samplers').each_with_index.map do |sampler, sampler_index|
|
|
10
|
+
AnimationSampler.new(sampler, document:, animation_index: index, index: sampler_index)
|
|
11
|
+
end.freeze
|
|
12
|
+
@channels = json.fetch('channels').each_with_index.map do |channel, channel_index|
|
|
13
|
+
Channel.new(channel, document:, animation: self, animation_index: index, index: channel_index)
|
|
14
|
+
end.freeze
|
|
15
|
+
rescue KeyError => e
|
|
16
|
+
raise FormatError, "invalid animation #{index}: #{e.message}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def nested_properties
|
|
20
|
+
[*channels, *samplers]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Channel < Properties::Base
|
|
24
|
+
TARGET_PATHS = %w[translation rotation scale weights].freeze
|
|
25
|
+
attr_reader :sampler, :target
|
|
26
|
+
|
|
27
|
+
def initialize(json, document:, animation:, animation_index:, index:)
|
|
28
|
+
super(json, document:, index:, owner_type: :animation_channel)
|
|
29
|
+
@sampler = animation.samplers.fetch(json.fetch('sampler'))
|
|
30
|
+
@target = Target.new(json.fetch('target'), document:, animation_index:, channel_index: index)
|
|
31
|
+
rescue IndexError, KeyError => e
|
|
32
|
+
raise FormatError, "invalid animation channel: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def target_node
|
|
36
|
+
target.node
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def target_path
|
|
40
|
+
target.path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def nested_properties
|
|
44
|
+
[target]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Target < Properties::Base
|
|
48
|
+
attr_reader :node, :path
|
|
49
|
+
|
|
50
|
+
def initialize(json, document:, animation_index:, channel_index:)
|
|
51
|
+
super(json, document:, owner_type: :animation_target)
|
|
52
|
+
@node = optional_node(json['node'], document, animation_index, channel_index)
|
|
53
|
+
@path = json.fetch('path').to_sym
|
|
54
|
+
return if Channel::TARGET_PATHS.include?(json['path']) && node
|
|
55
|
+
return if animation_pointer?(json)
|
|
56
|
+
|
|
57
|
+
raise FormatError, "invalid animation target #{json.inspect}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def optional_node(value, document, animation_index, channel_index)
|
|
63
|
+
return if value.nil?
|
|
64
|
+
|
|
65
|
+
document.fetch_reference(
|
|
66
|
+
:nodes, value, "animations/#{animation_index}/channels/#{channel_index}/target/node"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def animation_pointer?(json)
|
|
71
|
+
json['path'] == 'pointer' && json.dig('extensions', 'KHR_animation_pointer').is_a?(Hash)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class AnimationSampler < Properties::Base
|
|
77
|
+
INTERPOLATIONS = { 'LINEAR' => :linear, 'STEP' => :step, 'CUBICSPLINE' => :cubicspline }.freeze
|
|
78
|
+
attr_reader :input, :output, :interpolation
|
|
79
|
+
|
|
80
|
+
def initialize(json, document:, animation_index:, index:)
|
|
81
|
+
super(json, document:, index:, owner_type: :animation_sampler)
|
|
82
|
+
@input = document.fetch_reference(:accessors, json.fetch('input'),
|
|
83
|
+
"animations/#{animation_index}/samplers/#{index}/input")
|
|
84
|
+
@output = document.fetch_reference(:accessors, json.fetch('output'),
|
|
85
|
+
"animations/#{animation_index}/samplers/#{index}/output")
|
|
86
|
+
@interpolation = INTERPOLATIONS.fetch(json.fetch('interpolation', 'LINEAR'))
|
|
87
|
+
rescue KeyError => e
|
|
88
|
+
raise FormatError, "invalid animation sampler: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def duration
|
|
92
|
+
input.max&.first || 0.0
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Asset < Properties::Base
|
|
5
|
+
attr_reader :copyright, :generator, :version, :min_version
|
|
6
|
+
|
|
7
|
+
def initialize(json, document:)
|
|
8
|
+
super(json, document:, owner_type: :asset)
|
|
9
|
+
@copyright = json['copyright']
|
|
10
|
+
@generator = json['generator']
|
|
11
|
+
@version = json['version']
|
|
12
|
+
@min_version = json['minVersion']
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
module Properties
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :name, :extensions, :extras, :index, :owner_type, :source_json
|
|
7
|
+
|
|
8
|
+
def initialize(json, document:, index: nil, owner_type: nil)
|
|
9
|
+
@document = document
|
|
10
|
+
@source_json = json
|
|
11
|
+
@index = index
|
|
12
|
+
@name = json['name']
|
|
13
|
+
@extras = json['extras']
|
|
14
|
+
@raw_extensions = json.fetch('extensions', {})
|
|
15
|
+
@extensions = {}
|
|
16
|
+
@owner_type = owner_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extension(name)
|
|
20
|
+
extensions[name]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_extensions!
|
|
24
|
+
return self if @extensions_parsed
|
|
25
|
+
|
|
26
|
+
@extensions = @raw_extensions.each_with_object({}) do |(name, value), parsed|
|
|
27
|
+
parsed[name] = @document.extension_registry.parse(
|
|
28
|
+
name,
|
|
29
|
+
value,
|
|
30
|
+
on: @owner_type,
|
|
31
|
+
owner: self,
|
|
32
|
+
doc: @document
|
|
33
|
+
)
|
|
34
|
+
end.freeze
|
|
35
|
+
@extensions_parsed = true
|
|
36
|
+
nested_properties.each(&:parse_extensions!)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def nested_properties
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Buffer < Properties::Base
|
|
5
|
+
MESHOPT_EXTENSIONS = %w[EXT_meshopt_compression KHR_meshopt_compression].freeze
|
|
6
|
+
private_constant :MESHOPT_EXTENSIONS
|
|
7
|
+
|
|
8
|
+
attr_reader :uri, :byte_length
|
|
9
|
+
|
|
10
|
+
def initialize(json, document:, index:)
|
|
11
|
+
super(json, document:, index:, owner_type: :buffer)
|
|
12
|
+
@uri = json['uri']
|
|
13
|
+
@byte_length = json.fetch('byteLength')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def bytes
|
|
17
|
+
@document.buffer_resolver.resolve(self)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def meshopt_decode_target?
|
|
21
|
+
return false if uri || !index&.positive?
|
|
22
|
+
return false if @document.buffer_resolver.send(:supplied?, index)
|
|
23
|
+
|
|
24
|
+
meshopt_fallback? || referenced_as_meshopt_target?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def meshopt_fallback?
|
|
28
|
+
required_meshopt_extensions.any? do |name|
|
|
29
|
+
source_json.dig('extensions', name, 'fallback') == true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def referenced_as_meshopt_target?
|
|
34
|
+
@document.raw_json.fetch('bufferViews', []).any? do |view|
|
|
35
|
+
next false unless view.is_a?(Hash) && view['buffer'] == index
|
|
36
|
+
|
|
37
|
+
extensions = view['extensions']
|
|
38
|
+
extensions.is_a?(Hash) && required_meshopt_extensions.any? { |name| extensions[name].is_a?(Hash) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def required_meshopt_extensions
|
|
43
|
+
required = @document.extensions_required
|
|
44
|
+
required.is_a?(Array) ? MESHOPT_EXTENSIONS & required : []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private :meshopt_decode_target?, :meshopt_fallback?, :referenced_as_meshopt_target?,
|
|
48
|
+
:required_meshopt_extensions
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class BufferView < Properties::Base
|
|
5
|
+
TARGETS = { 34_962 => :array_buffer, 34_963 => :element_array_buffer }.freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :buffer, :byte_offset, :byte_length, :byte_stride, :target
|
|
8
|
+
|
|
9
|
+
def initialize(json, document:, index:)
|
|
10
|
+
super(json, document:, index:, owner_type: :buffer_view)
|
|
11
|
+
@buffer = document.fetch_reference(:buffers, json.fetch('buffer'), "bufferViews/#{index}/buffer")
|
|
12
|
+
@byte_offset = json.fetch('byteOffset', 0)
|
|
13
|
+
@byte_length = json.fetch('byteLength')
|
|
14
|
+
@byte_stride = json['byteStride']
|
|
15
|
+
@target = TARGETS.fetch(json['target'], json['target'])
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Camera < Properties::Base
|
|
5
|
+
attr_reader :type, :perspective, :orthographic
|
|
6
|
+
|
|
7
|
+
def initialize(json, document:, index:)
|
|
8
|
+
super(json, document:, index:, owner_type: :camera)
|
|
9
|
+
@type = json.fetch('type').to_sym
|
|
10
|
+
case @type
|
|
11
|
+
when :perspective
|
|
12
|
+
@perspective = Perspective.new(json.fetch('perspective'), document:)
|
|
13
|
+
when :orthographic
|
|
14
|
+
@orthographic = Orthographic.new(json.fetch('orthographic'), document:)
|
|
15
|
+
else
|
|
16
|
+
raise FormatError, "camera #{index} has invalid type #{@type.inspect}"
|
|
17
|
+
end
|
|
18
|
+
rescue KeyError => e
|
|
19
|
+
raise FormatError, "invalid camera #{index}: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def nested_properties
|
|
23
|
+
[perspective || orthographic]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Perspective < Properties::Base
|
|
27
|
+
attr_reader :aspect_ratio, :yfov, :zfar, :znear
|
|
28
|
+
|
|
29
|
+
def initialize(json, document:)
|
|
30
|
+
super(json, document:, owner_type: :camera_perspective)
|
|
31
|
+
@aspect_ratio = json['aspectRatio']
|
|
32
|
+
@yfov = json.fetch('yfov')
|
|
33
|
+
@zfar = json['zfar']
|
|
34
|
+
@znear = json.fetch('znear')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Orthographic < Properties::Base
|
|
39
|
+
attr_reader :xmag, :ymag, :zfar, :znear
|
|
40
|
+
|
|
41
|
+
def initialize(json, document:)
|
|
42
|
+
super(json, document:, owner_type: :camera_orthographic)
|
|
43
|
+
@xmag = json.fetch('xmag')
|
|
44
|
+
@ymag = json.fetch('ymag')
|
|
45
|
+
@zfar = json.fetch('zfar')
|
|
46
|
+
@znear = json.fetch('znear')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Image < Properties::Base
|
|
5
|
+
attr_reader :mime_type, :buffer_view
|
|
6
|
+
|
|
7
|
+
def initialize(json, document:, index:)
|
|
8
|
+
super(json, document:, index:, owner_type: :image)
|
|
9
|
+
@source_uri = json['uri']
|
|
10
|
+
@mime_type = json['mimeType']
|
|
11
|
+
@buffer_view = if json['bufferView'].nil?
|
|
12
|
+
nil
|
|
13
|
+
else
|
|
14
|
+
document.fetch_reference(:buffer_views, json['bufferView'], "images/#{index}/bufferView")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def uri
|
|
19
|
+
@source_uri unless @source_uri&.start_with?('data:')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def source?
|
|
23
|
+
buffer_view || @source_uri
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bytes
|
|
27
|
+
@bytes ||= begin
|
|
28
|
+
data = if buffer_view
|
|
29
|
+
buffer_view.buffer.bytes.byteslice(buffer_view.byte_offset, buffer_view.byte_length)
|
|
30
|
+
elsif @source_uri
|
|
31
|
+
@document.buffer_resolver.resolve_resource(@source_uri)
|
|
32
|
+
else
|
|
33
|
+
raise MissingResourceError, "image #{index} has neither uri nor bufferView"
|
|
34
|
+
end
|
|
35
|
+
raise FormatError, "image #{index} could not be read" unless data
|
|
36
|
+
if buffer_view && data.bytesize != buffer_view.byte_length
|
|
37
|
+
raise FormatError, "image #{index} exceeds its buffer"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
data.b.freeze
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def release!
|
|
45
|
+
@bytes = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class TextureInfo < Properties::Base
|
|
5
|
+
attr_reader :texture, :tex_coord
|
|
6
|
+
|
|
7
|
+
def initialize(json, document:, owner_type: :texture_info)
|
|
8
|
+
super
|
|
9
|
+
@texture = document.fetch_reference(:textures, json.fetch('index'), 'textureInfo/index')
|
|
10
|
+
@tex_coord = json.fetch('texCoord', 0)
|
|
11
|
+
rescue KeyError => e
|
|
12
|
+
raise FormatError, "invalid textureInfo: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def transform
|
|
16
|
+
extension('KHR_texture_transform')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class NormalTextureInfo < TextureInfo
|
|
21
|
+
attr_reader :scale
|
|
22
|
+
|
|
23
|
+
def initialize(json, document:)
|
|
24
|
+
super
|
|
25
|
+
@scale = json.fetch('scale', 1.0)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class OcclusionTextureInfo < TextureInfo
|
|
30
|
+
attr_reader :strength
|
|
31
|
+
|
|
32
|
+
def initialize(json, document:)
|
|
33
|
+
super
|
|
34
|
+
@strength = json.fetch('strength', 1.0)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class PbrMetallicRoughness < Properties::Base
|
|
39
|
+
attr_reader :base_color_factor, :base_color_texture, :metallic_factor,
|
|
40
|
+
:roughness_factor, :metallic_roughness_texture
|
|
41
|
+
|
|
42
|
+
def initialize(json, document:)
|
|
43
|
+
super(json, document:, owner_type: :pbr_metallic_roughness)
|
|
44
|
+
@base_color_factor = json.fetch('baseColorFactor', [1.0, 1.0, 1.0, 1.0]).freeze
|
|
45
|
+
@base_color_texture = texture_info(json['baseColorTexture'], document)
|
|
46
|
+
@metallic_factor = json.fetch('metallicFactor', 1.0)
|
|
47
|
+
@roughness_factor = json.fetch('roughnessFactor', 1.0)
|
|
48
|
+
@metallic_roughness_texture = texture_info(json['metallicRoughnessTexture'], document)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def nested_properties
|
|
52
|
+
[base_color_texture, metallic_roughness_texture].compact
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def texture_info(value, document)
|
|
58
|
+
TextureInfo.new(value, document:) if value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Material < Properties::Base
|
|
63
|
+
ALPHA_MODES = { 'OPAQUE' => :opaque, 'MASK' => :mask, 'BLEND' => :blend }.freeze
|
|
64
|
+
|
|
65
|
+
attr_reader :pbr, :normal_texture, :occlusion_texture, :emissive_texture,
|
|
66
|
+
:emissive_factor, :alpha_mode, :alpha_cutoff, :double_sided
|
|
67
|
+
|
|
68
|
+
def initialize(json, document:, index:)
|
|
69
|
+
super(json, document:, index:, owner_type: :material)
|
|
70
|
+
@pbr = PbrMetallicRoughness.new(json.fetch('pbrMetallicRoughness', {}), document:)
|
|
71
|
+
@normal_texture = NormalTextureInfo.new(json['normalTexture'], document:) if json['normalTexture']
|
|
72
|
+
@occlusion_texture = OcclusionTextureInfo.new(json['occlusionTexture'], document:) if json['occlusionTexture']
|
|
73
|
+
@emissive_texture = TextureInfo.new(json['emissiveTexture'], document:) if json['emissiveTexture']
|
|
74
|
+
@emissive_factor = json.fetch('emissiveFactor', [0.0, 0.0, 0.0]).freeze
|
|
75
|
+
@alpha_mode = ALPHA_MODES.fetch(json.fetch('alphaMode', 'OPAQUE'))
|
|
76
|
+
@alpha_cutoff = json.fetch('alphaCutoff', 0.5)
|
|
77
|
+
@alpha_cutoff_explicit = json.key?('alphaCutoff')
|
|
78
|
+
@double_sided = json.fetch('doubleSided', false)
|
|
79
|
+
rescue KeyError => e
|
|
80
|
+
raise FormatError, "invalid material #{index}: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unlit?
|
|
84
|
+
extensions.key?('KHR_materials_unlit')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def alpha_cutoff_explicit?
|
|
88
|
+
@alpha_cutoff_explicit
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def nested_properties
|
|
92
|
+
[pbr, normal_texture, occlusion_texture, emissive_texture].compact
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rgltf
|
|
4
|
+
class Primitive < Properties::Base
|
|
5
|
+
MODES = {
|
|
6
|
+
0 => :points, 1 => :lines, 2 => :line_loop, 3 => :line_strip,
|
|
7
|
+
4 => :triangles, 5 => :triangle_strip, 6 => :triangle_fan
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :attributes, :indices, :material, :mode, :targets
|
|
11
|
+
|
|
12
|
+
def initialize(json, document:, mesh_index:, index:)
|
|
13
|
+
super(json, document:, index:, owner_type: :primitive)
|
|
14
|
+
@attributes = json.fetch('attributes').to_h do |semantic, accessor_index|
|
|
15
|
+
[semantic.to_sym, document.fetch_reference(
|
|
16
|
+
:accessors,
|
|
17
|
+
accessor_index,
|
|
18
|
+
"meshes/#{mesh_index}/primitives/#{index}/attributes/#{semantic}"
|
|
19
|
+
)]
|
|
20
|
+
end.freeze
|
|
21
|
+
@indices = optional_reference(document, :accessors, json['indices'],
|
|
22
|
+
"meshes/#{mesh_index}/primitives/#{index}/indices")
|
|
23
|
+
@material = optional_reference(document, :materials, json['material'],
|
|
24
|
+
"meshes/#{mesh_index}/primitives/#{index}/material")
|
|
25
|
+
@mode = MODES.fetch(json.fetch('mode', 4))
|
|
26
|
+
@targets = json.fetch('targets', []).each_with_index.map do |target, target_index|
|
|
27
|
+
target.to_h do |semantic, accessor_index|
|
|
28
|
+
[semantic.to_sym, document.fetch_reference(
|
|
29
|
+
:accessors,
|
|
30
|
+
accessor_index,
|
|
31
|
+
"meshes/#{mesh_index}/primitives/#{index}/targets/#{target_index}/#{semantic}"
|
|
32
|
+
)]
|
|
33
|
+
end.freeze
|
|
34
|
+
end.freeze
|
|
35
|
+
rescue KeyError => e
|
|
36
|
+
raise FormatError, "invalid mesh primitive: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def optional_reference(document, collection, value, path)
|
|
42
|
+
value.nil? ? nil : document.fetch_reference(collection, value, path)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Mesh < Properties::Base
|
|
47
|
+
attr_reader :primitives, :weights
|
|
48
|
+
|
|
49
|
+
def initialize(json, document:, index:)
|
|
50
|
+
super(json, document:, index:, owner_type: :mesh)
|
|
51
|
+
@primitives = json.fetch('primitives').each_with_index.map do |primitive, primitive_index|
|
|
52
|
+
Primitive.new(primitive, document:, mesh_index: index, index: primitive_index)
|
|
53
|
+
end.freeze
|
|
54
|
+
@weights = json['weights']&.freeze
|
|
55
|
+
rescue KeyError => e
|
|
56
|
+
raise FormatError, "invalid mesh #{index}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def nested_properties
|
|
60
|
+
primitives
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def replace_primitives!(values)
|
|
64
|
+
@primitives = values.freeze
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|