acron.particles3d¶
Godot’s CPUParticles3D, replayed on Acron.
A Godot particle node describes its effect in plain numbers – how many particles, how big, how fast, which way, what colour over life – and those numbers are the part of an effect that PORTS. (The shaders on top of them do not; those are per-game.) This module is the machinery for replaying them, so a ported emitter reads as a transcription of its Godot node rather than as a guess.
Two kinds: Burst is one_shot – fired by an AnimationPlayer
track or a line of script, lives out its lifetime, removes itself – and
Loop re-emits for as long as it exists.
What is reproduced, per particle: emission shape, explosiveness (emission
spread over the lifetime), direction + spread cone, initial and radial
velocity, gravity, damping, scale_amount * scale_amount_curve, and
color * color_ramp.
What is not: Godot’s Curve is a cubic with per-point tangents and this reads
the same authored points as a polyline; hue_variation, angular_velocity,
orbit_velocity and particle collision have no stand-in.
A particle is a ``NodePath``, integrated in Python on a render task, not a GPU buffer. That is the honest limit of this module and the reason a caller with a thousand-a-second emitter has to cut the count and say so. It is enough for effect-sized emitters (tens to low hundreds) and it is not a particle SYSTEM; replacing the inner loop with instanced geometry is the obvious next step and has not been taken, because it is a rendering change and this module’s whole test surface is headless.
FRAME NOTE. Every direction here is in ACRON’s frame. Where a number comes
straight off a Godot node it is converted with the same change of basis the rest
of the port uses – acron.gdformat.transform.TO_ZUP – and never by hand.
|
One emitter's worth of particles, integrated on a render task. |
|
A Godot |
|
A Godot |
|
A CONTINUOUSLY emitting |
Global Functions
- active()¶
Every live emitter, as a new set.
The registry itself is private because a caller must not be able to drop an emitter out of it without destroying it; this is the read side, for a scene teardown that wants to report what it swept and for tests.
- card_geom(width, height, name='particle_card')¶
A camera-facing quad, in the frame
set_billboard_point_eyeexpects.
- clear_all()¶
Destroy every live emitter. For a scene teardown.
- cone_geom(radius, height, segs=8)¶
The muzzle-flash cone: APEX at the origin, opening out along +Y.
Both the shape and the mapping are the authored mesh’s own, read out of
muzzle_flash_particle_mesh_Cone.mesh: eight sides, the apex at the origin and a radius-1 ring one length out, and UVs that put the apex at the texture’s centre and the rim on a circle of radius 0.5 around it. That is what makesFlarePolar.pnga flare – it is a disc of rays seen down the barrel, and any other unwrap smears it into a band.
- emissive(rgb)¶
A Godot emissive colour,
albedo * emission_energy, kept HDR.This used to CLIP at 1.0, on the reasoning that the port draws these surfaces unlit “straight into the framebuffer” and the framebuffer would clip them anyway. It does not: the PBR frame renders into a float16 buffer and tonemaps it at the end of the frame, so an over-1 value is meaningful all the way through (measured on simplepbr’s Hable curve, 1.0 came out at 0.842 and 5.0 at 0.963) – so clipping first was throwing away both the extra brightness AND the hue: the laser’s flash is
(1, 0.79, 0.79) * 5, which clipped to flat white where the curve gives a hot pink-white, which is what AgX gives it upstream.It was also why bloom could not work. With every emitter pinned at 1.0 there is nothing above a glow threshold to find, and a bloom pass over this scene brightened the reactor core from yellow to gold and left it a hard-edged flat disc. Keeping the headroom is the half that had to come first (see
engine/shaders/bloom.frag).Only the negative clamp survives, because a negative emission is not a colour. Reaching the buffer with the value intact needs the surface to be drawn by a SHADER –
set_color_scaleon a shader-less node is clamped by the fixed pipeline before it multiplies anything, which is whatunshaded()is now for.
- glb_geom(loader, res_uri, node_name, name=None)¶
One AUTHORED mesh out of a GLB, rebuilt as a plain
v3n3c4t2geom.Several of the demo’s effect meshes are Godot
ArrayMeshresources (.mesh) that were extracted from a Blender file, and a.meshis a binary Godot resource this port has no decoder for. It does not need one for the reactor core: the four meshescore.tscnnames –CoreOutLight_Circle,_Sphere,_OuterRim,_Circle001– are the four meshes ofCoreOutLight.glb, which ships in the same directory and is what they were extracted FROM. Reading the GLB reads the authored geometry.The mesh is copied through
acron.geom.GeomBuilderrather than instanced where it lies, and the reason is the UV column. glTF names ittexcoord.0, so a texture stage that does not say so leavesacron_MultiTexCoord0reading atexcoordcolumn that is not there and every fragment samples one texel (RUNTIME.md rule 14) – while every other geom in this module is aGeomBuilderproduct carrying the plaintexcoord. Going through the builder makes the authored meshes and the hand-built ones the same kind of thing, and drops the GLB’s own material and cull state with it;unshaded()puts back what the.tscnasks for.NOTE the frame. acron-gltf has already turned Y-up into Z-up IN THE VERTEX DATA – measured: every node in this GLB carries an identity transform – so a mesh authored flat in glTF’s XY plane arrives flat in Acron’s XZ plane facing -Y, which is the frame
set_billboard_point_eyeandcard_geom()already work in.
- godot_dir(x, y, z)¶
A Godot-frame direction as a Acron
LVector3(the port’s one conversion).
- load_shader(name)¶
<shader dir>/<name>.vert+.frag, compiled once.Returns
Noneif it does not compile or no directory is set, which every caller has to be able to live with: a shader that fails takes only its own effect down.
- ray_geom(width, length, name='particle_ray')¶
A quad running from the origin out to
lengthalong -Y (a light ray).
- repeat_sampler()¶
A REPEATING sampler, for a map whose shader scrolls the UV off the tile.
Clamping is right for a particle quad – a repeat on a billboard bleeds the far edge back over the near one – and it is what
texture()sets, so it is what every emitter before the reactor core got. The core’s two shaders are the exception: both declarerepeat_enableand then walk the UV off the tile without bound (UV + speed * phaseon the flare rings,UV + TIME * time_scaleon the shell), and clamped they smear one row of texels across the whole surface.It is a SAMPLER and not a second
Texturebecause Acron’s texture pool hands out oneTextureobject per file: setting the wrap on it would set it for every other node drawing that map, last caller wins, silently. ASamplerStatepassed toset_textureoverrides the wrap for one node and leaves the shared object alone.
- set_shader_dir(path)¶
Where
load_shader()looks for<name>.vert/<name>.frag.The shaders a particle effect is drawn with belong to the GAME – they are ported
.gdshaderfiles – so the package holds the machinery and the game says where its own source lives. Clears the compiled cache, so a second game in one process cannot inherit the first’s shaders.
- sphere_geom(radius, height=None, segs=12, rings=8)¶
Godot’s
SphereMesh:radiusacross,heightalong its own +Y.A
SphereMeshwithheight != 2 * radiusis a squashed/stretched sphere, which is how the bullet trail’sradius 0.05, height 0.3capsule and the ember’sradius 0.01, height 0.1are authored.
- texture(loader, res_uri)¶
Load one of the demo’s VFX PNGs (all RGBA), clamped and mipmapped.
- unshaded(np, additive=False, two_sided=True, depth_write=False, bin_order=40, shader=None)¶
- unshaded_shader()¶
The unlit HDR surface shader, compiled once (
Noneif it fails).
- white_texture()¶
A 1x1 white texture, so the unlit shader’s sampler is always bound.
An unbound sampler happens to read white on this engine; depending on that is the trap (Acron binds every uniform a shader DECLARES).