riggle 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10165b0961011c1131d41f088a51b4651cc45f0e0b43856f9ddbfa8d0eb1ec0a
4
- data.tar.gz: c45457d6149866e9df109cd34e1c1818ce3e498ab7317e899474fcbabcd1f31e
3
+ metadata.gz: 4362a3d5e336c120434204e5c9fed8cf5f265c038d8c561dd305c8771c117f4c
4
+ data.tar.gz: b27d5a75da38eed92d5dbc0d537ed7bbb5b35c10c80954994668a567691725cc
5
5
  SHA512:
6
- metadata.gz: d7a587bbc0341247630217f64ff503788a8b79f238c2c87650ccd0fc3e0862eb2a45f40655ddc99d1b760dae1eb9f3e4f822ab66e393657595af960d9a319e7a
7
- data.tar.gz: 52d6f667a99962f7e6e9449a6ccebe1fdd220983de2383ade21ed9f9fab1b814981c29eb20c4e8b999c92c2852fa2cca0d320d546be32d8fe32ed8f77d332884
6
+ metadata.gz: 0d631d1a338a434473e9e4251952ecd8d5adc5b999a9fc39b8fcb237b661dc8d07ffc5221f4c58b789b48ea84b9e3f5b61282dd3b14b7ede012b560ad90083e9
7
+ data.tar.gz: 86d7d89d4c17d67570e53b82bed9f8c6dbc91ed45bc6d21893485ccad2dbec0da0d88a78a969458f726a4eb58cc7f62a7557c96b12da2f54ab52ad546ee1fed2
data/.codespellignore ADDED
File without changes
data/.yamllint ADDED
@@ -0,0 +1,16 @@
1
+ extends: default
2
+ ignore: |
3
+ build/
4
+ node_modules/
5
+ runtime/build/
6
+ tmp/
7
+ **/*.dSYM/**
8
+
9
+ rules:
10
+ comments:
11
+ min-spaces-from-content: 1
12
+ document-start: disable
13
+ line-length:
14
+ max: 200
15
+ truthy:
16
+ check-keys: false
data/CHANGELOG.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.1.1] - 2026-09-25
5
+ ## [0.1.2] - 2026-09-26
6
+
7
+ - Correct blended dual quaternion transforms during skinning.
8
+ - Reject invalid glTF node, skin, accessor, and buffer references with clear errors.
6
9
 
7
- - Correct the gem author and license attribution.
10
+ ## [0.1.1] - 2026-09-25
8
11
 
9
12
  ## [0.1.0] - 2026-09-23
10
13
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riggle
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/riggle.rb CHANGED
@@ -190,9 +190,7 @@ module Riggle
190
190
  def apply(primitive, skin, scene, method: :lbs)
191
191
  raise ArgumentError, "unknown skinning method: #{method}" unless %i[lbs dqs].include?(method.to_sym)
192
192
  return primitive.positions.dup unless skin
193
- matrices = skin.joints.map.with_index do |joint, index|
194
- scene.world_matrix(joint) * (skin.inverse_bind_matrices&.[](index) || Mat4.identity)
195
- end
193
+ matrices = joint_matrices(primitive, skin, scene)
196
194
  return apply_dual_quaternions(primitive, matrices) if method.to_sym == :dqs && matrices.all? { |matrix| rigid_transform?(matrix) }
197
195
  return apply_linear_blend(primitive, matrices)
198
196
  end
@@ -202,15 +200,25 @@ module Riggle
202
200
  return nil unless primitive.normals
203
201
  return primitive.normals.map(&:dup) unless skin
204
202
 
205
- matrices = skin.joints.map.with_index do |joint, index|
206
- scene.world_matrix(joint) * (skin.inverse_bind_matrices&.[](index) || Mat4.identity)
207
- end
203
+ matrices = joint_matrices(primitive, skin, scene)
208
204
  if method.to_sym == :dqs && matrices.all? { |matrix| rigid_transform?(matrix) }
209
205
  return apply_dual_quaternion_normals(primitive, matrices)
210
206
  end
211
207
  apply_linear_blend_normals(primitive, matrices)
212
208
  end
213
209
 
210
+ def joint_matrices(primitive, skin, scene)
211
+ primitive.joints&.each do |indices|
212
+ indices.each do |index|
213
+ raise ArgumentError, "invalid skin joint index: #{index}" unless index.is_a?(Integer) && index >= 0 && index < skin.joints.length
214
+ end
215
+ end
216
+ skin.joints.map.with_index do |joint, index|
217
+ scene.world_matrix(joint) * (skin.inverse_bind_matrices&.[](index) || Mat4.identity)
218
+ end
219
+ end
220
+ private_class_method :joint_matrices
221
+
214
222
  def apply_linear_blend(primitive, matrices)
215
223
  primitive.positions.each_with_index.map do |position, index|
216
224
  joints = primitive.joints&.[](index) || [0]
@@ -361,8 +369,10 @@ module Riggle
361
369
  private_class_method :dual_quaternion
362
370
 
363
371
  def transform_dual_quaternion(point, real, dual)
364
- real = quaternion_normalize(real)
365
- dual = quaternion_scale(dual, 1.0 / Math.sqrt(real.sum { |value| value * value }))
372
+ length = Math.sqrt(real.sum { |value| value * value })
373
+ return point if length.zero?
374
+ real = quaternion_scale(real, 1.0 / length)
375
+ dual = quaternion_scale(dual, 1.0 / length)
366
376
  rotated = quaternion_multiply(quaternion_multiply(real, [point.x, point.y, point.z, 0.0]), quaternion_conjugate(real))
367
377
  translation = quaternion_multiply(dual, quaternion_conjugate(real)).first(3).map { |value| value * 2 }
368
378
  Vec3.new(x: rotated[0] + translation[0], y: rotated[1] + translation[1], z: rotated[2] + translation[2])
@@ -417,7 +427,10 @@ module Riggle
417
427
  end
418
428
 
419
429
  def world_matrix(node)
420
- node = @nodes[node] if node.is_a?(Integer)
430
+ if node.is_a?(Integer)
431
+ raise ArgumentError, "invalid node index: #{node}" unless node >= 0 && node < @nodes.length
432
+ node = @nodes[node]
433
+ end
421
434
  index = @nodes.index(node)
422
435
  raise ArgumentError, "node is not part of the scene" unless index
423
436
  parent = @nodes.find { |candidate| candidate.children.to_a.include?(index) }
@@ -427,8 +440,8 @@ module Riggle
427
440
 
428
441
  def apply_pose!(pose)
429
442
  pose.each do |index, values|
443
+ raise ArgumentError, "invalid node index: #{index}" unless index.is_a?(Integer) && index >= 0 && index < @nodes.length
430
444
  node = @nodes[index]
431
- raise ArgumentError, "invalid node index: #{index}" unless node
432
445
  values.each do |key, value|
433
446
  node[key] = case key.to_sym
434
447
  when :translation, :scale then Vec3.new(x: value[0], y: value[1], z: value[2])
@@ -556,11 +569,26 @@ module Riggle
556
569
  json, binary = document(path)
557
570
  raise UnsupportedError, "glTF 2.x is required" unless json.dig("asset", "version").to_s.match?(/\A2\./)
558
571
  raise UnsupportedError, "required glTF extensions are not supported" unless json.fetch("extensionsRequired", []).empty?
572
+ reference = lambda do |items, index, kind|
573
+ raise ArgumentError, "invalid glTF #{kind} index" unless index.is_a?(Integer) && index.between?(0, items.length - 1)
574
+
575
+ items[index]
576
+ end
559
577
  buffers = json.fetch("buffers", []).map { |buffer| buffer_data(buffer, binary, base_dir) }
560
578
  views = json.fetch("bufferViews", [])
579
+ views.each do |view|
580
+ index = view.fetch("buffer")
581
+ raise ArgumentError, "invalid glTF buffer view" unless index.is_a?(Integer) && index.between?(0, buffers.length - 1)
582
+
583
+ offset = view.fetch("byteOffset", 0)
584
+ length = view.fetch("byteLength")
585
+ raise ArgumentError, "invalid glTF buffer view" unless offset.is_a?(Integer) && offset >= 0 && length.is_a?(Integer) && length >= 0 && offset + length <= buffers[index].bytesize
586
+ end
587
+ view_at = ->(index) { reference.call(views, index, "buffer view") }
588
+ accessors = json.fetch("accessors", [])
561
589
  access = lambda do |index|
562
- definition = json.fetch("accessors")[index]
563
- view = definition["bufferView"] && views[definition["bufferView"]]
590
+ definition = reference.call(accessors, index, "accessor")
591
+ view = view_at.call(definition["bufferView"]) if definition.key?("bufferView")
564
592
  count = definition["count"]
565
593
  components = COMPONENTS.fetch(definition["type"])
566
594
  format = FORMATS.fetch(definition["componentType"])
@@ -568,12 +596,16 @@ module Riggle
568
596
  read_values = lambda do |buffer_view, byte_offset, value_count, value_format, value_size, value_components|
569
597
  raw = buffers.fetch(buffer_view.fetch("buffer"))
570
598
  view_start = buffer_view.fetch("byteOffset", 0)
599
+ view_length = buffer_view.fetch("byteLength")
600
+ raise ArgumentError, "invalid glTF accessor byte offset" unless byte_offset.is_a?(Integer) && byte_offset >= 0
601
+ raise ArgumentError, "invalid glTF accessor count" unless value_count.is_a?(Integer) && value_count >= 0
602
+
571
603
  start = view_start + byte_offset
572
- view_end = view_start + buffer_view.fetch("byteLength")
604
+ view_end = view_start + view_length
573
605
  stride = buffer_view["byteStride"] || value_size
574
- raise ArgumentError, "invalid glTF accessor stride" if stride < value_size
606
+ raise ArgumentError, "invalid glTF accessor stride" unless stride.is_a?(Integer) && stride >= value_size
575
607
  needed = value_count.zero? ? 0 : (value_count - 1) * stride + value_size
576
- raise ArgumentError, "glTF accessor exceeds buffer view" if start + needed > view_end || start + needed > raw.bytesize
608
+ raise ArgumentError, "glTF accessor exceeds buffer view" if start + needed > view_end
577
609
  Array.new(value_count) { |item| raw.byteslice(start + item * stride, value_size).unpack(value_format * value_components) }
578
610
  end
579
611
  values = if view
@@ -587,11 +619,11 @@ module Riggle
587
619
  if sparse
588
620
  sparse_count = sparse.fetch("count")
589
621
  raise ArgumentError, "sparse accessor count exceeds accessor count" if sparse_count > count
590
- index_view = views.fetch(sparse.fetch("indices").fetch("bufferView"))
622
+ index_view = view_at.call(sparse.fetch("indices").fetch("bufferView"))
591
623
  index_component = sparse.fetch("indices").fetch("componentType")
592
624
  index_format = { 5121 => "C", 5123 => "S<", 5125 => "L<" }.fetch(index_component) { raise ArgumentError, "invalid sparse index component type" }
593
625
  index_values = read_values.call(index_view, sparse["indices"].fetch("byteOffset", 0), sparse_count, index_format, format_size(index_format), 1).map(&:first)
594
- value_view = views.fetch(sparse.fetch("values").fetch("bufferView"))
626
+ value_view = view_at.call(sparse.fetch("values").fetch("bufferView"))
595
627
  sparse_values = read_values.call(value_view, sparse["values"].fetch("byteOffset", 0), sparse_count, format, item_size, components)
596
628
  index_values.each_with_index do |target, sparse_index|
597
629
  raise ArgumentError, "sparse accessor index is out of range" if target >= count
@@ -626,7 +658,7 @@ module Riggle
626
658
  end
627
659
  image_refs = json.fetch("images", []).each_with_index.map do |image, index|
628
660
  next image_refs[index] if image["uri"]
629
- view = views.fetch(image.fetch("bufferView"))
661
+ view = view_at.call(image.fetch("bufferView"))
630
662
  raw = buffers.fetch(view.fetch("buffer")).byteslice(view.fetch("byteOffset", 0), view.fetch("byteLength"))
631
663
  image_refs[index].tap do |reference|
632
664
  if load_images
@@ -635,10 +667,10 @@ module Riggle
635
667
  end
636
668
  end
637
669
  end
638
- textures = json.fetch("textures", []).map { |texture| image_refs[texture["source"]] }
670
+ textures = json.fetch("textures", []).map { |texture| reference.call(image_refs, texture["source"], "image") }
639
671
  materials = json.fetch("materials", []).map do |material|
640
672
  pbr = material["pbrMetallicRoughness"] || {}
641
- texture = pbr["baseColorTexture"] && textures[pbr["baseColorTexture"]["index"]]
673
+ texture = pbr["baseColorTexture"] && reference.call(textures, pbr["baseColorTexture"]["index"], "texture")
642
674
  Material.new(name: material["name"], base_color_factor: pbr["baseColorFactor"] || [1, 1, 1, 1], base_color_texture: texture, emissive: material["emissiveFactor"] || [0, 0, 0], alpha_mode: material["alphaMode"] || "OPAQUE")
643
675
  end
644
676
  meshes = json.fetch("meshes", []).map do |mesh|
@@ -658,29 +690,56 @@ module Riggle
658
690
  joints: attributes["JOINTS_0"] && access.call(attributes["JOINTS_0"]),
659
691
  weights: attributes["WEIGHTS_0"] && access.call(attributes["WEIGHTS_0"]),
660
692
  indices: primitive["indices"] ? access.call(primitive["indices"]) : (0...position_values.length).to_a,
661
- material: materials[primitive["material"] || 0]
693
+ material: primitive.key?("material") ? reference.call(materials, primitive["material"], "material") : nil
662
694
  )
663
695
  end
664
696
  Mesh.new(name: mesh["name"], primitives: primitives)
665
697
  end
666
- nodes = json.fetch("nodes", []).map do |node|
698
+ node_definitions = json.fetch("nodes", [])
699
+ skin_definitions = json.fetch("skins", [])
700
+ parent_counts = Array.new(node_definitions.length, 0)
701
+ node_definitions.each do |node|
702
+ (node["children"] || []).each do |child|
703
+ reference.call(node_definitions, child, "node")
704
+ parent_counts[child] += 1
705
+ raise ArgumentError, "invalid glTF node hierarchy: multiple parents" if parent_counts[child] > 1
706
+ end
707
+ end
708
+ roots = parent_counts.each_index.select { |index| parent_counts[index].zero? }
709
+ visited = 0
710
+ until roots.empty?
711
+ visited += 1
712
+ (node_definitions[roots.pop]["children"] || []).each do |child|
713
+ parent_counts[child] -= 1
714
+ roots << child if parent_counts[child].zero?
715
+ end
716
+ end
717
+ raise ArgumentError, "invalid glTF node hierarchy: cycle" unless visited == node_definitions.length
718
+
719
+ nodes = node_definitions.map do |node|
720
+ children = node["children"] || []
721
+ reference.call(skin_definitions, node["skin"], "skin") if node.key?("skin")
667
722
  translation = node["translation"] || [0, 0, 0]
668
723
  rotation = node["rotation"] || [0, 0, 0, 1]
669
724
  scale = node["scale"] || [1, 1, 1]
670
725
  matrix = node["matrix"]&.each_slice(4)&.to_a
671
- Node.new(name: node["name"], children: node["children"] || [], mesh: node["mesh"] && meshes[node["mesh"]], translation: Vec3.new(x: translation[0], y: translation[1], z: translation[2]), rotation: Quat.new(x: rotation[0], y: rotation[1], z: rotation[2], w: rotation[3]), scale: Vec3.new(x: scale[0], y: scale[1], z: scale[2]), matrix: matrix && Mat4.new(matrix.transpose.flatten), skin: node["skin"])
726
+ Node.new(name: node["name"], children: children, mesh: node.key?("mesh") ? reference.call(meshes, node["mesh"], "mesh") : nil, translation: Vec3.new(x: translation[0], y: translation[1], z: translation[2]), rotation: Quat.new(x: rotation[0], y: rotation[1], z: rotation[2], w: rotation[3]), scale: Vec3.new(x: scale[0], y: scale[1], z: scale[2]), matrix: matrix && Mat4.new(matrix.transpose.flatten), skin: node["skin"])
672
727
  end
673
- skins = json.fetch("skins", []).map do |skin|
728
+ skins = skin_definitions.map do |skin|
729
+ joints = skin.fetch("joints")
730
+ joints.each { |index| reference.call(nodes, index, "node") }
731
+ reference.call(nodes, skin["skeleton"], "node") if skin.key?("skeleton")
674
732
  inverse_bind_matrices = if skin["inverseBindMatrices"]
675
733
  access.call(skin["inverseBindMatrices"]).map { |value| Mat4.new(value.each_slice(4).to_a.transpose.flatten) }
676
734
  end
677
- Skin.new(joints: skin.fetch("joints"), inverse_bind_matrices: inverse_bind_matrices, skeleton: skin["skeleton"])
735
+ Skin.new(joints: joints, inverse_bind_matrices: inverse_bind_matrices, skeleton: skin["skeleton"])
678
736
  end
679
737
  animations = json.fetch("animations", []).map do |animation|
680
738
  samplers = animation.fetch("samplers")
681
739
  channels = animation.fetch("channels").map do |channel|
682
- sampler = samplers.fetch(channel.fetch("sampler"))
740
+ sampler = reference.call(samplers, channel.fetch("sampler"), "animation sampler")
683
741
  target = channel.fetch("target")
742
+ reference.call(nodes, target.fetch("node"), "node")
684
743
  Channel.new(node_index: target.fetch("node"), path: target.fetch("path"), times: access.call(sampler.fetch("input")), values: access.call(sampler.fetch("output")), interpolation: sampler.fetch("interpolation", "LINEAR"))
685
744
  end
686
745
  Animation.new(name: animation["name"], channels: channels)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riggle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-25 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Small scene models, materials, animation data, and mesh accessors.
14
14
  email:
@@ -17,6 +17,8 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".codespellignore"
21
+ - ".yamllint"
20
22
  - CHANGELOG.md
21
23
  - LICENSE.txt
22
24
  - README.md