Decals thickness and following mesh

Currently not size y size 0.2m.

I would image moving it back in direction where it been launched by 0.18m but this will depends if you shoot flat floor it doesn’t make sense , but for thinks like those planks on door it would make sense because size would make it visible on both sides.

Did you set the decal’s “Top Level” on? If so, turn that off

I turned it off already, it was another thing after arrows​:+1:.

Decals have the normal threshold property. Adjust that to prevent projection to the other side.

is it this property ?

Screenshot 2026-08-14 at 16.03.00

Yes. Anything greater than zero will prevent projection to the other side.

This worked, but it got side-effects of not projecting decals on top of the planks and door handle.

Set it to a very small value.

0.001 it is the smallest value it takes.

Hm, strange. You’ll have to rely on adjusting the decal’s box then.

Is the decal always parallel to the door? Is the orientation at instantiation correct. I couldn’t tell with your tests with selecting the decal at runtime in RemoteView.

I don’t understand what’s the actual problem here. Just parent the decal to the door and adjust its size.

I just used it scene as example the decal is added on collision from fireball and explosion in runtime.

Size will cause more projection on wall around animation of opening doors is playing.

no, this was example scene.

I’m looking currently how to use this Visual Instance and Mask to avoid projection around .

@conz3d

	print(decal.get_cull_mask())
	# cull mask - not 3 selected  "1048571"
	# cull mask - all selected "1048575"
	# cull mask - not 3 and 5 selected "1048555"

BTW why does another node doesn’t name itself Decal_1 ?

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

Where do you initialize the decal_count? If it’s always set back to 0 than all decals will try to rename to Decal_0. But if there is already a Decal_0 it well get the generic name %Node%Number.
You have to initialize the var at the beginning of the script outside of _physics_process.

full script

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
static var decal_count: int = 0
var explosion_count: int = 0
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()
		decal.name = "Decal_%d" %decal_count
		decal_count += 1
		collider.add_child(decal)
		explosion.position = point
		decal.global_transform = Transform3D(Basis(rotate_up), point)
		explosion.name = "Explosion_%d" %explosion_count
		explosion_count += 1
		get_tree().current_scene.add_child(explosion)
		explosion.blast()
		queue_free()

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

func cleanup() -> void:
	queue_free()

Ah, it’s on the raycast. I assume it will run for every raycast. It’s not a master script that could count the decals. So every Ray starts with its own count and it’s always 0.
The Script that fires this ray should keep the amount of decals. If you want this with the name. The name was more or less for debug, as I was earlier unsure what the nodes may contain.

I removed exit_tree and this fixed it, but it will count to forever instead of cap itself.

current code

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
static var decal_count: int = 0
static var explosion_count: int = 0
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()
		decal.name = "Decal_%d" %decal_count
		decal_count += 1
		collider.add_child(decal)
		explosion.position = point
		decal.global_transform = Transform3D(Basis(rotate_up), point)
		explosion.name = "Explosion_%d" %explosion_count
		explosion_count += 1
		get_tree().current_scene.add_child(explosion)
		explosion.blast()
		queue_free()


func cleanup() -> void:
	queue_free()

the master script is launcher

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

At the moment you count the rays (that hit), not the decals. If you count down at the queue_free() here, it would be in the same frame.
You add the decal to the collider. So it’s not part of your extended raycast.

How does the decal get removed? At that point you would have to count down.
I would count the decals in the master script. And would send a signal on added and removed decal. And would give the projectile the information of the current decal count, Just for the name.
Maybe a little bit over complicated. And I can see some edge cases where the count could get messed up.

EDIT: As an alternative, you could store the amount in a singleton. Maybe there are better solution (hopes that someone jumps in).

decal script is self managed, the trick wrote by gertenko :slight_smile: , I see I made mistake in existing script not to put it into ready func.

extends Node3D

static var decals_amount: int = 0
const MAX_DECALS_AMOUNT: int = 10
@onready var decal: Decal = $Decal

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()
	print(decal.get_cull_mask())
	# cull mask - not 3 "1048571"
	# cull mask - all "1048575"
	# cull mask - not 3 and 5 "1048555"
func _exit_tree() -> void:
	decals_amount -= 1

func _on_timer_timeout() -> void:
	queue_free()

decal get removed by timer or count higher than 10.

extends Node3D

@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var audio_stream_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D

func blast() -> void:
	audio_stream_player_3d.play()
	animation_player.play("explosion")
	animation_player.animation_finished.connect(_on_animation_finished)

func _on_animation_finished(_anim_name: StringName) -> void:
	queue_free()

blast get removed by animation finished.