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 +4 -4
- data/.codespellignore +0 -0
- data/.yamllint +16 -0
- data/CHANGELOG.md +5 -2
- data/lib/riggle/version.rb +1 -1
- data/lib/riggle.rb +85 -26
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4362a3d5e336c120434204e5c9fed8cf5f265c038d8c561dd305c8771c117f4c
|
|
4
|
+
data.tar.gz: b27d5a75da38eed92d5dbc0d537ed7bbb5b35c10c80954994668a567691725cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d631d1a338a434473e9e4251952ecd8d5adc5b999a9fc39b8fcb237b661dc8d07ffc5221f4c58b789b48ea84b9e3f5b61282dd3b14b7ede012b560ad90083e9
|
|
7
|
+
data.tar.gz: 86d7d89d4c17d67570e53b82bed9f8c6dbc91ed45bc6d21893485ccad2dbec0da0d88a78a969458f726a4eb58cc7f62a7557c96b12da2f54ab52ad546ee1fed2
|
data/.codespellignore
ADDED
|
File without changes
|
data/.yamllint
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.1.
|
|
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
|
-
|
|
10
|
+
## [0.1.1] - 2026-09-25
|
|
8
11
|
|
|
9
12
|
## [0.1.0] - 2026-09-23
|
|
10
13
|
|
data/lib/riggle/version.rb
CHANGED
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
|
|
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
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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 =
|
|
563
|
-
view = 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 +
|
|
604
|
+
view_end = view_start + view_length
|
|
573
605
|
stride = buffer_view["byteStride"] || value_size
|
|
574
|
-
raise ArgumentError, "invalid glTF accessor stride"
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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.
|
|
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-
|
|
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
|