flutter3d 0.4.3
flutter3d: ^0.4.3 copied to clipboard
A 3D engine on Flutter GPU: scene graph, glTF/OBJ/.f3d loading, PBR, shadows, bloom, skinning.
0.4.3 #
RenderSettings.anisotropy. Taps a model's texture samplers may take along a foreshortened axis, applied at bind time to every material sampler that is trilinear and carries no level of its own, clamped toGraphicsDevice.maxAnisotropyon the way. One by default, so every picture is the bytes it was. A setting rather than a property of the asset because glTF has no way to say it: asamplerin the file names filters and wrap modes and nothing about taps. A level's brushes do not come through here — the bridge sizes their sampler to the device at load, and the setting leaves a sampler that already asks for taps alone.anisotropic-floorjoins the Impeller and software golden sets, with the WebGL reference recorded at merge: the demo's ground under the cube, stretched to the horizon, retextured with a checkerboard and its chain and seen from just above it. The setting is asserted to be one or more, asSamplerOptions.anisotropyalready was.Renderer.relinkShaders: the engine's half of a hot reload. ALoadedShaderLibrary.refreshswaps the code behind a stage and keeps the handle; a pipeline is a pair of stages linked, and the linked object still holds the old code until it is built again. This drops every pipeline the renderer linked — materials, shadows, sky, post, debug lines — so the next frame links the refreshed stages, at the cost of the first frame.loaded-shader, a golden in the Impeller and software sets: the teapot wearingExampleStripes, a look compiled into the example's own bundle (example/shaders/, built byexample/tool/build_shaders.sh) and loaded from bytes throughGraphicsDevice.loadShadersbefore the renderer is built.GoldenScene.shaderBundlenames the asset; the software backend draws it from the example's own Dart transcription.- Auto exposure.
RenderSettings.autoExposuremeters the frame: a luminance node writes the lit scene's log luminance into a 64×64 eight-bit target,GraphicsDevice.readbackhands the bytes back a frame or two later,ExposureMeteraverages the band between two percentiles of their histogram — the brightest fifth, so a dark corridor does not push a torchlit wall past white — andExposureAdaptermoves the exposure towards the answer in stops, at one rate climbing and another falling, between two limits. The composite exposes each frame with what the frame before was metered at;FrameResult.exposureandRenderer.exposuresay what that was. Off by default, since every golden is recorded at the setting's own number;auto-exposureis the one scene that turns it on. Its Impeller and software references are recorded here; the web one is not, becausegolden_web.shholds a fixed port for the whole of its run and so records when the branch lands. - Picking by pixel.
Renderer.pickPixel(u, v)asks the next frame which mesh is drawn at a point: that frame draws every visible mesh once more with theObjectIdstage into a frame-sized target, reads the one pixel back and answers with the node whose number came back — null for the clear colour, the batch for an instanced batch. Only on a frame somebody asked; otherwise the node is inactive and the graph culls it. A frame that fails after the question was asked answers it with the failure rather than never.Raycasterstays for a game, which wants an answer now and needs no frame. FrameResourceIds.luminanceandFrameResourceIds.objectIds, both frame outputs while their node is active, since their consumer is a readback the graph cannot see.- A pick through a hole answers with what is seen through it. A masked
material — glTF's
MASK— discards under its cutoff in the scene pass, and the id stage now discards the same fragments: it is handed the material's texture, cutoff and tint alpha inIdInfo.mask, so a click through a fence's hole answers with the thing behind the fence rather than the fence. Before, the id pass wrote every fragment and picked the plane the hole was cut in, which contradicted the one promise picking by pixel makes. - A frame that fails to build answers its questions too. The catch that
answers a pending
pickPixelwith the frame's failure covered the passes but not what came before them — an application node reading a name nothing writes fails in the graph's compile, and a question taken off the renderer there was on no list anybody would ever finish. Both catches now answer it, and the test is a node with a misspelled read. - The exposure meter asks once per answer. A readback in flight is not
joined by another: the luminance pass still runs every frame, the copy and
the download behind it are skipped until the last one has landed. On
flutter_gpu each answer is a
toByteDataoff a staging texture, and a meter asking every frame kept two or three of those in the air and a staging pool to match; an exposure that adapts over seconds cannot tell a reading every frame from one every other frame. - A device that refuses on the spot costs the question, not the frame.
Both readbacks are asked through
Future.sync, because a refusal is synchronous by contract —readbackRegionOfthrows before a future exists — and the backends refuse that way on their own account too: WebGL2 gives no fence for a lost context, flutter_gpu throws rather than returning false when a copy is turned down. Asked bare, the first of those came out of the luminance or the id node and took the whole picture down over a click, and the exposure meter's in-flight flag stayed set for ever, so it never asked the device again anddebugMeterFailuresstayed at nought — the one number that would have said so. - A blended surface is picked as though it were opaque, written down
rather than left to whichever way the pass fell. Glass, a translucent
marker, an additive flash answer with themselves and not with what is seen
through them:
MASKsays "there is nothing here" and is discarded in both passes,BLENDsays "there is something here, faintly", and that is still a thing to click on.Renderer.pickPixelsays so and a software-backend test holds it — a red pane at half alpha over a box, the click answering the pane while the picture shows the box through it. FrameResult.exposuredefaults toRenderSettings.defaultExposurerather than to its own copy of the number.example/lib/surface_probe.dartmeasures flutter_gpu'sGpuImageSurfaceagainst theasImage()pathpresentuses, andsurface_probe_main.dartruns it the wayconformance_main.dartruns the conformance suite: an application, because Flutter GPU needs Impeller and a headless test does not have it. In the example rather than in the backend because it reaches flutter_gpu directly, and an instrument is not part of a backend's API. It runs the ring twice — as the renderer has it, and holding the presented frame back one frame more, which is the promise the surface makes and the only version whose texture count compares with it — and its allocation control runs over the resize as well, so a pool that has not shrunk means the surface kept something rather than that the collector had not run. Its report and arithmetic are held by a headless test inexample/test.- The ring of finished frames says what its accounting tracks. The
callback that returns a frame texture to rotation fires when the
renderer's own GPU work is done, which is before the compositor has
sampled the texture; the note at
_ldrFramesnow says so, with what the probe measured, what the ring could do about it, and why it has not. - X-ray silhouettes.
RenderSettings.xraynames a layer, and every visible node on it is drawn twice more at the end of the scene pass: once to mark the stencil where it is visible, with no colour and no depth write, and once as a flat unlit colour where it fails the depth test and the stencil says no marked node's visible part is there. The stencil is what keeps the lit half of a half-hidden monster lit and one monster from painting through another; every mark is written before any paint. Both draws go through the same node encoder as the lit one, so skinning and batches come for free. Nothing is emitted when the mask is zero, the device has no stencil, or nothing visible is on the layer — every other golden is the bytes it was.stencil-xrayjoins the set on all three backends. Neither extra draw writes the surface buffer: they useLightingModel.xray, which is unlit with its second output compiled away, so a silhouette describes no surface to ambient occlusion or reflections — and it cannot, since it is drawn precisely where its node is behind something else. A software test holds one scene's surface buffer byte-identical with silhouettes on and off. - Reflection probes.
ReflectionProbeNodeis the scene seen from a point: six views drawn into the faces of a cube throughColorTarget.face, convolved into a roughness chain on the device throughColorTarget.mipLevel— the same lobeEnvironmentMap.prefilterbuilds on the host, as a full-screen pass per face per level — and read by the physical model of the nearest mesh whoseradiusit reaches, one probe per object and no blending. A kept probe is drawn once and again oninvalidate();refreshFaceEveryFrameredraws a face a frame after the first six.Scene.probesis the registry;probeFaceViewProjectionis the camera each face is drawn through, mirrored in x because the cube-map table is left-handed, and with y negated as well on a bottom-left backend — a half turn there rather than a mirror, so the winding flips on one origin and not the other.probe-caris a golden scene: recorded for Impeller and the software rasteriser, and recorded for the web set at the merge, which is when that set is written. Not built on a device whosesupportsRenderToMipis false; the material then reads what it read before. - A probe is read at its own strength.
ReflectionProbeNode.intensity, one by default, is what the surfaces that read a probe scale it by — notScene.ambientIntensity, which the sky environment shares with the flat ambient. A probe is the room's light measured, and a crypt whose ambient sits at six per cent would otherwise reflect its walls at six per cent. The strength travels in the slot the flat ambient already uses, since a draw reads one term or the other.isCapturedsays when every face stands at the current generation, so a level can hold its visibility culling until its kept probes have seen the whole of it. Measured on the racing demo's player car, a rolling probe costs the frame two milliseconds of build on Impeller and one in Chrome, and nothing in the raster half. - A lightmapped draw reads no probe, which is the same "one term or the other" rule the strength above follows, from the other side. A lightmap holds this surface's indirect light per texel; a probe's roughest level is a coarser answer to the same question, and the lit models add the lightmap on top of the environment rather than choosing — so a crypt wall that took both counted the room's bounce twice, once baked and once captured. The walls keep the bake, which is finer than a probe can be, and the probe lights what the bake does not reach: props, enemies, anything skinned or batched. What a wall gives up is its specular lobe, and a rough dielectric's is very nearly nothing.
- One whole cube a frame, across the scene. A level with a probe in every room used to capture all of them on the frame the level appeared — four probes is twenty-four views of a level the visibility culler is still holding off, inside one frame, at load. Now the first probe that has none takes the frame and the rest wait their turn, so a level stands over as many frames as it has rooms with a probe and the cost is paid in frames drawn without culling rather than in one very long one. A probe waiting its turn binds nothing rather than an allocation nobody has drawn into; one whose chain is merely stale keeps showing it. A rolling probe's single face is not rationed.
- The environment cube is sampled with a linear mip filter, so a roughness slides between levels rather than snapping, and so the backend that folds the mip filter into minification reads the level the shader named at all.
0.4.2 #
- Lightmaps.
MeshNode.lightmappedpicks a vertex stage that reads the colour attribute as a place inMaterial.lightmap, and every lit model addsalbedo × lightmapbeside its ambient, with a one-texel black bound where a material has none so nothing branches and every picture without a map is the bytes it was.lightmapped-roomis a golden in all three sets. - KTX2 is read. A pure-Dart reader for the container, and a port of Basis
Universal's ETC1S transcoder verified level by level against the encoder's
own unpack of three real files: mip chains and alpha slices included, run
on an isolate where there is one.
uploadEncodedImagesniffs the format beforedart:uisees the bytes; a Basis file arrives as RGBA8, and a file carrying its own BC, ETC2 or ASTC blocks is uploaded as those blocks, chain and all, onceGraphicsDevice.supportsTextureFormathas said the device samples them. uploadEncodedImagetakes areportcallback and says why an image was left out — a refused supercompression scheme, a family the device does not sample, a size that is not whole blocks.ModelAssetand the material loader route it into theirwarnings.- glTF reads
KHR_texture_basisu: a coresourcewins while it exists, and the extension's KTX2 is what a file that ships only that falls back to. The extension may be required without the file being refused. - Still refused by name: UASTC, Zstandard and ZLIB supercompression, texture arrays, cube maps, 3D textures.
0.4.1 #
- A minimal example.
example/lib/minimal_main.dartis one sphere and one point light in seventy lines, opened throughflutter3d_backendthe way a new application would — andexample.mdputs those lines on pub.dev's Example tab in place of the full model browser. A smoke test runs the file headless through the software fallback, so the claim in its comment is a thing CI checks rather than a thing the comment says.
0.4.0 #
- A pass's scene is lit by its own lights.
encodeSceneused to encode whatever scene it was handed with the frame's light buffer, gathered from the world at the top of the frame — so a view-model studio's two lights were never uploaded, every held weapon drew near-black, and the studio's light indices read shadow-atlas rows assigned to the world's torches. A scene other than the frame's now gets its own lights gathered into a pass buffer and the no-shadow table; the frame's own scene reuses the frame's buffers unchanged. - Everything the engine creates, something now releases.
ModelAssetgainedrelease, giving meshes and maps back with identity dedup — surfaces share meshes, materials share maps, and one image can sit in two slots of one material.Renderer.disposereleases the window-sized targets and drains both frames-in-flight rings instead of leaving them to a collector that, on WebGL2, deletes nothing. The texture upload path disposes itsui.Codec, which had leaked a decoder per decoded image.
0.3.0 #
- A point light's normal offset is measured in texels, not metres, which is
what it was always compensating for: a texel of the shadow map covers a patch
of surface, records it at one distance, and every fragment in that patch which
is not the point measured compares against a distance from somewhere else. The
patch is a solid angle and grows with range, so a flat 2 cm was right close to
a lamp and a third of what was needed ten metres out. What it looked like: the
floor under the golden teapot shadowing itself everywhere the lamp reached,
ending in a straight line where the floor's own edge projects — a straight
edge across a shadow in a scene with no straight edges in it.
pointNormalOffsetnow defaults to 1.5 texels. - The static cube-shadow bake now redraws when the settings that decide it
change. It redrew when a light took another's atlas row and at no other
time, so
casterFaces,depthPaddingand the cube tile size could be set and nothing happened — measured on the crypt as two frames identical to the pixel under oppositecasterFaces.StaticBakeKeyandshouldBakeStatichold the rule, and exclude everything the lookup reads: a bias must not cost a re-bake. - Six point lights may cast at once, not four. The crypt hangs six torches, so two of them lit their corner and cast nothing — and which two changed as the player walked, because the rows go to whatever matters most from where the camera is. Two torches side by side behaving differently is what a player reads as "the shadows are broken". Affordable only after the line below: at the old cube tile the extra rows would have cost 100 MB, and they cost 25.
ShadowSettings.cubeResolution, defaulting to 512, so a cube face no longer inherits the cascade's tile size. It did, and the atlas is thirty-six tiles: a game asking for a 1024 sun was allocating 201 MB per atlas and there are two. The golden sets did not move when it changed.- The Khronos sample models are no longer this package's assets. They moved
to
flutter3d_samples, a dev dependency here: declared influtter.assetsthey were 4.1 MB in every application that depended on the engine — a third of the shooter's web asset payload, none of it ever loaded — and four fifths of this package's own archive. The decoder tests read them from disk as before. .fmat: a material as a file of its own, withMaterialDecoderas the boundary for formats the engine does not ship. Consulted before the built-in reader, so a project can replace it rather than only add to it.- Image-based lighting:
EnvironmentMap.prefilterbuilds a specular chain and a diffuse level from a cube map or from sky settings, over a fixed golden-angle spiral so two independently written backends agree exactly. LookSettingsin the composite pass: colour grading, vignette, grain and chromatic aberration.- Materials carry a parameter block, numeric parameters and extra texture slots, so an application's own shader has somewhere to read from.
0.2.0 #
- Cascaded directional shadows, cube shadows for point and spot lights into an atlas, screen-space reflections and ambient occlusion, a sky and two-colour ambient light.
- A frame graph that culls the passes a frame does not need, and a frame resource ring that returns a target when the GPU says so rather than a fixed number of frames later.
- Instancing and CPU-built mip chains, uploaded level by level because no backend can be trusted to generate the same ones.
- Written against
flutter3d_hardwarethroughout, so the backend is a value a caller hands in rather than a compile-time choice.
0.1.0 #
- A CPU geometry layer with no GPU in it: vertex layouts, meshes, and shapes as values — surfaces of revolution generate the sphere, cylinder, cone, torus, capsule and disc.
- A scene graph whose transforms are held by version counters rather than dirty flags, so a stale world matrix is structurally impossible.
- glTF 2.0 / GLB and Wavefront OBJ behind one document abstraction, plus
.f3d, the engine's own container: the teapot loads in 1.1 µs instead of 4.54 ms, and the two render pixel for pixel. - Six lighting models, each a pre-built shader; directional shadows, an HDR pipeline with tone mapping and bloom, skinning, animation, BVH culling, LOD and CPU picking.