TransformUnderMouse with Alpha pixels check for quad-based Transforms

Hi, All ! Happy new year!
I want to make precise Mouse picking of quad based Transforms - like TCastleImageTransform, or TCastleScene with 2d sprite, in this case TransformUnderMouse performs hit-test over whole quad (geometry) and not taking into account transparent pixels of loaded textures. I found those similar topics : Convenient way to determine exact point under mouse - #4 by michalis and Get all geometry under raycast? - #5 by bugan11 where some ideas presented , but no complete solution so far. So here is my attempt (and a screenshot with wrong work), somewhat compilation of ideas from above topics.
The issues I have still - raycast is a bit buggy, not hitting properly the desired polygon, esp. if you move (zoom out) away from it, and this is not the case for standard TransformUnderMouse (for this I wrote my solution and tried also one from the code in Bugan11 topic, claimed to be working, but incomplete). And ofc. the pixel check is not working (the one I wrote in my method) as well.
Maybe somebody will have ideas, I also posted this in discord, but seems less people are checking it than the forum.
my-new-project.zip (25.1 KB)

2 Likes

Hi, I’ll take a look and answer around the weekend.

I’m indeed offline on Discord from quite some time, sorry about it – I got busy lately working on a big new feature ( Web Target | Manual | Castle Game Engine ) and I kind of went offline for a long time :slight_smile: I will catch up with Discord eventually. And I will answer here when I have time to carefully read and investigate.

Happy 2025!

1 Like

Just a note that this topic is not forgotten, I have this in TODO, to read carefully and answer something useful :slight_smile:

1 Like

Thanks , @michalis !
Maybe you could give some hints and I could try by myself first.
It is even a bit hard to debug this to understand what’s going on, that’s why I’m blind without clues for now…

It seems that I did it !
I rechecked the comments in the Castle sources and found that I need PreciseCollisions to be set, which makes proper Triangle hit within raycast. I also tried various Coords translations to get proper Texture coords out of Triangle+HitPoint (and it seems that comments in the castle code are misleading in some cases). Ofc I fixed couple of my own errors in this example.
So here it is, please, guys, check it, if it fulfills the task and common sense.
@michalis, maybe you could suggest improvements or simplifications (for example more solid way to get texture node from hit transform).



As seen on screenshots, red dot marks a mouse pointer and detects which transform is under mouse, correctly filtering alpha pixels, and highlighting the wireframe of the transform with red. And it works on any zoom, on any position (click and drag in example) and even overlapping transforms.
alpha_mouse_hittest_working.zip (25.2 KB)

2 Likes

Indeed, you need PreciseCollisions = true right now to make collision queries like Viewport.TransformUnderMouse precise.

More info: We have a plan for new “hit API” (it will be TCastleViewport.PointingDeviceHit) to use physics by default (which means it will ignore PreciseCollisions), and then at some point deprecate PreciseCollisions (see Roadmap | Manual | Castle Game Engine ). At this (future) point we should only use physics engine to detect collisions, and it should account for whatever colliders (like TCastleMeshCollider) or not are present. This will be done slowly naturally, i.e. first we will deprecate old things (TransformUnderMouse and PreciseCollisions) but keep them working for some releases, to give time everyone to upgrade, before we actually remove the “old ways”.

Right now, while we use physics engine (Kraft), we also have our own implementation of collision detection used by some routines, like the TransformUnderMouse. A scene collides “as its bounding box” when PreciseCollisions = false, and as a precise mesh (set of polygons) when PreciseCollisions = true. See “13. Old system for collisions and gravity” on Physics | Manual | Castle Game Engine .

I tested your application in case I can provide any suggestions, esp. looking at TViewMain.GetSceneUnderMouseWithPixelTest, added some fixes to CGE comments, and added TAbstractTextureNode.Color to make it all easier.

Details:

  • You don’t need to track FMousePos in TViewMain.Motion yourself. Instead, just access Container.MousePosition.

  • To debug pointers, instead of Integer(Pointer(...)).ToString you can use ready PointerToStr(...) from CastleStringUtils.

  • For “production usage”, I would add some “sanity counter” to the repeat .. until false loop, to break after e.g. 1000 iterations. Reasons: We’re checking collisions using floating-point numbers and relying that opetations like LRayNode.Point + LDirection.AdjustToLength(SingleEpsilon) reliably move the point “beyond” the first hit. These calculations may be imprecise in some edge-cases (e.g. when hugely large or incredibly small distances are involved), in which case it’s good to be sure we don’t “hang indefinitely” the loop.

  • As for your comment:

    // according to comments in the source code, ITexCoord2D expects world coordinates, but test shows it takes Local
    LTexCoords := LRayNode.Triangle^.ITexCoord2D(LHitLocalCoords);
    

    I see that indeed our ITexCoord / ITexCoord2D comments have been wrong. They said “For a given position (in world coordinates)” but the correct is “For a given position (in the local coordinates of the containing TCastleScene)”.

    ( The mistake was due to the fact that terminology in TTriangle was confusing, we had Local which is “coordinate space of the shape” and World which is “coordinate space of TCastleScene”. )

    I committed fixes to various comments in TTriangle to reflect this. And I renamed TTriangle.World → TTriangle.SceneSpace . See TTriangle.World is actually "in TCastleScene coordinate space", renam… · castle-engine/castle-engine@b703735 · GitHub .

  • As for

    // according to comments in the source code, LRayNode.Point is in local coordinates, but the test looks it is in world
    LHitLocalCoords := LRay.Transform.WorldToLocal(LRayNode.Point);
    

    Oh, indeed, another wrong comment we had. This was wrong comment in CGE:

    { Position, in local coordinate system of this 3D object,
      of the picked 3D point.
      ... }
    Point: TVector3;
    

    We actually had a correct information a bit above:

    { Information about ray collision with a single 3D object.
      Everything (Point, RayOrigin, RayDirection) is expressed
      in the parent coordinate system of this TCastleTransform (in @link(Item)). }
    TRayCollisionNode = record
    

    So, strictly speaking, Point is not in world coordinates, it’s also not in LRayNode.Item coordinates. It’s in coordinates of the LRayNode.Item parent (which can be usually accessed as just LRayNode.Item.Parent, if you don’t place the same TCastleTransform multiple times in the viewport – the latter is allowed but complicates some logic).

    Fixed in CGE commit Fix comment about TRayCollisionNode.Point coordinate system · castle-engine/castle-engine@d47705c · GitHub .

    This also means your

    LHitLocalCoords := LRay.Transform.WorldToLocal(LRayNode.Point)
    

    can be written, to be always correct, as

    LHitLocalCoords := LRay.Transform.InverseTransform.MultPoint(LRayNode.Point)
    
  • Accessing the texture easier, without creating the new TImageTextureNode:

    TX3DGraphTraverseState.MainTexture contains the reference to the texture node that was used to render this shape. You can get TX3DGraphTraverseState from the triangle, like LRayNode.Triangle^.State, if you use the CastleShapes unit.

    So just take LRayNode.Triangle^.State.MainTexture.

    Note: State is never nil, no need to check.

    MainTexture may be nil, may be an arbitrary TAbstractTextureNode instance. In the common case (without multi-texturing) it’s just TImageTextureNode, though you can easily write code to handle any TAbstractTexture2DNode descendant (this in pratice means TImageTextureNode and TPixelTextureNode – the latter is seldom, but occurs e.g. when you have glTF with embedded data or X3D file with PixelTexture).

    You can access colors using TCastleImage.Colors, this is a bit easier than your code.

    You also cannot easily handle any TEncodedImage – TEncodedImage may be TGPUCompressedImage or TCastleImage. The TGPUCompressedImage would be hard to handle, as it means data is compressed using algorithm like S3TC, ETC… so you’d need to implement decompression, at least for some algorithms. ( When rendering, GPU does this decompression. ) I do recommend to just not do this, my code below just makes a warning when it encounters TEncodedImage that is not TCastleImage (so, in practice, it is for sure TGPUCompressedImage).

  • Oh, minor note, I see you have function IIF(ACond: Boolean; IfTrue, IfFalse: string): string;. Note that there’s IfThen overload in StrUtils doing the same :slight_smile:

Overall, I got this code for TViewMain.GetSceneUnderMouseWithPixelTest:

Add to uses clause CastleStringUtils, CastleLog. The above implements my suggestions above except the “sanity counter”.

Version 2.0

It is also possible to account for filtering (nearest, bilinear) and repeat/clamp modes. See how GetDiffuseTexture is doing it: castle-engine/src/scene/castleraytracer.pas at d47705c0cea96d32746fc3943cec5034f1f4fb98 · castle-engine/castle-engine · GitHub .

Hmmm, I decided to make it better and easier. In the latest CGE commit, I exposed a new method TAbstractTextureNode.Color that makes the last part of your task a bit easier, see TAbstractTextureNode.Color to query texture contents from Pascal · castle-engine/castle-engine@5ebe44c · GitHub . The GetDiffuseTexture from CastleRayTracer is removed (it was internal there anyway, and now ray-tracer calls TAbstractTextureNode.Color).

So you can now write:

LMainTexture := LRayNode.Triangle^.State.MainTexture;
if LMainTexture <> nil then
begin
  LPixelColor := LMainTexture.Color(LTexCoords);
  Result := LPixelColor[3] < 0.5; // Consider pixel transparent if alpha is less than 0.5
end;

In the end, this is the code for TViewMain.GetSceneUnderMouseWithPixelTest, that requires the very latest engine versio to work (just committed, it will be available in Download | Castle Game Engine in a few hours), and accounts for all my suggestions (except the “sanity counter”!):

Have fun! And thank you for all the feedback – we did improve resulting CGE thanks to this :slight_smile:

2 Likes

THanks, @michalis ! What an expanded response from you, it is very informative and helpful !
I will pull the CGE sources (I rarely use full snapshot package) and your gist and play with all the details to see the improvements you’ve brought.
I’m glad, that this case introduced points to increase CGE clarity and richness, thanks for sorting this out and investing your time, I know you have lots of other todos for engine (webtarget is a killer feature for sure :metal:)
And I’m happy that now I have distilled solution which I would soon use in my game :innocent:
Thank you for all this commitment !

1 Like

Additional correction (I edited my post above to account for it):

It the “version 2.0” code, I made a mistake yesterday, not accounting for the case that LRayNode.Triangle^.State.MainTexture may be nil. Yesterday’s code would crash on shapes that don’t use any texture.

Fixed now. I edited both the above post and the gist with version 2.0 to account for it. One can see the diff in gist history .

1 Like

Ok, so I transferred the code from test project to my main one and found one detail in discussed solution, that’s need to be covered I think.
The fact is, in the test project all quad-objects are placed in editor without any hierarchy, while in the game I use hierarchy and design-files, in this case the calls to just LRay.Info(LRayNode) are giving not the full picture, so I decided to add cycling through LRay.Count, which helped me to address transient objects (namely the quads with sprites) inside TCastleTransformDesign which hides them.
So I added this test case in the test project and attaching it (ofc it contains all suggestions from @michalis )
In my game I also use CastleImageTransforms as field cells, bc they have convenient Color property, and they are “properly” hittested in my case along with the sprites above them, bc I use rectangular images and for me it works ok, but I also noticed that in test project TCastleImageTransform with non-rect textrure (I changed 1 of scenes to image) is not behaving the same way as TCastleScene which we were testing before. If you try to test, you will see, that LRay.Count is reported as 3 for imageTransform (same as for scenes), but it didn’t come to PC or TxC debug line (which stands for PixelCheck and TextureCheck), and that means LRayNode.Triangle is not accessible for this kind of workflow. The reason could be that TCastleImageTransform is not having PreciseCollisions property which is needed.
So maybe some additional adjustment for TCastleImageTransform is needed for alphapixel check to be performed (or one could use alternative TCastleScene for it).

And one more small question - is the code optimal in the sense of reducing redundant calculations, and is properly gradually discards unneeded steps with the checks? Or could it be done better performance-wise?

Thanks !
alpha_mouse_hittest_working_v2.zip (25.3 KB)

The TRayCollision.Info indeed returns only part of the information, it returns (from this comment):

the first TRayCollisionNode on the list
except it doesn’t return TRayCollisionNode that relates to
internal TCastleTransform (that has csTransient
flag is ComponentState).

I found that this is always, or almost always, all that you need, that’s why we advise to not iterate over this list yourself ( castle-engine/src/transform/castletransform_initial_types.inc at 8ce77b04e4420cfd4b9032aff43e7e883eaa8824 · castle-engine/castle-engine · GitHub ).

I’ll investigate your testcase to see why it was necessary, thank you – I will want to address it as I design the promised “new hit API”, TCastleViewport.PointingDeviceHit, mentioned in one of the posts above. I will want to account for this.

Ah, indeed. Internally the TCastleImageTransform is actually using the TCastleScene, but indeed there’s no TCastleImageTransform.PreciseCollisions so there’s no information about the exact triangle hit and position within that triangle.

Hm. I decided to just add it :slight_smile: Done in Add TCastleImageTransform.PreciseCollisions · castle-engine/castle-engine@d8ed28d · GitHub .

It will be available in downloads soon, once the commit titled " Add TCastleImageTransform.PreciseCollisions" is no longer visible on this page.

1 Like