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)
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 I will catch up with Discord eventually. And I will answer here when I have time to carefully read and investigate.
Happy 2025!
Just a note that this topic is not forgotten, I have this in TODO, to read carefully and answer something useful
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)
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
inTViewMain.Motion
yourself. Instead, just accessContainer.MousePosition
. -
To debug pointers, instead of
Integer(Pointer(...)).ToString
you can use readyPointerToStr(...)
fromCastleStringUtils
. -
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 likeLRayNode.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 hadLocal
which is “coordinate space of the shape” andWorld
which is “coordinate space of TCastleScene”. )I committed fixes to various comments in
TTriangle
to reflect this. And I renamedTTriangle.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 inLRayNode.Item
coordinates. It’s in coordinates of theLRayNode.Item
parent (which can be usually accessed as justLRayNode.Item.Parent
, if you don’t place the sameTCastleTransform
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 getTX3DGraphTraverseState
from the triangle, likeLRayNode.Triangle^.State
, if you use theCastleShapes
unit.So just take
LRayNode.Triangle^.State.MainTexture
.Note:
State
is nevernil
, no need to check.MainTexture
may benil
, may be an arbitraryTAbstractTextureNode
instance. In the common case (without multi-texturing) it’s justTImageTextureNode
, though you can easily write code to handle anyTAbstractTexture2DNode
descendant (this in pratice meansTImageTextureNode
andTPixelTextureNode
– the latter is seldom, but occurs e.g. when you have glTF with embedded data or X3D file withPixelTexture
).You can access colors using
TCastleImage.Colors
, this is a bit easier than your code.You also cannot easily handle any
TEncodedImage
–TEncodedImage
may beTGPUCompressedImage
orTCastleImage
. TheTGPUCompressedImage
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 encountersTEncodedImage
that is notTCastleImage
(so, in practice, it is for sureTGPUCompressedImage
). -
Oh, minor note, I see you have
function IIF(ACond: Boolean; IfTrue, IfFalse: string): string;
. Note that there’sIfThen
overload inStrUtils
doing the same
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
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 )
And I’m happy that now I have distilled solution which I would soon use in my game
Thank you for all this commitment !
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 .
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 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.