2. On a model
Sit plumes on nodes of a moving model, from outside its JSX.
A real scene's engines are nodes of a loaded model: a glTF's meshes, groups or
bones. They aren't in your JSX, so you can't nest an <Afterburner> inside
them. Use target instead.
Nesting, when you can
If the engine is in your JSX, nest the plume in it. <Afterburner> is a group,
and follows its parent like any child:
<group ref={aircraft}>
<AircraftModel />
{/* 7 m aft of an aircraft that flies along -Z: the plume's own way */}
<Afterburner position={[0, 0, 7]} />
</group>target, when you can't
target takes any Object3D. The plume is portalled into it, so it follows the
node however the model moves. Its position and rotation (or direction)
are then in that node's frame:
import { useGLTF } from "@react-three/drei";
const { scene, nodes } = useGLTF("/fighter.glb");
<>
<primitive object={scene} />
<Afterburner
target={nodes.FX_Exhaust_L}
position={[0, 0, 0.08]} // at the petals' trailing edge, in the node's frame
direction={[0, 0, -1]} // exhaust down the node's -Z, out of the back
params={{ nozzleRadiusM: 0.29, nozzleAspect: 1.67 }} // the exit as modelled
/>
</>;The easiest numbers are none at all. Give the model an empty on each nozzle
exit, pointing the way the exhaust goes, and the plume sits on it at the
origin. That is what the model above does: FX_Exhaust_L and FX_Exhaust_R
are empties in Blender, exported as glTF nodes, and parented to the nozzles'
vectoring gimbals, so the plumes follow the nozzles as they swivel.
Without one, open the model in a viewer and read the exit plane's position off
the engine node, in that node's own axes. Measure the exit's radius too, and
pass it as nozzleRadiusM, so the flame leaves the nozzle that is drawn.
This model's exits are ovals, 0.76 m wide and 0.45 m high inside the petals.
nozzleAspect gives the plume that shape, width over height, and
nozzleRadiusM is the round exit of the same area, √(0.38 × 0.227). Match
them to the exit as drawn: the dry engine's glow fills that outline, so a
smaller one leaves a dark rim inside the nozzle. The width runs along the
plume frame's x, which this anchor's direction leaves level. See
Shaped nozzles for flat, square and any other
outline.
When the node arrives later
A node you get from a ref only exists after the first render. Keep it in state, so the plume re-renders onto it once it exists. A callback ref does that directly:
const [engine, set_engine] = useState<Object3D | null>(null);
<>
<mesh ref={set_engine}>…</mesh>
<Afterburner target={engine} position={[0, 0, 1]} />
</>;Until target is set, the plume sits where it would without it.
Several engines
Nozzles with the same scene and preset share one batch, so two engines are still one draw:
{
[left, right].map((engine, index) => (
<Afterburner
key={index}
target={engine}
position={[0, 0, 1]}
params={{ nozzleRadiusM: 0.35 }}
/>
));
}A different nozzleRadiusM scales every length of the plume with it. A
smaller engine gives a smaller plume of the same shape.
Next: drive it from code.