Bull roll ability, console screen, tests
This commit is contained in:
+502
@@ -0,0 +1,502 @@
|
||||
extends Node
|
||||
## Runtime debug overlays (autoload "DebugDraw").
|
||||
##
|
||||
## Two mechanisms, both gated by DP "Debug" bool params so they cost nothing when off:
|
||||
## • Immediate-mode 3D lines — call DebugDraw.line()/ray() each frame; flushed once
|
||||
## per frame into a single ImmediateMesh. Used for raycasts and the bone overlay.
|
||||
## • Shape overlays — a MeshInstance3D attached to each CollisionShape3D, reconciled
|
||||
## incrementally so static geometry is never re-instanced and freshly spawned
|
||||
## bodies get picked up within one interval.
|
||||
##
|
||||
## Toggle from the console:
|
||||
## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false)
|
||||
## show_states — floating AI-state tags + velocity vectors over each matador
|
||||
## show_animation_name — floating current-clip tag over every AnimationPlayer host
|
||||
## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally
|
||||
## show_grid — 1 m reference grid on the ground plane around the origin
|
||||
## show_axes — RGB world-axis gizmo at the origin
|
||||
## show_wireframe — render the whole viewport in wireframe
|
||||
|
||||
const REBUILD_INTERVAL := 0.5
|
||||
const STATS_INTERVAL := 0.2 # stats text is re-composed 5×/sec, not every frame
|
||||
|
||||
const COLOR_BODY := Color(0.30, 1.0, 0.45) # collision wireframes (green)
|
||||
const COLOR_AREA := Color(0.0, 0.9, 1.0) # hit-area volumes (cyan, translucent)
|
||||
const COLOR_BONE := Color(1.0, 0.35, 0.9) # skeleton bones (magenta)
|
||||
const COLOR_VEL := Color(1.0, 0.85, 0.2) # matador velocity vectors (amber)
|
||||
const COLOR_ANIM := Color(0.55, 0.85, 1.0) # animation-name tags (sky blue)
|
||||
const COLOR_GRID := Color(0.45, 0.45, 0.45, 0.5)
|
||||
const AREA_ALPHA := 0.25
|
||||
|
||||
const GRID_HALF := 20 # grid extends ±GRID_HALF cells from the origin
|
||||
const GRID_STEP := 1.0 # metres between grid lines
|
||||
const AXIS_LEN := 3.0 # length of each world-axis gizmo arm
|
||||
|
||||
var _line_mesh: ImmediateMesh
|
||||
var _lines: PackedVector3Array = PackedVector3Array()
|
||||
var _colors: PackedColorArray = PackedColorArray()
|
||||
|
||||
var _overlays: Dictionary = {} # CollisionShape3D instance id -> MeshInstance3D
|
||||
var _skeletons: Array[Skeleton3D] = []
|
||||
var _anim_players: Array[AnimationPlayer] = []
|
||||
var _reconcile_t: float = 0.0
|
||||
var _prev_flags: String = ""
|
||||
|
||||
var _state_labels: Dictionary = {} # matador instance id -> Label3D
|
||||
var _anim_labels: Dictionary = {} # AnimationPlayer instance id -> Label3D
|
||||
|
||||
var _stats_layer: CanvasLayer = null
|
||||
var _stats_label: Label = null
|
||||
var _stats_t: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS # keep drawing while the console pauses the game
|
||||
process_priority = 1000 # flush after everyone has queued their lines
|
||||
# One-time: wireframe debug-draw needs the wireframe index buffers generated.
|
||||
RenderingServer.set_debug_generate_wireframes(true)
|
||||
_line_mesh = ImmediateMesh.new()
|
||||
var inst := MeshInstance3D.new()
|
||||
inst.mesh = _line_mesh
|
||||
inst.material_override = _line_material()
|
||||
inst.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(inst)
|
||||
|
||||
|
||||
# ── Public immediate-mode API ──────────────────────────────────────────────────
|
||||
|
||||
func line(from: Vector3, to: Vector3, color: Color = Color.YELLOW) -> void:
|
||||
_lines.append(from)
|
||||
_lines.append(to)
|
||||
_colors.append(color)
|
||||
_colors.append(color)
|
||||
|
||||
|
||||
func ray(from: Vector3, to: Vector3, color: Color = Color.YELLOW) -> void:
|
||||
line(from, to, color)
|
||||
|
||||
|
||||
# ── Frame flush ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_reconcile(delta)
|
||||
if DP.b("show_bones"):
|
||||
_draw_bones()
|
||||
if DP.b("show_states"):
|
||||
_update_state_labels()
|
||||
elif not _state_labels.is_empty():
|
||||
_clear_state_labels()
|
||||
if DP.b("show_animation_name"):
|
||||
_update_anim_labels()
|
||||
elif not _anim_labels.is_empty():
|
||||
_clear_anim_labels()
|
||||
if DP.b("show_grid"):
|
||||
_draw_grid()
|
||||
if DP.b("show_axes"):
|
||||
_draw_axes()
|
||||
_update_wireframe()
|
||||
_update_stats(delta)
|
||||
_flush_lines()
|
||||
|
||||
|
||||
func _flush_lines() -> void:
|
||||
_line_mesh.clear_surfaces()
|
||||
if not _lines.is_empty():
|
||||
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||
for i in _lines.size():
|
||||
_line_mesh.surface_set_color(_colors[i])
|
||||
_line_mesh.surface_add_vertex(_lines[i])
|
||||
_line_mesh.surface_end()
|
||||
_lines.clear()
|
||||
_colors.clear()
|
||||
|
||||
|
||||
# ── Bone overlay ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _draw_bones() -> void:
|
||||
for sk in _skeletons:
|
||||
if not is_instance_valid(sk):
|
||||
continue
|
||||
var gx := sk.global_transform
|
||||
for b in sk.get_bone_count():
|
||||
var parent := sk.get_bone_parent(b)
|
||||
if parent < 0:
|
||||
continue
|
||||
var a := gx * sk.get_bone_global_pose(parent).origin
|
||||
var c := gx * sk.get_bone_global_pose(b).origin
|
||||
line(a, c, COLOR_BONE)
|
||||
|
||||
|
||||
# ── Matador AI-state overlay ───────────────────────────────────────────────────
|
||||
# A billboarded state tag hovers over each matador, coloured by state, with an
|
||||
# amber velocity vector — reads the whole arena's AI at a glance while tuning combat.
|
||||
|
||||
func _update_state_labels() -> void:
|
||||
var seen: Dictionary = {}
|
||||
for node: Node in get_tree().get_nodes_in_group(&"matador"):
|
||||
var m := node as Node3D
|
||||
if m == null:
|
||||
continue
|
||||
var id := m.get_instance_id()
|
||||
seen[id] = true
|
||||
var lbl: Label3D = _state_labels.get(id)
|
||||
if lbl == null:
|
||||
lbl = _new_state_label()
|
||||
_state_labels[id] = lbl
|
||||
var state_name := "?"
|
||||
if m.has_method(&"ai_state_name"):
|
||||
state_name = m.call(&"ai_state_name")
|
||||
lbl.text = state_name
|
||||
lbl.modulate = _state_color(state_name)
|
||||
lbl.global_position = m.global_position + Vector3.UP * 2.2
|
||||
if m is CharacterBody3D:
|
||||
var v: Vector3 = (m as CharacterBody3D).velocity
|
||||
v.y = 0.0
|
||||
if v.length() > 0.5:
|
||||
var base := m.global_position + Vector3.UP * 0.15
|
||||
line(base, base + v * 0.18, COLOR_VEL)
|
||||
for id: int in _state_labels.keys():
|
||||
if not seen.has(id):
|
||||
var lbl: Label3D = _state_labels[id]
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_state_labels.erase(id)
|
||||
|
||||
|
||||
func _new_state_label() -> Label3D:
|
||||
var lbl := Label3D.new()
|
||||
lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
lbl.no_depth_test = true
|
||||
lbl.fixed_size = true
|
||||
lbl.pixel_size = 0.0006
|
||||
lbl.font_size = 64
|
||||
lbl.outline_size = 12
|
||||
lbl.outline_modulate = Color(0.0, 0.0, 0.0, 0.9)
|
||||
add_child(lbl)
|
||||
return lbl
|
||||
|
||||
|
||||
func _clear_state_labels() -> void:
|
||||
for id: int in _state_labels:
|
||||
var lbl: Label3D = _state_labels[id]
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_state_labels.clear()
|
||||
|
||||
|
||||
const _STATE_COLORS := {
|
||||
"WANDER": Color(0.70, 0.70, 0.70),
|
||||
"FLEE": Color(0.40, 0.80, 1.00),
|
||||
"BRACE": Color(1.00, 0.90, 0.30),
|
||||
"SIDESTEP": Color(1.00, 0.60, 0.10),
|
||||
"ATTACK": Color(1.00, 0.30, 0.30),
|
||||
"ROLL": Color(0.60, 1.00, 0.60),
|
||||
"THROW": Color(1.00, 0.40, 0.90),
|
||||
"RAGDOLL": Color(0.40, 0.40, 0.40),
|
||||
}
|
||||
|
||||
|
||||
func _state_color(state_name: String) -> Color:
|
||||
return _STATE_COLORS.get(state_name, Color.WHITE)
|
||||
|
||||
|
||||
# ── Animation-name overlay ─────────────────────────────────────────────────────
|
||||
# A billboarded tag showing the clip each AnimationPlayer is currently playing,
|
||||
# hovering above its nearest Node3D host — catches wrong / stuck animation states
|
||||
# at a glance. Players are re-collected on the reconcile tick (see _collect).
|
||||
|
||||
func _update_anim_labels() -> void:
|
||||
var seen: Dictionary = {}
|
||||
for ap in _anim_players:
|
||||
if not is_instance_valid(ap):
|
||||
continue
|
||||
var clip := ap.current_animation
|
||||
# Skip finished / dormant sub-players so a host with several AnimationPlayers
|
||||
# (e.g. a matador's main + olé + death players) doesn't stack empty tags.
|
||||
if clip.is_empty() and not ap.is_playing():
|
||||
continue
|
||||
var host := _node3d_host(ap)
|
||||
if host == null:
|
||||
continue
|
||||
var id := ap.get_instance_id()
|
||||
seen[id] = true
|
||||
var lbl: Label3D = _anim_labels.get(id)
|
||||
if lbl == null:
|
||||
lbl = _new_state_label()
|
||||
lbl.modulate = COLOR_ANIM
|
||||
_anim_labels[id] = lbl
|
||||
lbl.text = clip if not clip.is_empty() else "—"
|
||||
lbl.global_position = host.global_position + Vector3.UP * 2.6
|
||||
for id: int in _anim_labels.keys():
|
||||
if not seen.has(id):
|
||||
var lbl: Label3D = _anim_labels[id]
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_anim_labels.erase(id)
|
||||
|
||||
|
||||
func _clear_anim_labels() -> void:
|
||||
for id: int in _anim_labels:
|
||||
var lbl: Label3D = _anim_labels[id]
|
||||
if is_instance_valid(lbl):
|
||||
lbl.queue_free()
|
||||
_anim_labels.clear()
|
||||
|
||||
|
||||
# Nearest Node3D at or above `node` — the anchor an AnimationPlayer's tag hovers over.
|
||||
func _node3d_host(node: Node) -> Node3D:
|
||||
var n := node
|
||||
while n != null:
|
||||
if n is Node3D:
|
||||
return n as Node3D
|
||||
n = n.get_parent()
|
||||
return null
|
||||
|
||||
|
||||
# ── World reference overlays ───────────────────────────────────────────────────
|
||||
|
||||
func _draw_grid() -> void:
|
||||
var extent := GRID_HALF * GRID_STEP
|
||||
for i in range(-GRID_HALF, GRID_HALF + 1):
|
||||
var o := i * GRID_STEP
|
||||
line(Vector3(o, 0.01, -extent), Vector3(o, 0.01, extent), COLOR_GRID)
|
||||
line(Vector3(-extent, 0.01, o), Vector3(extent, 0.01, o), COLOR_GRID)
|
||||
|
||||
|
||||
func _draw_axes() -> void:
|
||||
line(Vector3.ZERO, Vector3.RIGHT * AXIS_LEN, Color.RED) # +X
|
||||
line(Vector3.ZERO, Vector3.UP * AXIS_LEN, Color.GREEN) # +Y
|
||||
line(Vector3.ZERO, Vector3(0.0, 0.0, 1.0) * AXIS_LEN, Color.DODGER_BLUE) # +Z
|
||||
|
||||
|
||||
func _update_wireframe() -> void:
|
||||
var vp := get_viewport()
|
||||
var want := Viewport.DEBUG_DRAW_WIREFRAME if DP.b("show_wireframe") else Viewport.DEBUG_DRAW_DISABLED
|
||||
if vp.debug_draw != want:
|
||||
vp.debug_draw = want
|
||||
|
||||
|
||||
# ── Stats overlay ──────────────────────────────────────────────────────────────
|
||||
# A 2D corner readout of frame cost + scene load, plus a per-state matador tally so
|
||||
# a combat slowdown or a stuck AI swarm shows up immediately.
|
||||
|
||||
func _update_stats(delta: float) -> void:
|
||||
if not DP.b("show_stats"):
|
||||
if _stats_layer != null:
|
||||
_stats_layer.visible = false
|
||||
return
|
||||
_ensure_stats_ui()
|
||||
_stats_layer.visible = true
|
||||
_stats_t -= delta
|
||||
if _stats_t > 0.0:
|
||||
return
|
||||
_stats_t = STATS_INTERVAL
|
||||
_stats_label.text = _compose_stats()
|
||||
|
||||
|
||||
func _ensure_stats_ui() -> void:
|
||||
if _stats_layer != null:
|
||||
return
|
||||
var mono := SystemFont.new()
|
||||
mono.font_names = PackedStringArray(["JetBrains Mono", "DejaVu Sans Mono", "monospace"])
|
||||
|
||||
_stats_layer = CanvasLayer.new()
|
||||
_stats_layer.layer = 64
|
||||
add_child(_stats_layer)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.position = Vector2(8.0, 8.0)
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.0, 0.0, 0.0, 0.55)
|
||||
bg.set_content_margin_all(6.0)
|
||||
panel.add_theme_stylebox_override("panel", bg)
|
||||
_stats_layer.add_child(panel)
|
||||
|
||||
_stats_label = Label.new()
|
||||
_stats_label.add_theme_font_override("font", mono)
|
||||
_stats_label.add_theme_font_size_override("font_size", 13)
|
||||
_stats_label.add_theme_color_override("font_color", Color(0.60, 1.0, 0.60))
|
||||
panel.add_child(_stats_label)
|
||||
|
||||
|
||||
func _compose_stats() -> String:
|
||||
var fps := int(Performance.get_monitor(Performance.TIME_FPS))
|
||||
var proc := Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0
|
||||
var phys := Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0
|
||||
var draws := int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
||||
var prims := int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
|
||||
var vmem := int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED) / 1048576.0)
|
||||
var nodes := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
var phys3d := int(Performance.get_monitor(Performance.PHYSICS_3D_ACTIVE_OBJECTS))
|
||||
return "fps %d proc %.1fms phys %.1fms\ndraws %d prims %s vmem %dMB\nnodes %d phys3d %d\n%s" % [
|
||||
fps, proc, phys, draws, _si(prims), vmem, nodes, phys3d, _matador_tally()]
|
||||
|
||||
|
||||
func _matador_tally() -> String:
|
||||
var mats := get_tree().get_nodes_in_group(&"matador")
|
||||
if mats.is_empty():
|
||||
return "matadors 0"
|
||||
var counts: Dictionary = {}
|
||||
for node: Node in mats:
|
||||
var s := "?"
|
||||
if node.has_method(&"ai_state_name"):
|
||||
s = node.call(&"ai_state_name")
|
||||
counts[s] = int(counts.get(s, 0)) + 1
|
||||
var parts: PackedStringArray = PackedStringArray()
|
||||
for s: String in counts:
|
||||
parts.append("%s:%d" % [s.substr(0, 2), counts[s]])
|
||||
return "matadors %d %s" % [mats.size(), " ".join(parts)]
|
||||
|
||||
|
||||
func _si(n: int) -> String:
|
||||
if n >= 1000000:
|
||||
return "%.1fM" % (n / 1000000.0)
|
||||
if n >= 1000:
|
||||
return "%.0fk" % (n / 1000.0)
|
||||
return str(n)
|
||||
|
||||
|
||||
# ── Collision / hit-area overlays (incremental) ────────────────────────────────
|
||||
|
||||
func _reconcile(delta: float) -> void:
|
||||
var col := DP.b("show_collisions")
|
||||
var hit := DP.b("show_hitboxes")
|
||||
var bone := DP.b("show_bones")
|
||||
var anim := DP.b("show_animation_name")
|
||||
if not (col or hit or bone or anim):
|
||||
if not _overlays.is_empty():
|
||||
_clear_overlays()
|
||||
_skeletons.clear()
|
||||
_anim_players.clear()
|
||||
_prev_flags = ""
|
||||
return
|
||||
|
||||
var flags := "%d%d%d%d" % [int(col), int(hit), int(bone), int(anim)]
|
||||
_reconcile_t -= delta
|
||||
if flags != _prev_flags: # a toggle just changed — refresh immediately
|
||||
_prev_flags = flags
|
||||
_reconcile_t = 0.0
|
||||
if _reconcile_t > 0.0:
|
||||
return
|
||||
_reconcile_t = REBUILD_INTERVAL
|
||||
|
||||
_skeletons.clear()
|
||||
_anim_players.clear()
|
||||
var wanted: Dictionary = {}
|
||||
var scene := get_tree().current_scene
|
||||
if scene != null:
|
||||
_collect(scene, col, hit, bone, anim, wanted)
|
||||
|
||||
# Drop overlays whose shape is gone or no longer wanted.
|
||||
for id: int in _overlays.keys():
|
||||
var mi: MeshInstance3D = _overlays[id]
|
||||
if not wanted.has(id) or not is_instance_valid(mi):
|
||||
if is_instance_valid(mi):
|
||||
mi.queue_free()
|
||||
_overlays.erase(id)
|
||||
# Add overlays for newly seen shapes.
|
||||
for id: int in wanted:
|
||||
if not _overlays.has(id):
|
||||
var entry: Array = wanted[id]
|
||||
_overlays[id] = _attach(entry[0] as CollisionShape3D, entry[1] as bool)
|
||||
|
||||
|
||||
func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted: Dictionary) -> void:
|
||||
if bone and node is Skeleton3D and (node as Skeleton3D).is_visible_in_tree():
|
||||
_skeletons.append(node as Skeleton3D)
|
||||
if anim and node is AnimationPlayer:
|
||||
_anim_players.append(node as AnimationPlayer)
|
||||
if node is CollisionShape3D:
|
||||
var cs := node as CollisionShape3D
|
||||
if cs.shape != null and not cs.disabled and _drawable(cs.shape):
|
||||
var parent := cs.get_parent()
|
||||
var is_area := parent is Area3D
|
||||
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for
|
||||
# the skeleton; otherwise every corpse floods the view with capsules.
|
||||
var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D)
|
||||
if (is_area and hit) or (is_body and col):
|
||||
wanted[cs.get_instance_id()] = [cs, is_area]
|
||||
for child in node.get_children():
|
||||
_collect(child, col, hit, bone, anim, wanted)
|
||||
|
||||
|
||||
# Skip only the unbounded shapes whose debug mesh is meaningless/huge; concave arena
|
||||
# geometry is fine — Godot caches each shape's debug mesh, so it's built at most once.
|
||||
func _drawable(shape: Shape3D) -> bool:
|
||||
return not (shape is WorldBoundaryShape3D or shape is HeightMapShape3D)
|
||||
|
||||
|
||||
func _attach(cs: CollisionShape3D, is_area: bool) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
if is_area:
|
||||
var solid := _solid_mesh(cs.shape)
|
||||
mi.mesh = solid if solid != null else cs.shape.get_debug_mesh()
|
||||
mi.material_override = _translucent_material(COLOR_AREA)
|
||||
else:
|
||||
mi.mesh = cs.shape.get_debug_mesh()
|
||||
mi.material_override = _wire_material(COLOR_BODY)
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
cs.add_child(mi)
|
||||
return mi
|
||||
|
||||
|
||||
func _clear_overlays() -> void:
|
||||
for id: int in _overlays:
|
||||
var mi: MeshInstance3D = _overlays[id]
|
||||
if is_instance_valid(mi):
|
||||
mi.queue_free()
|
||||
_overlays.clear()
|
||||
|
||||
|
||||
# ── Meshes & materials ─────────────────────────────────────────────────────────
|
||||
|
||||
# A filled mesh matching a primitive shape, for translucent volume overlays. Returns
|
||||
# null for shapes without a clean solid equivalent (caller falls back to wireframe).
|
||||
func _solid_mesh(shape: Shape3D) -> Mesh:
|
||||
if shape is BoxShape3D:
|
||||
var m := BoxMesh.new()
|
||||
m.size = (shape as BoxShape3D).size
|
||||
return m
|
||||
if shape is SphereShape3D:
|
||||
var m := SphereMesh.new()
|
||||
m.radius = (shape as SphereShape3D).radius
|
||||
m.height = m.radius * 2.0
|
||||
return m
|
||||
if shape is CapsuleShape3D:
|
||||
var m := CapsuleMesh.new()
|
||||
m.radius = (shape as CapsuleShape3D).radius
|
||||
m.height = (shape as CapsuleShape3D).height
|
||||
return m
|
||||
if shape is CylinderShape3D:
|
||||
var m := CylinderMesh.new()
|
||||
m.top_radius = (shape as CylinderShape3D).radius
|
||||
m.bottom_radius = (shape as CylinderShape3D).radius
|
||||
m.height = (shape as CylinderShape3D).height
|
||||
return m
|
||||
return null
|
||||
|
||||
|
||||
func _line_material() -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
mat.no_depth_test = true
|
||||
return mat
|
||||
|
||||
|
||||
func _wire_material(color: Color) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.albedo_color = color
|
||||
mat.no_depth_test = true
|
||||
return mat
|
||||
|
||||
|
||||
func _translucent_material(color: Color) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(color.r, color.g, color.b, AREA_ALPHA)
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
return mat
|
||||
Reference in New Issue
Block a user