Decals thickness and following mesh

Godot Version

4.7.1

Question

I have noticed the behavior of decals on KayKit assets a bit strange when attached to moving mesh, on doors.

First instance worked ok if I left Y size 2.0m but this caused both side of walls have decal visible which wasn’t desired.

When I reduced size to 0.2m to prevent this it behave differently, should reuse remote transform or is there different solution ?

Code is same like in Fireballs - is there way to make burned/smoked area? - #8 by gertkeno but I just repasted it here for both fireball itself and decal

Decal.gd

extends Node3D

static var decals_amount: int = 0
const MAX_DECALS_AMOUNT: int = 10

func _ready() -> void:
	decals_amount += 1
	if decals_amount > MAX_DECALS_AMOUNT:
		var all_decals := get_tree().get_nodes_in_group("Decals")
		all_decals.pop_at(0).queue_free()

func _exit_tree() -> void:
	decals_amount -= 1

func _on_timer_timeout() -> void:
	queue_free()

Fireball.gd

extends RayCast3D

@export var speed := 50.0
@export var explosion_scene: PackedScene
@export var decal_scene: PackedScene	
@onready var audio_stream_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D

func _ready() -> void:
	audio_stream_player_3d.play()

func _physics_process(delta: float) -> void:
	position += global_basis * Vector3.FORWARD * delta * speed
	target_position = Vector3.FORWARD * delta * speed
	force_raycast_update()
	var collider = get_collider()
	if is_colliding():
		if collider.has_method("take_damage"):
			collider.take_damage(5.0)
		audio_stream_player_3d.stop()
		var point := get_collision_point()
		var normal := get_collision_normal() 
		var rotate_up := Quaternion(Vector3.UP, normal) # gertenko magic

		var explosion := explosion_scene.instantiate()
		var decal := decal_scene.instantiate()
		explosion.position = point
		decal.position = point
		decal.quaternion = rotate_up # gertenko magic

		get_tree().current_scene.add_child(explosion)
		get_tree().current_scene.add_child(decal)
		explosion.blast()
		queue_free()
			
func cleanup() -> void:
	queue_free()

launcher.gd

extends Node3D

const PROJECTILE = preload("res://Projectile/projectile.scn")

@onready var timer: Timer = $Timer
@onready var animation_tree: AnimationTree = $"../../AnimationTree"



func _physics_process(delta: float) -> void:
	if timer.is_stopped():
		if Input.is_action_pressed("click"):
			animation_tree.set("parameters/OneShotAttack/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
			timer.start(0.5)
			var attack = PROJECTILE.instantiate() as RayCast3D
			add_child(attack)
			attack.global_transform = global_transform

Decal scene_tree and inspector properties :

Projectile scene_tree

Video demo of issue

For the door, the decal gets added to current_scene, so it’s parented to the world and might stay frozen at the impact point while the door swings away. Maybe parenting the decal would work:

var decal := decal_scene.instantiate()
collider.add_child(decal)
decal.global_position = point
decal.quaternion = rotate_up

Set the global position after add_child so it lands on the impact point in the door’s space. From then on the decal inherits the door’s movement for free, no RemoteTransform needed. RemoteTransform would also work but it’s an extra node and an extra thing to clean up for the same result.

I tried global_position and add as child before but it didn’t worked with low thickness.

I’ll try tomorrow later it and post result if this solved it or not.

I think I see why the snippet fell apart at low thickness. After add_child the decal lives inside the door’s transform, and decal.quaternion sets the local rotation. rotate_up was computed in world space, so if the door node has any rotation of its own, the projection box ends up tilted by that same amount. At Y 2.0 the box is deep enough that it still crosses the surface even when tilted, but at 0.2 a tilted box can miss the wall entirely, which matches what you’re describing.

Try setting the whole transform in global space after parenting:

var decal := decal_scene.instantiate()
collider.add_child(decal)
decal.global_transform = Transform3D(Basis(rotate_up), point)

Two other things worth checking. If the door mesh has a scale on it (KayKit assets sometimes do), a child decal inherits that scale and the thin projection box gets squashed or skewed, so parent to a node in the door that has no scale if you can. And if you’d rather keep a bigger Y size for tolerance without the decal showing on the far side of the wall, push the decal center out along the normal so most of the depth sits in front of the surface:

decal.global_position = point + normal * (decal.size.y * 0.5 - 0.05)

That keeps around 5cm reaching into the wall while the rest of the depth faces outward, so a 0.5m Y size won’t reach the other side anymore.

So I made update but don’t see a much of difference.

func _physics_process(delta: float) -> void:
	position += global_basis * Vector3.FORWARD * delta * speed
	target_position = Vector3.FORWARD * delta * speed
	force_raycast_update()
	var collider = get_collider()
	if is_colliding():
		if collider.has_method("take_damage"):
			collider.take_damage(5.0)
		audio_stream_player_3d.stop()
		var point := get_collision_point()
		var normal := get_collision_normal() 
		var rotate_up := Quaternion(Vector3.UP, normal) # gertenko magic

		var explosion := explosion_scene.instantiate()
		var decal := decal_scene.instantiate()
		collider.add_child(decal)
		explosion.position = point
		#decal.position = point
		decal.global_transform = Transform3D(Basis(rotate_up), point)
		#decal.quaternion = rotate_up # gertenko magic
		get_tree().current_scene.add_child(explosion)
		explosion.blast()
		queue_free()
			

In sense of scale I changed door scene, but it all defaults.
Maybe it should be attached as child of wall_doorway_door node to copy rotation from animation?

Current results

For just to be sure what projectile hits, I have checked it it’s Doorway which should have rotation.

and in remote it’s confirmed the decal is child there.

Your tree looks a little bit crooked. Do I understand it correct, that the AnimationPlayer is animating only the mesh (wall_doorway_door)? And the StaticBody3D is a child of this animated door mesh? And the Decal is a child of this StaticBody? That all feels wrong.
A StaticBody shouldn’t be animated, which it is in your case.
If you want forces to move the door, you could use a RidgitBody as the parent. If you want to animate it with the AnimationPlayer, it should be an AnimatableBody3D as the parent.
Under this parent node place the mesh and CollisionShape and the Decal as children.
EDIT: and animate the parent (the AnimatableBody3d), so the mesh and decal will follow the animation.

Yes you understand it correctly, didn’t know that makes some difference.

Is this correct ?

Why is “Doors” child of a mesh?
I would keep trees as flat as possible. Just from the names I would build it

  • Walls (Node3D)
    – WallDoorway01 (StaticBody3D)
    — CollisionShape (for the doorway)
    — wall_doorway (MeshInstance3D, as sibling of Door)
    — Door01(AnimatableBody3D)
    ---- CollisionShape
    ---- wall_doorway_door (MeshInstance)
    ---- Decal

Then animate the Door01.

And why is it plural (Doors)? Isn’t it only one entity? You can have multiple “Door” maybe in a Node3D “Doors”, but than it’s not a child of a specific doorway. Is it Important that the Door is a child of the doorway? Just to be sure that there will only be the one Door that will get animated.

I kept it as this was original structure from Kaykit asset .

I just saw two nodes under, so named it as Doors, but yes technically it’s only one entity.

Ok here is updated scene

Main question now

How to animate Door_AnimatedBody from Pivot of Door_Mesh?

  • short demo what I mean

edit: I got it, there is offset I just apply it to Door_AnimatedBody instead of mesh and collision.

I would place the wall_mesh under the Wall_StaticBody. The Wall_StaticBody is the object, the mesh is just the visual representation of that object in the world.I would handle meshes just as the visual attachment of an object, it’s not the object itself.

Should it works now ?

Updated but not difference

Current code is

extends RayCast3D

@export var speed := 50.0
@export var explosion_scene: PackedScene
@export var decal_scene: PackedScene	
@onready var audio_stream_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D

func _ready() -> void:
	audio_stream_player_3d.play()

func _physics_process(delta: float) -> void:
	position += global_basis * Vector3.FORWARD * delta * speed
	target_position = Vector3.FORWARD * delta * speed
	force_raycast_update()
	var collider = get_collider()
	if is_colliding():
		if collider.has_method("take_damage"):
			collider.take_damage(5.0)
		audio_stream_player_3d.stop()
		var point := get_collision_point()
		var normal := get_collision_normal() 
		var rotate_up := Quaternion(Vector3.UP, normal) # gertenko magic

		var explosion := explosion_scene.instantiate()
		var decal := decal_scene.instantiate()
		collider.add_child(decal)
		print(collider)
		explosion.position = point
		#decal.position = point
		decal.global_transform = Transform3D(Basis(rotate_up), point)
		#decal.quaternion = rotate_up # gertenko magic
		get_tree().current_scene.add_child(explosion)
		explosion.blast()
		queue_free()
			
func cleanup() -> void:
	queue_free()

Haven’t analyzed the code. The tree looks better. Not sure what all the Node3Ds are below DoorAnimatedBody.
I would switch Debug → Visible Collision Shapes to on. Just to ensure that all Collisions are correct.
I would rename every node that get instantiated to avoid Nodes with names like “@Node3D@49”, before attaching it as a child.
You could pause (with expanded Remote view) the runtime and analyze the orientation of the decal in the world. (Selecting it in the remote view). You have to switch the “Input” to 3D, after the pause, it’s above the game view.
You have to understand the state of your world at that moment.

Decals.

That’s what I did, frame by frame.

To me looks like it getting wrong copy of rotation, maybe I should not copy it as it’s child of that node.

How do I do this ? it usually like first instance got name of scene and rest is some random one’s.

edit: I found mistake, I had checked top_level.

As tried it place it manually, it looks like it needs some offset from normal .

But also now when goes with doors it projects decal on wall. Which looks an off.

As an example:

var decal_count: int = 0
...
		var decal := decal_scene.instantiate()
		decal_count += 1
		decal.name = "Decal_%d" %decal_count
		collider.add_child(decal)

That’s a tricky situation, if the decal slices the wall it will draw there. Every mesh has a VisualInstance3D, there you can define layers. In decal there are CullMasks. Default the decal is drawn on all layers, you coud divide the wall and door mehes to different layer, and a decal can draw on only one of them. But you would need a more complex Decal logic to detect if it should draw on the wall layer or on the door layer.

So here is clearest demo what I mean by offset it’s relative z axis to door, but also it cause projection once on wall and once not which looks a bit odd.

Maybe duplication of decal, but how would I even check it ?

  • would I need to check decal itself if collide with some other instance to add child there ?

Yep, the effect is as expected.
The Ray gives the object it collided with, there you could detect doors, maybe they are all in a group. A group like “moveable_objects” or something like that. They are all on one layer other than 1 and the rest of the world is on layer 1.
If the collided object is in the group, set the CullMask to that layer, else let it at 1.
Alternative:
The moveable objects on one layer and the world on another. I’m sure you can get the VisualInstance3D layer from the collided object. And set the cull mask of the decal to that layer before adding it to the world.

I have used cull_mask for water to avoid decal for it.

Will need to do some research for methods what’s available in engine.

What about offset?

How should I approach it?

He mentioned that bit of code, but I guess making decal size bigger on Y would make issue with wall more obvious unless I do this visual instance split.

In the example the decal is set to the point (the hit point of the ray), than adds 1/2 of the size in Y to moves the decal in the direction of the hit normal, as the origin of the decal is in the center, so the decal will sit with it’s outer face on the hit surface, than in the example it’s moved 0.05m into the wall.
I’m not sure that this is the best solution, as a decal can have a fade out, to fade out over distance to the surface.
How far the decal reaches is defined by it’s Y size. But you have already a relative flat decal. It’s still visible on the other side of the wall/door?
IMHO controlling it by the size is the better solution, as you did.