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 .
Decals have the normal threshold property. Adjust that to prevent projection to the other side.
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.
conz3d
August 14, 2026, 3:26pm
31
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)
conz3d
August 14, 2026, 3:52pm
35
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()
conz3d
August 14, 2026, 3:57pm
37
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
conz3d
August 14, 2026, 4:17pm
39
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 , 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()
Your raycast can get normals.
var collider = get_collider()
if is_colliding():
var normal := get_collision_normal()
but then it seems like you are using remote transforms to copy this positional data instead of moving your _explode function under this if is_colliding() statement
if is_colliding():
audio_stream_player_3d.stop()
var point := get_collision_point()
var normal := get_collision_normal() # get normal
var rotate_up := Quaternion(Vector3.UP, normal) # use normal to rotate
va…
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.