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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +270 -0
  4. data/Rakefile +10 -0
  5. data/benchmark/accessors.rb +26 -0
  6. data/benchmark/compare.rb +28 -0
  7. data/benchmark/sample_assets.rb +61 -0
  8. data/examples/README.md +23 -0
  9. data/examples/build_triangle.rb +40 -0
  10. data/examples/convert.rb +24 -0
  11. data/examples/inspect.rb +27 -0
  12. data/lib/rgltf/accessor_reader.rb +142 -0
  13. data/lib/rgltf/buffer_resolver.rb +95 -0
  14. data/lib/rgltf/builder/accessor_data.rb +40 -0
  15. data/lib/rgltf/builder/accessors.rb +130 -0
  16. data/lib/rgltf/builder/component_builders.rb +89 -0
  17. data/lib/rgltf/builder/document_properties.rb +32 -0
  18. data/lib/rgltf/builder/materials.rb +132 -0
  19. data/lib/rgltf/builder.rb +176 -0
  20. data/lib/rgltf/document.rb +159 -0
  21. data/lib/rgltf/errors.rb +10 -0
  22. data/lib/rgltf/extension.rb +79 -0
  23. data/lib/rgltf/extensions/khr_lights_punctual.rb +101 -0
  24. data/lib/rgltf/extensions/khr_materials_emissive_strength.rb +29 -0
  25. data/lib/rgltf/extensions/khr_materials_unlit.rb +19 -0
  26. data/lib/rgltf/extensions/khr_texture_transform.rb +42 -0
  27. data/lib/rgltf/glb.rb +71 -0
  28. data/lib/rgltf/properties/accessor.rb +142 -0
  29. data/lib/rgltf/properties/animation.rb +96 -0
  30. data/lib/rgltf/properties/asset.rb +15 -0
  31. data/lib/rgltf/properties/base.rb +45 -0
  32. data/lib/rgltf/properties/buffer.rb +50 -0
  33. data/lib/rgltf/properties/buffer_view.rb +18 -0
  34. data/lib/rgltf/properties/camera.rb +50 -0
  35. data/lib/rgltf/properties/image.rb +48 -0
  36. data/lib/rgltf/properties/material.rb +95 -0
  37. data/lib/rgltf/properties/mesh.rb +67 -0
  38. data/lib/rgltf/properties/node.rb +135 -0
  39. data/lib/rgltf/properties/sampler.rb +35 -0
  40. data/lib/rgltf/properties/scene.rb +14 -0
  41. data/lib/rgltf/properties/skin.rb +25 -0
  42. data/lib/rgltf/properties/texture.rb +19 -0
  43. data/lib/rgltf/properties.rb +17 -0
  44. data/lib/rgltf/validation/accessors.rb +205 -0
  45. data/lib/rgltf/validation/animations.rb +184 -0
  46. data/lib/rgltf/validation/buffers.rb +95 -0
  47. data/lib/rgltf/validation/cameras.rb +76 -0
  48. data/lib/rgltf/validation/context.rb +133 -0
  49. data/lib/rgltf/validation/images_materials.rb +159 -0
  50. data/lib/rgltf/validation/meshes.rb +214 -0
  51. data/lib/rgltf/validation/root_asset_extensions.rb +134 -0
  52. data/lib/rgltf/validation/scenes_nodes.rb +141 -0
  53. data/lib/rgltf/validation/skins.rb +94 -0
  54. data/lib/rgltf/validator.rb +53 -0
  55. data/lib/rgltf/version.rb +5 -0
  56. data/lib/rgltf/writer/buffer_merger.rb +72 -0
  57. data/lib/rgltf/writer/default_omitter.rb +51 -0
  58. data/lib/rgltf/writer/extension_serializer.rb +64 -0
  59. data/lib/rgltf/writer.rb +95 -0
  60. data/lib/rgltf.rb +133 -0
  61. data/tasks/sample_assets.rake +115 -0
  62. 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