Skip to content

Commit f725f0c

Browse files
committed
Joints visualization: few fixes and optimizations
- Make it right for both HAnimHumanoid and Skin - Make it call ChangedAll if needed, but not multiple times - Do not visit joint multiple times - share FontStyle - more reliably make shape transparent
1 parent 7667273 commit f725f0c

File tree

2 files changed

+135
-73
lines changed

2 files changed

+135
-73
lines changed

castle_model_viewer.dpr

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2739,13 +2739,11 @@ procedure TEventsHandler.MenuClick(const MenuItem: TMenuItem);
27392739
if MessageInputQuery(Window, 'Joint Visualization Size (default based on scene size):',
27402740
Vis.JointVisualizationSize) then
27412741
begin
2742-
Vis.VisualizeAllHumanoids(Scene.RootNode);
2743-
MessageOK(Window, Format('%d H-Anim Humanoids (%d Joints inside) processed.', [
2744-
Vis.HumanoidsProcessed,
2742+
Vis.VisualizeAllHumanoids(Scene);
2743+
MessageOK(Window, Format('Processed %d H-Anim Humanoid nodes.' + NL + '%d joints (HAnimJoint nodes) inside (that have not been processed before).', [
2744+
Vis.SkinsProcessed,
27452745
Vis.JointsProcessed
27462746
]));
2747-
if Vis.HumanoidsProcessed <> 0 then
2748-
Scene.ChangedAll;
27492747
end;
27502748
finally FreeAndNil(Vis) end;
27512749
end;
@@ -2760,12 +2758,11 @@ procedure TEventsHandler.MenuClick(const MenuItem: TMenuItem);
27602758
if MessageInputQuery(Window, 'Joint Visualization Size (default based on scene size):',
27612759
Vis.JointVisualizationSize) then
27622760
begin
2763-
Vis.VisualizeAllTransformations(Scene.RootNode);
2764-
MessageOK(Window, Format('%d Joints processed.', [
2761+
Vis.VisualizeAllSkins(Scene);
2762+
MessageOK(Window, Format('Processed %d Skin nodes.' + NL + '%d joints (Transform nodes) inside (that have not been processed before).', [
2763+
Vis.SkinsProcessed,
27652764
Vis.JointsProcessed
27662765
]));
2767-
if Vis.JointsProcessed <> 0 then
2768-
Scene.ChangedAll;
27692766
end;
27702767
finally FreeAndNil(Vis) end;
27712768
end;
@@ -3577,7 +3574,7 @@ begin
35773574
'non-convex (forces faces to be triangulated carefully)', 33));
35783575
M.Append(TMenuItem.Create('Convert Inline to Group (pulls external content into this model)', 33000));
35793576
M.Append(TMenuSeparator.Create);
3580-
M.Append(TMenuItem.Create('Add Joints Visualization ...', 45));
3577+
M.Append(TMenuItem.Create('Add Skin Joints Visualization ...', 45));
35813578
M.Append(TMenuItem.Create('Add H-Anim Joints Visualization ...', 42));
35823579
Result.Append(M);
35833580
M := TMenu.Create('_Clipboard');

code/v3dsceneskeletonvisualize.pas

Lines changed: 128 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
Copyright 2006-2022 Michalis Kamburelis.
2+
Copyright 2006-2025 Michalis Kamburelis.
33
44
This file is part of "castle-model-viewer".
55
@@ -28,9 +28,10 @@
2828

2929
interface
3030

31-
uses X3DNodes;
31+
uses X3DNodes, CastleSceneCore;
3232

3333
type
34+
{ Visualize skeleton inside TSkinNode or THAnimHumanoidNode. }
3435
TSkeletonVisualize = class
3536
strict private
3637
{ Assigned only between JointVisualizationBegin/End.
@@ -39,34 +40,43 @@ TSkeletonVisualize = class
3940
SphereGeometry: TSphereNode;
4041
SphereAppearance: TAppearanceNode;
4142
SphereMaterial: TMaterialNode;
43+
FontStyle: TFontStyleNode;
4244
{ @groupEnd }
4345
procedure VisualizeHumanoid(Node: TX3DNode);
44-
procedure VisualizeTransformation(Node: TX3DNode);
45-
procedure JointVisualizationBegin;
46-
procedure JointVisualizationEnd;
46+
procedure VisualizeHAnimJoint(Node: TX3DNode);
47+
procedure VisualizeSkin(Node: TX3DNode);
48+
procedure VisualizeTransformInSkin(Node: TX3DNode);
49+
procedure JointVisualizationBegin(const Scene: TCastleSceneCore);
50+
procedure JointVisualizationEnd(const Scene: TCastleSceneCore);
4751
{ In case of H-Anim Joint, it is relative to humanoid root,
4852
and so should be placed in humanoid root.
4953
In case of other transformations, they are relative to parent transformation,
5054
and so should be placed in parent transformation. }
5155
function JointVisualization(const Joint: TX3DNode): TTransformNode;
56+
procedure MakeShapeTransparent(const Shape: TShapeNode);
5257
public
53-
{ Set before using VisualizeXxx }
58+
{ Set before using VisualizeAllHumanoids or VisualizeAllSkins. }
5459
JointVisualizationSize: Single;
55-
{ Informatio available after using VisualizeAllHumanoids. }
56-
HumanoidsProcessed: Cardinal;
60+
61+
{ Information available after using VisualizeAllHumanoids or VisualizeAllSkins.
62+
@groupBegin }
63+
SkinsProcessed: Cardinal;
5764
JointsProcessed: Cardinal;
65+
{ @groupEnd }
66+
5867
{ Show H-Anim Humanoid joints. }
59-
procedure VisualizeAllHumanoids(const Node: TX3DNode);
60-
{ Show all transformations. }
61-
procedure VisualizeAllTransformations(const Node: TX3DNode);
68+
procedure VisualizeAllHumanoids(const Scene: TCastleSceneCore);
69+
70+
{ Show Skin joints. }
71+
procedure VisualizeAllSkins(const Scene: TCastleSceneCore);
6272
end;
6373

6474
implementation
6575

66-
uses SysUtils,
76+
uses SysUtils, Math,
6777
CastleUtils, X3DFields, CastleStringUtils;
6878

69-
procedure TSkeletonVisualize.JointVisualizationBegin;
79+
procedure TSkeletonVisualize.JointVisualizationBegin(const Scene: TCastleSceneCore);
7080
const
7181
MatName = 'HumanoidJointVisualizeMat';
7282
begin
@@ -86,25 +96,56 @@ procedure TSkeletonVisualize.JointVisualizationBegin;
8696
SphereMaterial := TMaterialNode.Create(MatName);
8797
SphereMaterial.FdTransparency.Value := 0.3;
8898
SphereAppearance.FdMaterial.Value := SphereMaterial;
99+
100+
FontStyle := TFontStyleNode.Create;
101+
FontStyle.Size := JointVisualizationSize;
102+
103+
Scene.BeginChangesSchedule;
89104
end;
90105

91-
procedure TSkeletonVisualize.JointVisualizationEnd;
106+
procedure TSkeletonVisualize.JointVisualizationEnd(const Scene: TCastleSceneCore);
92107
begin
93108
SphereShape.FreeIfUnused;
94109
SphereShape := nil;
110+
111+
FontStyle.FreeIfUnused;
112+
FontStyle := nil;
113+
114+
Scene.EndChangesSchedule;
115+
end;
116+
117+
procedure TSkeletonVisualize.MakeShapeTransparent(const Shape: TShapeNode);
118+
var
119+
MatInfo: TMaterialInfo;
120+
begin
121+
// create Shape.Appearance if necessary
122+
if Shape.Appearance = nil then
123+
Shape.Appearance := TAppearanceNode.Create;
124+
Assert(Shape.Appearance <> nil);
125+
Shape.Appearance.AlphaMode := amBlend;
126+
127+
// create Shape.Appearance.Material if necessary
128+
if Shape.Appearance.Material = nil then
129+
Shape.Appearance.Material := TMaterialNode.Create;
130+
Assert(Shape.Appearance.Material <> nil);
131+
132+
// make transparency at least 0.5
133+
MatInfo := Shape.Appearance.Material.MaterialInfo;
134+
Assert(MatInfo <> nil);
135+
MatInfo.Transparency := Max(0.5, MatInfo.Transparency);
95136
end;
96137

97138
function TSkeletonVisualize.JointVisualization(const Joint: TX3DNode): TTransformNode;
98139
var
99140
TextShape: TShapeNode;
100141
TextGeometry: TTextNode;
101-
FontStyle: TFontStyleNode;
102142
CenterRoute: TX3DRoute;
103143
JointCenter: TSFVec3f;
104144
JointName: String;
105145
begin
106146
{ Handle Joint being of THAnimJointNode or TTransformNode.
107-
TODO: Abstract this using TTransformFunctionality in CGE. }
147+
Note: Not using TTransformFunctionality, as we need to differentiate
148+
name getting anyway. }
108149
if Joint is THAnimJointNode then
109150
begin
110151
JointCenter := THAnimJointNode(Joint).FdCenter;
@@ -131,8 +172,6 @@ function TSkeletonVisualize.JointVisualization(const Joint: TX3DNode): TTransfor
131172
TextGeometry.SetText([JointName]);
132173
TextShape.Geometry := TextGeometry;
133174

134-
FontStyle := TFontStyleNode.Create;
135-
FontStyle.Size := JointVisualizationSize;
136175
TextGeometry.FontStyle := FontStyle;
137176

138177
Result.AddChildren(SphereShape);
@@ -149,82 +188,108 @@ function TSkeletonVisualize.JointVisualization(const Joint: TX3DNode): TTransfor
149188
end;
150189

151190
procedure TSkeletonVisualize.VisualizeHumanoid(Node: TX3DNode);
152-
153-
{ Change shape's material to be transparent.
154-
This doesn't guarantee that material is changed, as we don't want to
155-
change the way shape is displayed (so we don't add Appearance
156-
or Material if they didn't exist etc.) }
157-
procedure MakeShapeTransparent(const Shape: TShapeNode);
158-
var
159-
Mat: TMaterialNode;
160-
begin
161-
if Shape.Appearance <> nil then
162-
begin
163-
if (Shape.Appearance.FdMaterial.Value <> nil) and
164-
(Shape.Appearance.FdMaterial.Value is TMaterialNode) then
165-
begin
166-
Mat := TMaterialNode(Shape.Appearance.FdMaterial.Value);
167-
if Mat.FdTransparency.Value = 0 then
168-
Mat.FdTransparency.Value := 0.5;
169-
end;
170-
end;
171-
end;
172-
173191
var
174192
HumanoidNode: THAnimHumanoidNode;
175-
Joint: THAnimJointNode;
176-
JointVis: TTransformNode;
177193
I: Integer;
178194
begin
179195
HumanoidNode := Node as THAnimHumanoidNode;
180-
Inc(HumanoidsProcessed);
196+
Inc(SkinsProcessed);
181197

182198
{ make all existing skin shapes transparent.
183199
This helps to see joints and their names through }
184200
for I := 0 to HumanoidNode.FdSkin.Count - 1 do
185201
if HumanoidNode.FdSkin[I] is TShapeNode then
186202
MakeShapeTransparent(TShapeNode(HumanoidNode.FdSkin[I]));
203+
end;
204+
205+
procedure TSkeletonVisualize.VisualizeHAnimJoint(Node: TX3DNode);
206+
var
207+
Joint: THAnimJointNode;
208+
JointVis: TTransformNode;
209+
begin
210+
Joint := Node as THAnimJointNode;
211+
212+
// avoid processing the same Joint many times
213+
if Joint.MetadataBoolean['JointVisualization'] then
214+
Exit;
215+
Joint.MetadataBoolean['JointVisualization'] := true;
216+
217+
if Joint.Humanoid = nil then
218+
Exit; // cannot visualize, joint not part of HAnimHumanoid
187219

188-
{ for each joint, add it's visualization }
189-
for I := 0 to HumanoidNode.FdJoints.Count - 1 do
190-
if HumanoidNode.FdJoints[I] is THAnimJointNode then
191-
begin
192-
Joint := THAnimJointNode(HumanoidNode.FdJoints[I]);
193-
JointVis := JointVisualization(Joint);
194-
HumanoidNode.FdSkin.Add(JointVis);
195-
end;
220+
JointVis := JointVisualization(Joint);
221+
Joint.Humanoid.FdSkin.Add(JointVis);
196222
end;
197223

198-
procedure TSkeletonVisualize.VisualizeAllHumanoids(const Node: TX3DNode);
224+
procedure TSkeletonVisualize.VisualizeAllHumanoids(const Scene: TCastleSceneCore);
199225
begin
200-
JointVisualizationBegin;
226+
JointVisualizationBegin(Scene);
201227
try
202-
Node.EnumerateNodes(THAnimHumanoidNode,
228+
Scene.RootNode.EnumerateNodes(THAnimHumanoidNode,
203229
{$ifdef FPC}@{$endif} VisualizeHumanoid, false);
204-
finally JointVisualizationEnd end;
205-
end;
206230

231+
{ Note: Don't depend on HumanoidNode.FdJoints list for this,
232+
as it became optional around X3D 4.1.
233+
So we need EnumerateNodes call for THAnimJointNode. }
234+
Scene.RootNode.EnumerateNodes(THAnimJointNode,
235+
{$ifdef FPC}@{$endif} VisualizeHAnimJoint, false);
236+
237+
if (SkinsProcessed <> 0) or (JointsProcessed <> 0) then
238+
Scene.ChangedAll;
239+
finally JointVisualizationEnd(Scene) end;
240+
end;
207241

208-
procedure TSkeletonVisualize.VisualizeTransformation(Node: TX3DNode);
242+
procedure TSkeletonVisualize.VisualizeTransformInSkin(Node: TX3DNode);
209243
var
210244
Joint, JointVis: TTransformNode;
211245
begin
246+
Joint := Node as TTransformNode;
247+
212248
// ignore adding debug visualization to debug visualization
213-
if IsPrefix('JointVisualization', Node.X3DName, false) then
249+
if IsPrefix('JointVisualization', Joint.X3DName, false) then
214250
Exit;
215251

216-
Joint := Node as TTransformNode;
252+
// avoid processing the same Joint many times, e.g. because it's in both Skin.skeleton and Skin.joints
253+
if Joint.MetadataBoolean['JointVisualization'] then
254+
Exit;
255+
Joint.MetadataBoolean['JointVisualization'] := true;
256+
217257
JointVis := JointVisualization(Joint);
218258
Joint.AddChildren(JointVis);
219259
end;
220260

221-
procedure TSkeletonVisualize.VisualizeAllTransformations(const Node: TX3DNode);
261+
procedure TSkeletonVisualize.VisualizeSkin(Node: TX3DNode);
262+
var
263+
SkinNode: TSkinNode;
264+
I: Integer;
265+
begin
266+
SkinNode := Node as TSkinNode;
267+
Inc(SkinsProcessed);
268+
269+
for I := 0 to SkinNode.FdShapes.Count - 1 do
270+
if SkinNode.FdShapes[I] is TShapeNode then
271+
MakeShapeTransparent(TShapeNode(SkinNode.FdShapes[I]));
272+
273+
for I := 0 to SkinNode.FdJoints.Count - 1 do
274+
if SkinNode.FdJoints[I] is TTransformNode then
275+
VisualizeTransformInSkin(SkinNode.FdJoints[I]);
276+
end;
277+
278+
procedure TSkeletonVisualize.VisualizeAllSkins(const Scene: TCastleSceneCore);
222279
begin
223-
JointVisualizationBegin;
280+
JointVisualizationBegin(Scene);
224281
try
225-
Node.EnumerateNodes(TTransformNode,
226-
{$ifdef FPC}@{$endif} VisualizeTransformation, false);
227-
finally JointVisualizationEnd end;
282+
Scene.RootNode.EnumerateNodes(TSkinNode,
283+
{$ifdef FPC}@{$endif} VisualizeSkin, false);
284+
285+
// No need to search for TTransformNode, VisualizeSkin will iterate over joints.
286+
// This is in contrast to VisualizeAllHumanoids, where we need to search
287+
// for THAnimJointNode.
288+
289+
// make sure to call ChangedAll
290+
if (SkinsProcessed <> 0) or (JointsProcessed <> 0) then
291+
Scene.ChangedAll;
292+
finally JointVisualizationEnd(Scene) end;
228293
end;
229294

230295
end.

0 commit comments

Comments
 (0)