229 lines
7.5 KiB
GDScript
229 lines
7.5 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
## Value coercion + gradient/curve builders for particle properties.
|
|
|
|
const MaterialValues := preload("res://addons/godot_ai/handlers/material_values.gd")
|
|
|
|
const _EMISSION_SHAPES := {
|
|
"point": ParticleProcessMaterial.EMISSION_SHAPE_POINT,
|
|
"sphere": ParticleProcessMaterial.EMISSION_SHAPE_SPHERE,
|
|
"sphere_surface": ParticleProcessMaterial.EMISSION_SHAPE_SPHERE_SURFACE,
|
|
"box": ParticleProcessMaterial.EMISSION_SHAPE_BOX,
|
|
"points": ParticleProcessMaterial.EMISSION_SHAPE_POINTS,
|
|
"directed_points": ParticleProcessMaterial.EMISSION_SHAPE_DIRECTED_POINTS,
|
|
"ring": ParticleProcessMaterial.EMISSION_SHAPE_RING,
|
|
}
|
|
|
|
|
|
## Resolve a shape name to the int enum, or return null.
|
|
static func resolve_emission_shape(value: Variant) -> Variant:
|
|
if value is int:
|
|
return value
|
|
if value is float:
|
|
return int(value)
|
|
if value is String:
|
|
var key := String(value).to_lower()
|
|
if _EMISSION_SHAPES.has(key):
|
|
return _EMISSION_SHAPES[key]
|
|
return null
|
|
|
|
|
|
static func emission_shape_names() -> Array:
|
|
return _EMISSION_SHAPES.keys()
|
|
|
|
|
|
## Build a Gradient from {stops: [{time, color}]} dict.
|
|
static func build_gradient(value: Variant) -> Variant:
|
|
if value is Gradient:
|
|
return value
|
|
if value is GradientTexture1D:
|
|
return (value as GradientTexture1D).gradient
|
|
if not (value is Dictionary):
|
|
return null
|
|
var d: Dictionary = value
|
|
if not d.has("stops"):
|
|
return null
|
|
var stops_array = d.get("stops")
|
|
if not (stops_array is Array):
|
|
return null
|
|
var offsets := PackedFloat32Array()
|
|
var colors := PackedColorArray()
|
|
for stop in stops_array:
|
|
if not (stop is Dictionary):
|
|
return null
|
|
offsets.append(float(stop.get("time", 0.0)))
|
|
var c = MaterialValues.parse_color(stop.get("color"))
|
|
if c == null:
|
|
return null
|
|
colors.append(c)
|
|
var grad := Gradient.new()
|
|
grad.offsets = offsets
|
|
grad.colors = colors
|
|
return grad
|
|
|
|
|
|
## Build a GradientTexture1D wrapping a Gradient (what ParticleProcessMaterial.color_ramp wants).
|
|
static func build_gradient_texture(value: Variant) -> Variant:
|
|
if value is GradientTexture1D:
|
|
return value
|
|
var grad = build_gradient(value)
|
|
if grad == null:
|
|
return null
|
|
var tex := GradientTexture1D.new()
|
|
tex.gradient = grad
|
|
return tex
|
|
|
|
|
|
## Build a Curve from [{time, value}] or {points: [...]} (float-over-time).
|
|
static func build_curve(value: Variant) -> Variant:
|
|
if value is Curve:
|
|
return value
|
|
if value is CurveTexture:
|
|
return (value as CurveTexture).curve
|
|
var points_array: Variant = null
|
|
if value is Array:
|
|
points_array = value
|
|
elif value is Dictionary and value.has("points"):
|
|
points_array = value["points"]
|
|
if not (points_array is Array):
|
|
return null
|
|
var curve := Curve.new()
|
|
for pt in points_array:
|
|
if not (pt is Dictionary):
|
|
return null
|
|
var t := float(pt.get("time", 0.0))
|
|
var v := float(pt.get("value", 0.0))
|
|
curve.add_point(Vector2(t, v))
|
|
return curve
|
|
|
|
|
|
static func build_curve_texture(value: Variant) -> Variant:
|
|
if value is CurveTexture:
|
|
return value
|
|
var curve = build_curve(value)
|
|
if curve == null:
|
|
return null
|
|
var tex := CurveTexture.new()
|
|
tex.curve = curve
|
|
return tex
|
|
|
|
|
|
## Coerce a particle property value to the appropriate type.
|
|
## Handles: Vector3/gravity/direction, Color, float, int, bool, enum strings.
|
|
## For color_ramp returns a GradientTexture1D; for *_curve returns CurveTexture.
|
|
static func coerce(property: String, value: Variant, target_type: int) -> Dictionary:
|
|
# Special-cased properties.
|
|
if property == "emission_shape":
|
|
var shape = resolve_emission_shape(value)
|
|
if shape == null:
|
|
return {
|
|
"ok": false,
|
|
"error": "Invalid emission_shape '%s'. Valid: %s" % [
|
|
value, ", ".join(emission_shape_names())
|
|
],
|
|
}
|
|
return {"ok": true, "value": int(shape)}
|
|
|
|
if property == "color_ramp" or property == "color_initial_ramp":
|
|
var tex = build_gradient_texture(value)
|
|
if tex == null:
|
|
return {"ok": false, "error": "Invalid gradient for %s (expected {stops: [{time, color}]})" % property}
|
|
return {"ok": true, "value": tex}
|
|
|
|
if property == "color" and value is Dictionary and not (value as Dictionary).has("stops"):
|
|
# color is a single Color, not a ramp.
|
|
var c = MaterialValues.parse_color(value)
|
|
if c == null:
|
|
return {"ok": false, "error": "Invalid color"}
|
|
return {"ok": true, "value": c}
|
|
|
|
if property.ends_with("_curve"):
|
|
var tex = build_curve_texture(value)
|
|
if tex == null:
|
|
return {"ok": false, "error": "Invalid curve for %s (expected [{time, value}])" % property}
|
|
return {"ok": true, "value": tex}
|
|
|
|
# Fall through to the material coercer (handles Color/Vec3/Vec2/float/int/bool/enum).
|
|
return MaterialValues.coerce_material_value(property, value, target_type)
|
|
|
|
|
|
## Build a StandardMaterial3D suitable for GPUParticles3D draw-pass rendering.
|
|
##
|
|
## Godot's default Mesh has no material, which means ParticleProcessMaterial's
|
|
## color_ramp (which drives the COLOR varying) gets ignored and particles
|
|
## render as flat white squares that don't face the camera. A correct default
|
|
## must have vertex_color_use_as_albedo=true, billboard=particles, unshaded,
|
|
## and alpha transparency so the gradient actually modulates the pixels.
|
|
##
|
|
## Config is an optional dict that overrides individual properties. Supported
|
|
## keys match BaseMaterial3D properties (plus enum-by-name via MaterialValues):
|
|
## blend_mode: "mix" | "add" | "sub" | "mul"
|
|
## transparency: "disabled" | "alpha" | "alpha_scissor" | "alpha_hash" | "alpha_depth_pre_pass"
|
|
## shading_mode: "unshaded" | "per_pixel" | "per_vertex"
|
|
## billboard_mode: "disabled" | "enabled" | "fixed_y" | "particles"
|
|
## vertex_color_use_as_albedo: bool
|
|
## emission_enabled: bool
|
|
## emission: Color
|
|
## emission_energy_multiplier: float
|
|
## albedo_color: Color
|
|
## albedo_texture: res:// path
|
|
## (anything else accepted by BaseMaterial3D.set())
|
|
static func build_draw_material(config: Dictionary) -> StandardMaterial3D:
|
|
var mat := StandardMaterial3D.new()
|
|
# Sensible defaults for particle draw-pass rendering.
|
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
mat.vertex_color_use_as_albedo = true
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
|
mat.billboard_keep_scale = true
|
|
# Configure from dict overrides.
|
|
for key in config:
|
|
var prop_name := String(key)
|
|
var prop_type := _object_property_type(mat, prop_name)
|
|
if prop_type == TYPE_NIL:
|
|
continue
|
|
var coerce_result := MaterialValues.coerce_material_value(
|
|
prop_name, config[prop_name], prop_type
|
|
)
|
|
if coerce_result.ok:
|
|
mat.set(prop_name, coerce_result.value)
|
|
return mat
|
|
|
|
|
|
static func _object_property_type(obj: Object, name: String) -> int:
|
|
if obj == null:
|
|
return TYPE_NIL
|
|
for prop in obj.get_property_list():
|
|
if prop.name == name:
|
|
return int(prop.get("type", TYPE_NIL))
|
|
return TYPE_NIL
|
|
|
|
|
|
## Serialize for response.
|
|
static func serialize(value: Variant) -> Variant:
|
|
if value == null:
|
|
return null
|
|
if value is GradientTexture1D:
|
|
var grad := (value as GradientTexture1D).gradient
|
|
if grad == null:
|
|
return {"type": "GradientTexture1D", "stops": []}
|
|
var stops: Array = []
|
|
for i in grad.offsets.size():
|
|
var c: Color = grad.colors[i]
|
|
stops.append({
|
|
"time": grad.offsets[i],
|
|
"color": {"r": c.r, "g": c.g, "b": c.b, "a": c.a},
|
|
})
|
|
return {"type": "GradientTexture1D", "stops": stops}
|
|
if value is CurveTexture:
|
|
var curve := (value as CurveTexture).curve
|
|
if curve == null:
|
|
return {"type": "CurveTexture", "points": []}
|
|
var points: Array = []
|
|
for i in curve.get_point_count():
|
|
var p := curve.get_point_position(i)
|
|
points.append({"time": p.x, "value": p.y})
|
|
return {"type": "CurveTexture", "points": points}
|
|
return MaterialValues.serialize_value(value)
|