[solved] Making Models semi-transparent. Setting baseColorFactor+alphaMode (in GLTF material) via code

I want to make my models conditionally semi-transparent, so I could see other objects through them.
I split this big task into two parts, the second part should be trivial, esp. with SphereCast introduced, I will cast a sphere from camera and see if it hits continuously desired objects, and initiate the material transparency change.

And for transparency change I have a question on how I might approach this:
example from Heroes 5


and here is what I made via editing GLTF (based on reference docs glTF™ 2.0 Specification):

"pbrMetallicRoughness": {
        "baseColorFactor": [
          1,
          1,
          1,
          0.6 // here was 1
        ],        
      },
      "alphaMode": "BLEND" // here was OPAQUE
    },

this gave me this good result:


However, for models in general I need to change all the materials (in this particular model 3 materials with 3 textures, and also this is a complex model combined from some separate parts), so it would be a burden to change it all recursively, and also I’m not sure if it represented in code in x3d/classes properties fully.
That’s what I want to ask, how could I approach this ? If only recursively, then I would go for it, but it’s always good to have easier alternative :slight_smile:

Ability to render models as ‘ghosts’ would be nice, indeed. I’m not CGE expert, and with my current knowledge of the engine I’d go for these:

  1. recursive, as you propose, because it’s fast enough and easy enough. It’ll also work on every model that you throw into the game. But… if you have other models using the same materials, I guess all of them would change the appearance. That would mean you can’t use cache, or reference (clones). I may be wrong, though.

  2. make the materials semi-transparent from design, and then - from code - change the RenderingOptions.Blending. It should be false for all, and true only for the models you want. Which would be simple & fast.

  3. Make custom property “ghost: boolean” and hack the renderer to apply transparency on-the-go… but even I’m not that crazy. However some option like this could be in the engine, as ‘ghosts’ are not uncommon (e.g. in construction games it’s heavily used to project the hologram before placing / modifying objects).

I’d go for 2. But I only quickly tested it just right now.

1 Like

A picture showing the option 2. Inside Blender I used simple cube with a texture that has alpha channel. In the code, I generated a few of these with 50% of them having Blending := false; and another 50% Blending := true

for i...
    Model.RenderOptions.Blending := ((i mod 2) = 0);
    Model.RenderOptions.BlendingDestinationFactor := bdDstAlpha;
    Model.RenderOptions.BlendingSourceFactor := bsDstAlpha;

1 Like

All the material parameters that we take into account when rendering, from glTF or X3D or any other format, are exposed in CGE and you can change it by finding and tweaking the TPhysicalMaterialNode properties in your models.

Use Scene.RootNode.EnumerateNodes with a callback to find all occurrences of the given node type in your model. To change AlphaMode, you would do sthg like this:

type
  TMyView = ...
  private
    DesiredAlphaModeOfPhysicalMaterial: TAlphaMode;
    procedure SetAlphaModeOfPhysicalMaterial(Node: TX3DNode);
    procedure SetAlphaModeOfScene(const Scene: TCastleScene; const DesiredAlphaMode: TAlphaMode);
  end;

procedure TMyView.SetAlphaModeOfScene(const Scene: TCastleScene; const DesiredAlphaMode: TAlphaMode);
begin
  DesiredAlphaModeOfPhysicalMaterial := DesiredAlphaMode;
  if Scene.RootNode <> nil then // RootNode can be nil if nothing loaded
    Scene.RootNode.EnumerateNodes(TPhysicalMaterialNode,
      {$ifdef FPC}@{$endif} SetAlphaModeOfPhysicalMaterial, false);
end;

procedure TMyView.SetAlphaModeOfPhysicalMaterial(Node: TX3DNode);
var
  PhysicalMaterialNode: TPhysicalMaterialNode;
begin
  // safe to typecast, because EnumerateNodes only calls this for TPhysicalMaterialNode
  PhysicalMaterialNode := Node as TPhysicalMaterialNode;

  // tweak the material properties as you wish.
  // As a demo, we change the AlphaMode of the material to the desired value.
  PhysicalMaterialNode.AlphaMode := DesiredAlphaModeOfPhysicalMaterial;
end;

(above is untested, you can grep our examples/ for various usage of EnumerateNodes( and TPhysicalMaterialNode though we don’t have exact example of tweaking AlphaMode).

And now you can use this like SetAlphaModeOfScene(MyScene, amBlend) and SetAlphaModeOfScene(MyScene, amOpaque).

An alternative solution is to just change MyScene.RenderOptions.Blending. When it’s false, we treat all materials as opaque. It’s faster and easier, but it’s also a very specific solution to the exact case you describe (toggle opaque / blending). The solution above, with EnumerateNodes is fully generic so you can tweak any node property to anything this way :slight_smile:

Our cache (like TCastleScene.Cache and some automatic GPU resource caching) does not make materials “shared”. So, no, material parameters of TPhysicalMaterialNode are not shared between TCastleScene instances, and TCastleScene.Cache or other mechanism doesn’t change it. Maybe the resulting material GPU resources are shared (like shader is shared now if hash of shader logic is equal), but this is an implementation detail that you don’t need to worry about.

Only if you use TCastleTransformReference than 100% of the scene is shared, so including material nodes. IOW, we always very clearly document if some CGE mechanism causes “sharing” meaning that changing 1 thing changes many things :slight_smile:

I show above the “recursive” approach, this is basically what Scene.RootNode.EnumerateNodes does.

Indeed, this is also an option.

I agree it’s a common use-case, but I don’t think we need anything special for this. And every design may need a different definition “how goes a ghost exactly look like”. Better to leave this to each application, we give you tools – RenderOptions.Blending, EnumerateNodes, more options to tweak look by Shader Effects (Compositing Shaders) | Castle Game Engine etc.

2 Likes

Guys, @michalis, @DiggiDoggi , thanks for the examples !
I checked the process in Editor and here I see the things I anticipated :frowning:


here we see that changing the blending property of sub-model changes transparency of the material, but this material is the one I edited manually in GLTF file (remember there are 3 materials in this file, for each texture, wood on the edges, for example, which is not transparent), so I would need to edit all models and all materials to have baseColorFactor with value that I need, and for it I don’t think any property is exposed to be changed via code, or ?
and also here is the whole model:

setting blending for “root” model doesn’t affect the blending property of its children, so for this I would need to do again some special processing to traverse all children.
I don’t mind of doing these changes manually, I would find a way, just publishing my results, and maybe you will have some ideas or outcome

I see your point. The blending solution is only good when you have few shapes inside one model, instead of few separate models. Then you could (considering the scale) have just 1 bigger texture baked instead of 3. But it wouldn’t wok if you need the 3 parts separate.

So I guess enumerating remains the only option? Unless you want to consolidate the models. I will try with shaders. I need similar “ghost” stuff for me myself anyway, but can’t use transparency for it. And if I succeed I’ll get you updated.

1 Like

Indeed in my/our answers we focused on a solution for a single TCastleScene. There is no special solution that would change material properties of many scenes (unless they are the same scenes using TCastleTransformReference, but that’s not your case). Simply enumerate scenes. It’s easy :slight_smile:

procedure DoSomething;

  procedure DoSomethingRecursively(const T: TCastleTransform);
  var
    Child: TCastleTransform;
  begin
    for Child in T do
    begin
      if Child is TCastleScene then
        // just an example, referring to SetAlphaModeOfScene from my previous post
        SetAlphaModeOfScene(TCastleScene(Child), ...); 
      DoSomethingRecursively(Child);
    end;
  end;

begin
  DoSomethingRecursively(MyViewport.Items);
end;

Above I start from MyViewport.Items, so I do something on all scenes in the viewport. You could of course also start from any specific TCastleTransform.

If you want to iterate over all transformations that have a certain “trait”, our behaviors are a great way to “tag” your transformations. Then you can use SomeParent.FindAllBehaviors(TMyBehavior) to find all transforms with TMyBehavior. For every behavior, you can look at its parent (TCastleBehavior). E.g.

var
  AllHighlightable: TCastleBehaviorList;
  B: TCastleBehavior;
begin
  AllHighlightable := MyViewport.Items.FindAllBehaviors(THighlightableBehavior);
  try
    for B in AllHighlightable do
    begin
      Assert(B is THighlightableBehavior);
      // casting, assuming we only added THighlightableBehavior to TCastleScene
      Scene := B.Parent as TCastleScene;
      // again using SetAlphaModeOfScene from my previous post as just an example
      SetAlphaModeOfScene(Scene, ...); 
    end;
  finally FreeAndNil(AllHighlightable) end;
end;

In bad chess tutorial I discusssed a bit more “using behaviors to tag your TCastleTransform instances”.

1 Like

So, Thanks, guys, I ended up with a mix of the code presented here in the topic (I will post if needed by someone) and it works smoothly, and what is important - no manual adjustments needed, neither in gltf files, nor design files.
For now I made a test solution with mouse hover:


But I’m gonna work on SphereCasts onto warriors and check if sphere hits the model in front (before) of warrior

2 Likes

Here is the video of final result in game - I used raycasts (sphere casts are not suitable for me as they require colliders/physics, which I don’t use), and the code of method that I used, with it I have an array of Transforms of desired type hit by ray.

function CView.GetTransformsFromRayByClass(AViewport: TCastleViewport; 
  AClass: TCastleTransformClass; AOrigin, ADirection: TVector3): TArray<TCastleTransform>;
var
  LRay: TRayCollision;
  LRayNode: TRayCollisionNode;
  LCounter, LChildIndex: Integer;
begin
  Result := nil;
  LCounter := 0;
  repeat
    LRay := AViewport.Items.WorldRay(AOrigin, ADirection);
    if not Assigned(LRay) then
      Exit;
    for LChildIndex := 0 to LRay.Count - 1 do
    begin
      LRayNode := LRay.Items[LChildIndex];
      if LRayNode.Item is AClass then
      begin
        SetLength(Result, Length(Result) + 1);
        Result[High(Result)] := LRayNode.Item;
      end;
    end; 
    AOrigin := LRayNode.Point + ADirection.AdjustToLength(0.2);
  until PostInc(LCounter) > 30;
end;

for real use case you might need to adjust Counter limit and ray origin adjustment scalar (0.2 in this code), and I’m calling it so:

Buildings := GetTransformsFromRayByClass(Viewport1, TBuilding, 
      GameUnit.WorldTranslation, Camera1.Translation - GameUnit.WorldTranslation);

the code for Transparency change:

procedure CView.SetTransparencyOfPhysicalMaterial(Node: TX3DNode);
begin
  (Node as TPhysicalMaterialNode).Transparency := IFF(FIsBlendOn, 0.5, 1.0);
end;

procedure CView.SetAlphaModeOfAppearance(Node: TX3DNode);
begin
  (Node as TAppearanceNode).AlphaMode := IfThen<TAlphaMode>(FIsBlendOn, amBlend, amOpaque);
end;

procedure CView.SetAlphaModeOfScene(const Scene: TCastleScene; AIsBlendOn: Boolean);
var
  LChild: TCastleTransform;
begin
  FIsBlendOn := AIsBlendOn; 
  for LChild in Scene do
    if (LChild is TCastleScene) and Assigned(TCastleScene(LChild).RootNode) then
      with TCastleScene(LChild).RootNode do
      begin
        EnumerateNodes(TAppearanceNode, {$ifdef FPC_OBJFPC}@{$endif} SetAlphaModeOfAppearance, False);
        EnumerateNodes(TPhysicalMaterialNode, {$ifdef FPC_OBJFPC}@{$endif} SetTransparencyOfPhysicalMaterial, False);
      end;
end;

2 Likes