flutter3d_core 0.7.4
flutter3d_core: ^0.7.4 copied to clipboard
The engine's rendering core with no Flutter SDK behind it: scene graph, animation, the render graph, and model/material loading down to the one call each still needs an injected reader or decoder for.
0.7.4 #
Every effect below was checked against the paper or engine it comes from, and
most of what was wrong was wrong the same way: a number in the right units at
one distance, one resolution or one substep count. Pictures move, and the
goldens that show them were recorded again on all four backends. Upgrade
flutter3d_impeller, flutter3d_webgl, flutter3d_webgpu and
flutter3d_cpu with this: the renderer binds uniforms (BloomInfo.tint,
ShaftInfo.sun) that only their 0.7.4 shaders declare.
- AgX is AgX, and it is linear.
TonemapCurve.agxreturned its sigmoid's display-encoded value as if it were linear, and the composite encoded it to sRGB a second time: 18% grey reached the screen at 187/255 rather than 128, and a red velvet (0.6, 0.07, 0.1) came out pastel. It is now Wrensch's "Minimal AgX" whole — inset, sigmoid, outset,pow(2.2)— without themix(luma, v, 0.84)that took a sixth of the saturation off every frame.TonemapCurve.agxFulldraws the same picture and is deprecated. - A double-sided material casts its shadow from both sides.
MeshNode.castsShadowFromEveryFaceis new: true forShadowCastingMode.doubleSided, as before, and now also for a node whose material isdoubleSided, as Godot, Filament and Unity take the shadow pass's cull from the material. A sheet draped over a ball threw a detached crescent — the half of it facing away from the light — and every double-sided leaf card cast only its back. The cascade and cube caches notice the change without the node moving. And the back of a double-sided surface is now lit from its own side:gl_FrontFacingreverses the normal, as glTF asks, in the lit shaders and in the surface buffer, and the tangent frame of a normal map turns with it. - The sun's normal offset grows with the cascade's texel and the slope. A flat two centimetres was enough while a closed mesh recorded only the faces turned away from the sun; a double-sided one records its lit faces too, and a cube shadowed itself in a diagonal along its triangle split. The offset is now that distance plus one texel of the cascade scaled by the slope to the light, the measure point and spot shadows already took.
- A long shadow no longer ends in a straight line. The last cascade's depth is fitted to the casters, and a floor past its far plane was treated as outside the map and lit: an evening shadow was cut off where the far plane met the ground, straight across the teapot's. Past the far plane is behind every caster, and the last cascade now clamps there, in the lit shaders and in the light shafts.
- The sun's shadow survives a light list gathered per draw. A node on a light channel, or in a scene of more than eight lights, gets its own list, and the shader was told the caster's index in the frame's list: the sun's shadow vanished from that node and a lamp at that position was tested against the sun's cascades.
- Shadow caches notice what they missed. A swapped mesh, morph weights,
an instance moved inside a batch, and
ShadowSettings.casterFaceschanged at runtime redraw the cascade and the cube faces that show them, and hiding a static caster drops it from the static bake. - Screen-space reflections lose their ghosts. A floor of roughness 0.3
showed shifted, smeared copies of what stood on it. The march now starts at
a jittered fraction of a stride and halves the last stride five times
before it reads a colour (McGuire and Mara); a hit fades with the length of
the ray; a surface turned away from the ray is not a hit; roughness removes
the reflection by 0.25 rather than 0.45, since this pass has no blur; the
Fresnel term is Schlick's with F0 = 0.04 rather than a 15% floor; and the
surface buffer is read nearest. Defaults follow Filament:
steps32,stride0.1,thickness0.12. - Light shafts scatter the sun's light instead of veiling the frame. They
added
strength × colourin proportion to how much of a ray was lit — a flat 0.15 over nearly every pixel of a daylight scene. They are now single scattering with transmittance and a Henyey–Greenstein phase, in the caster's own colour and intensity: bright towards the sun, faint with it behind.LightShaftSettings.strengthis now the air's density per metre (default 0.005, a clear day's haze),anisotropyis new (default 0.6), andcolortints rather than supplies the light. The cascade is chosen by distance from the eye, asshadow.glslchooses a surface's. - Dithering is on by default, and centred.
LookSettings.ditherdefaults to one 8-bit step, which is what turns the coloured rings round a small bright lamp's bloom back into a gradient. The Bayer cell's mean was -1/32 of a step; it is nought now, so a flat colour stays the colour it was.LookSettings.isNeutralno longer asks about it. - The grade does what its documentation says. Contrast pivots on linear
light's mid grey (0.18, as a power) rather than on 0.5, which in linear light
is a bright highlight — a contrast of 1.2 also took about a stop off. Lift is
c · (1 − lift) + lift, so white stays white. A LUT is indexed and answered in sRGB, the space a.cubeis written in. - Bloom's halation warms each level once. On the way up a level already
holds every level below it, and warming each compounded: at 0.5 the widest
came out red 1.78 and blue 0.63 rather than 1.25 and 0.83. Each step now
carries the ratio between its level and the one above.
BloomSettings.scatteris new — each level weighsscatter^level; one, the default, is the bloom this has always drawn. The first step down averages its four taps with Karis's1 / (1 + luma), so a one-texel highlight no longer flickers. - Depth of field. The test of whether a sample's disc reached the pixel was
r <= max(tapRadius, radius), which always held, so a sharp object bled into the blur behind it; it now keeps a sample only as far as its own disc reaches. Nothing drawn — the sky — is infinitely far, so it blurs as the far field does rather than staying sharp. The spiral turns per pixel. - Contact shadows start each step at a jittered point, so eight steps are
not eight flat bands across a penumbra, and accept a blocker within twice a
step's own depth as well as within
thickness. - Ambient occlusion's blur reads the surface buffer nearest and weighs
depth relative to the centre's own:
AmbientOcclusionSettings.blurDepthFalloffis a fraction of depth now, default 0.02 (the old ten centimetres at five metres). - Contact shadows march without a shadow map. They took their direction
from the light that casts the map, so clearing
castsShadowon the sun, the cheap setup the pass exists for, switched them off as well. They now follow the first directional light when no light casts one. - Depth of field reads depth nearest. A filtered tap at a silhouette against the sky mixed the object's depth with the sky's zero, and the gather spread the sharp object into the blurred sky around it.
- Bloom's
halationis read up to 2.5 and a negativescatteras zero. Past 2.86 the blue weight divided by zero and the pyramid filled with NaN. ReflectionSettingssays what the pass cannot know: the surface buffer holds no metalness, so every surface reflects as a dielectric.
0.7.3 #
- A material's own vertex stage gets the morph state when it declares it.
0.7.2 fixed
Material.polylineon Impeller by binding morphs to a material's own stage only when the node morphed. A stage written fromMeshVertexdeclares the morph block and texture either way, so on a plain mesh it went without them: another draw's weights on the software backend, garbage on WebGL, a native failure on Metal. The bind now followsLightingModel.vertexStageMorphs, which is new, true by default, false forLightingModel.polyline, and round-trips through.fmatasvertexMorphs. MaterialBindings.lightingModelbuilds the model a material in the language needs. Every flag comes off the program,usesLightList: falseincluded. A hand-built model that samples maps defaulted the light list on, and the emitted stage never declares it.
0.7.2 #
Material.polylinedraws on Impeller. The renderer bound the morph block and texture to whichever vertex stage drew, andPolylineVertexdeclares neither. The missing block was skipped, but flutter_gpu refuses a texture bound to a slot the stage does not have, so every polyline threw "Failed to bind texture" at its first draw on macOS and iOS, in 0.7.0 and 0.7.1 alike. WebGL and the software backend let the extra bind pass. A material's own vertex stage now gets the morph state only when the node morphs; the engine's four mesh stages still get it on every draw. Found by drawing an unlit sphere, a polyline and a lit cube one at a time on Metal.
0.7.1 #
-
A surface with no normal map is flat again, whichever way its tangent runs. The fallback normal texture stores 0.5 as byte 128, which decodes to 0.0039 rather than 0, so every map-less material was tilted by about a third of a degree along its tangent and its shading depended on the tangent's direction. The renderer now sends a normal scale of zero with the fallback, which leaves exactly (0, 0, 1) on every backend. Setting
normalScale: 0by hand is no longer needed for that. -
Scene.defaultLightWhenUnlit, a switch for the light nobody added. A scene with no live light is still lit by one directional light by default. A light counts as live only when it is visible and above zero intensity, so turning off the only lamp used to switch a bright default light on. Set the flag to false and an unlit scene stays unlit, with no black light parked in it to keep the count above zero. -
Material.baseColoris documented as what it is: sRGB in rgb, linear in alpha. The doc said linear, and every shader converts it withtoLinear, so a colour converted by hand went through the curve twice. Nothing about the rendering changed. -
An unlit draw no longer crashes Metal on the first frame. 0.7.0 bound the light list, the
LightListInfoblock andlight_list_texture, to every draw. The Unlit stage (and the polyline, which uses it) gathers no lights and keeps neither in its compiled Metal function, so the bind went to a slot index the driver does not have and the process died insidesetFragmentBuffer:offset:atIndex:on macOS and iOS alike. Vulkan accepted the same bind, which is why Android drew. The list is now bound only whenLightingModel.usesLightListsays the stage reads it. That is new, defaults tousesMaterialMaps, round-trips through.fmataslightList, and is false for anything the material language emits. -
The rest of a full read of the engine. A failed frame no longer keeps its texture out of rotation or leaves the timeline block open; a new shadow resolution, a resize,
disposeandFrameResources.providerelease what they replace. A flickering torch past the first eight lights keeps flickering, a spot light taking over a point light's atlas row clears its tiles, and a replacedSkydraws.lookAtand the IK solvers work under a scaled parent, a skinned mesh is picked in its current pose and an instanced batch against every instance, and morphs and skinned bounds stop culling a visible mesh.Scene.clearis linear.Material.copykeeps the parameter block, parameters and extra textures. -
The decoders refuse a bad file instead of hanging or reading past it. meshopt, sparse glTF accessors,
.f3drecord counts, zstd and HDR sizes and KTX2 universal levels are checked, each with its ownFormatException. meshopt filters are refused by name rather than decoded as noise, OBJ keeps corners with different UVs apart on the web, smooth OBJ normals follow positions across UV seams, USDZ flips winding under a mirror, JPEG decodes non-interleaved and multi-scan files, and.fmatkeepsmipLinearand a custom vertex stage. -
LightNode.castsShadowis read on the sun too. The renderer cast the first directional light whatever its flag said, because only the cube shadows read it, so "this sun does not cast" could only be said by turning shadows off for the frame. It now picks the first directional light that asks. The default follows the type: left out of the constructor, it is true for a directional light and false for the others, which is what every scene already drew. A sun built withcastsShadow: falsestops casting.
Its flutter3d_* dependencies ask for ^0.7.1, and it asks for vector_math ^2.4.3, image ^4.10.1.
0.7.0 #
-
The first publication: the engine with no Flutter SDK behind it. The 0.1.0 below was a number carried inside the workspace and never reached pub.flutter-io.cn. 0.7.0 is the number the whole shelf goes out on, so one number names one tree and
^0.7.0on anyflutter3d_*package resolves against every other; publishing first at 0.7.0 skips numbers nobody outside ever saw, anddoc/boundary-0.7.0.mdlists the thirteen packages that begin here. This is the renderer, the scene graph, animation and the asset layer thatflutter3d0.6.0 held, moved out whole (mcp-03n), with everything written since. It depends onflutter3d_hardwareandvector_mathand on nothing else, andflutter3dre-exports it, so an application importingpackage:flutter3d/flutter3d.dartkeeps every name. What follows is what changed against the engine insideflutter3d0.6.0. -
Accepted
flutter3d_geometry,flutter3d_formatsandflutter3d_fbx. They arepackage:flutter3d_core/geometry.dartandpackage:flutter3d_core/formats.dartnow, each importable on its own and both exported fromflutter3d_core.dart;FbxDecoderis part of the formats library. All three were plain Dart withvector_mathas their only third-party dependency and none was published, so the package boundary gave a caller that wanted a mesh or a.glbwithout the renderer nothing a library does not. Their tests, tools and skills moved with them; the skills areflutter3d-core-geometry-meshesandflutter3d-core-formats-reading-models.FbxDecoderrecognises an FBX file by its magic or its extension and refuses every one with aFormatExceptionthat says to export glTF; the reader is not written.flutter3d_core.darthides the formats library'sKtx2Texture, which carries a rawvkFormat, behind the engine's own, which carries aTextureFormat. -
What a file written against
flutter3d0.6.0 has to change.uploadEncodedImagerequiresdecodeImage, anImageDecoderanswering anRgba8Image, because this package cannot calldart:ui.flutter3dhasdefaultImageDecoder, anddecodeImagePurehere reads PNG and baseline JPEG with no SDK at all. Three enums gained a value, so an exhaustiveswitchover one stops compiling until it answers it:LightType.area,MaterialAlphaMode.hashedandModelFormat.stl.GraphicsDevice.presentis gone fromflutter3d_hardware, which this package re-exports; a frame is shown throughpresentFrameinflutter3d_app.SceneNode.visibleis a setter over a cached value where it was a field, so hiding a branch reaches everything under it.AssetSource,FileAssetSourceandfileUriResolverare here, andBundleAssetSourceandassetUriResolver, which name Flutter's asset bundle, stayed influtter3d. -
The frame says what it did.
FrameResult.passesis oneFramePassper node the compiled graph ran, in order, withmicros,drawCalls,trianglesandpipelineSwitches(gfx-01n).FrameResult.trianglesandinstancescount the scene's meshes, an instanced batch once per instance.skippedlists each pass that did not run with aPassSkipreason: off by its own setting, disabled by name, unconsumed, starved, orunsupportedon this device.PassSkipis a final class with constants, so a sixth reason will not break aswitch.antiAliasingreports what smoothed the edges and why multisampling was declined, which is one of two things: the device, or a pass in this frame that reads the surface buffer. Counting per pass found thatdrawCallshad never included the shadow map or the bloom chain; a directional fixture that read 4 reads 13. -
Passes are addressed by name.
RenderSettings.disabledPassestakes node names, and the graph refuses a name no node carries, so'fxaa'for the node calledantialiasis an error and never a silently unchanged frame.RenderSettings.passOrderpublishes the names in registration order,undisablePassesthe three the graph will not drop (scene,composite,object ids), andprobePassNamethe name of a probe's pass.Renderer.planFramecompiles the graph for a frame without drawing it and answers theCompiledFrameGraph, with its own light buffer and shadow slots so that a frame drawn after a plan is the frame drawn without one.RenderSettings.forMeasurement()is the complete set for reading values off a debug view: tone map off, exposure 1, bloom off. It replaces three hand-built copies that left bloom on, which moved theanimation-weightsgolden. -
A frame captured pass by pass, and one pass measured.
Renderer.captureNextFrame()is a one-shot that answers aFrameCapture: eachCapturedPasswith its reads, optional reads, writes and keeps by the graph's own names, and an image of everything it wrote. A resource with no image says why: tile memory, multisampled, a cube, or a name nothing provided.FrameCapture.firstBlackfinds the first pass whose output came back black. It is exact on the software rasteriser; on the hardware backends a readback may resolve after the queue moved on.contributionBetweendifferences two frames into aPassContributionof four numbers: pixels changed, their mean change, the largest change and the bounds of the change. -
A caller's own post effect, and post-processing on a buffer from outside.
FullscreenEffectwraps the read-modify-write of the right resource, the transient target and the shared vertex stage; it has a name and anenabled, so it appears inpassesandskippedand answers todisabledPasseslike a pass of the engine's. Two constructors pick the phase.Renderer.renderPost(hdr, settings)runs bloom and the composite over an HDR buffer handed in, registered throughFrameGraph.addExternal, and answers aPostFrameResult; withkeepHdrit also returns the bloomed buffer. Reflections and ambient occlusion are not in that call, since both read the surface buffer. -
A second colour attachment is refused where opening it would abort. On Impeller's OpenGL ES path a second attachment reaches an
FML_CHECKand the process stops, in release too. The five nodes that read the surface buffer are culled on a device whosemaxColorAttachmentsis 1 and reported asPassSkip.unsupported, and the scene pass attaches only what the device can open (gfx-50n). -
Culling and the work a still scene no longer does. The render list culls by the mesh's world box where it used the sphere around it, which for a wall or a fence reached up to sqrt(3) times further (
gfx-61n). A scene that moved refitsSceneBvhand no longer rebuilds it: 23 ms to 0.85 ms on 50 000 meshes, andRenderList.bvhThresholdis a field, 256 by default (gfx-62n).SceneNode.subtreeBoundslets a branch outside the frustum cost one test, withRenderList.consideredcounting them (gfx-66n). Each shadow cascade and each cube face culls its casters against its own frustum (gfx-63n).worldMatrixandvisibleInHierarchyreturn on one integer comparison againstSceneNode.changeEpoch, andSkeleton.updatereturns at once for a pose it already computed: 16.4 us to 4.7 us on 64 joints (gfx-64n,gfx-65n). The directional shadow pass is cached on the cascade matrices, the change epoch and the casters' materials, so a still camera over a still scene draws no cascade (gfx-68n). None of the forty-four golden scenes moved. -
Identical draws batched, resolution as a setting, and memory given back.
RenderSettings.batchIdenticalDrawsmerges a run of four or more opaque nodes that share a mesh, a material, mirroring and a reflection probe into one instanced call, andFrameResult.batchedDrawscounts them; it is off by default because it was compared byte for byte on the software rasteriser only.InstancedMeshNodegainedensureCapacityandclear.RenderSettings.renderScale, clamped to [0.1, 1], sizes every target of the frame, andFrameResult.frameis the smaller texture, which a presenter already stretches.AdaptiveScaleis the policy over it, driven byFrameResult.cpuMicros: down above 1.1 times the target period, up only below 0.7 times it and after six clean windows.Renderer.releaseTransientTargetstrims the render target pool, andMemoryPressureReleaseinflutter3d_appcalls it on a memory warning. -
More than eight lights reach one draw. Twenty-four more come from a texture holding every scene light, with a per-draw block of row numbers and fade scales; a draw with eight lights or fewer never reads it. The tail casts no shadow.
FrameResult.lightsDroppednow means light the frame could deliver nowhere (gfx-74n).RenderSettings.lightFadeBandturns the edge of a draw's light list into a smoothstep ramp, and 0, the default, is the old hard edge byte for byte; on the fixture built for it the change between neighbouring frames goes from 3.16% to 0.23%.LightNode.channelsandSceneNode.lightChannelsare bit masks compared where the lights of a draw are chosen, so a light off an object's channel does not take one of its slots. -
LightType.area: a rectangle that lights like a rectangle. The diffuse term is Lambert's polygon form factor, exact and with no table; the specular term is the representative point, and its known error is a lobe not widened by the panel's solid angle. It takes over the two per-light arrays a panel has no use for, so it adds no bytes to a draw. The brute-force quadrature it is tested against agrees to better than half a per cent across five arrangements (gfx-77n).Photometricconverts betweenLightNode.intensityand lumens, candela and lux, anchored so that an 800-lumen point lamp is intensity 1. Nothing applies it automatically. -
Shadows. A cut-out caster casts its cut-out:
ShadowDepthMaskedandShadowDistanceMaskedtestMaterial.alphaCutoffagainst the map's alpha times the base colour's, on the directional and the point path (gfx-60n).ShadowSettings.directionalLightRadiusabove 0 searches for the blocker and widens the penumbra with its distance; 0 is the 3x3 kernel as before.ContactShadowSettingsmarches sixteen steps toward the sun against the depth the scene wrote, at the frame's own resolution, fades a hit with its distance along the march, and declines when nothing directional lights the scene (gfx-76n). The cascade atlas and the cube atlases own their depth buffers. They took one from the pool each frame, and Impeller's Vulkan backend caches a framebuffer keyed on the colour attachment alone, so the second pass drew into the first pass's depth or crashed; reported upstream as flutter/flutter#192538. -
IrradianceField: one bounce of diffuse light from the room itself. A grid of probes, each an octahedral tile of irradiance with a gutter and the two depth moments Chebyshev's inequality needs to stop light crossing a wall. A probe is gathered with rays through the engine's raycaster,gatherandgatherProbe, and a probe inside geometry is found by counting rays that land on the inside of a surface. It reaches a draw through the ambient uniform that already existed, sampled facing up and facing down per object, so a large floor reads one point of the field. A scene with noScene.irradianceFieldis byte for byte what it was (gfx-81n). -
The composite: tone curves, a colour table and a three-way grade.
RenderSettings.tonemapCurveis aTonemapCurveofneutral,aces,agx,reinhardoragxFull, the last with its gamut rotation, andneutralis the old curve value for value.LookSettings.luttakes a strip of N slices of N by N withlutStrength, andbuildIdentityLutmakes the neutral one; an identity table was measured to change no byte of an eight-bit frame.lift,gammaandgainare colours,whiteBalanceandtintsit beside the oldertemperature, andditheris a 4x4 ordered cell added after the sRGB encode: a 512-pixel dark ramp went from 39 flat runs to 255. Every default is an exact identity. -
Anti-aliasing, occlusion, bloom, a lens, shafts and per-view exposure.
AntiAliasSettingsis an FXAA pass after the composite, for the frame in which anything reads the surface buffer and multisampling is therefore off;sharpenrides in it and leaves a flat area untouched by construction.AmbientOcclusionSettings.blurTapsandblurDepthFalloffadd a blur weighted by depth, so occlusion does not spread across a silhouette.BloomSettings.referenceHeightkeeps a glow the same share of the picture at any resolution throughbloomLevelsFor, andhalationwarms the wide levels of the chain and leaves the core.DepthOfFieldSettingsis a thin lens:focusDistance,focalLength,apertureas an f-number andsensorWidth; it is a gather, so a foreground blur does not spread over a sharp background.LightShaftSettingsmarches the view ray against the directional shadow map sixteen steps a pixel and draws nothing when there is no caster.AutoExposureSettings.perViewmeters and exposes each view of a split screen from the one luminance readback. All of them are off by default. -
Viewport shading read out of the surface buffer.
RenderSettings.viewportShadingtakes aViewportShadingSettingswhose mode isnormals,clay,outlineorcurvature. It is drawn in the present phase from the normal and depth the scene pass already writes, so no node's material is swapped to show it (gfx-43ntogfx-45n). -
Materials: a lighting model per surface, a vertex stage, hashed alpha and a source language.
SurfaceMaterial.lightingModelpicks one of the six models by shader name;.fmatreads and writes it, glTF and OBJ have nowhere to carry it, and.f3d's fixed material record has no room for it yet (mat-04).LightingModel.vertexShaderNamenames a vertex stage an application supplies throughRenderer.create(materials:): the name is the unskinned entry point and<name>Skinnedthe skinned one, and the pipeline cache key includes it. Instanced and lightmapped draws keep the engine's stages (gfx-75n).MaterialAlphaMode.hashedkeeps a fraction of pixels equal to the opacity, hashed on world position, in the opaque half with depth written and nothing sorted; picking treats it as a mask at one half.parseMaterialreads a material written as typed parameters and a fragment body,emitMaterialFragmentwrites GLSL from the tree,evaluateMaterialruns the same tree on the CPU, anddescribeMaterialreads the bindings off it. The language has no uniform block, no loop, no#defineand no vertex body (gfx-84n). -
A route as one line.
buildPolylinewrites each point twice with its neighbours, a signed half width and a colour, andMaterial.polylinepairs thePolylineVertexstage with Unlit, so a line is widened in the vertex stage and a camera move rebuilds nothing. Joins are mitres held at four half widths.Material.parametersnow reaches a vertex stage the material brought as well as a fragment stage. The same commit fixedFrameInfobeing bound through the engine'sMeshVertexhandle when the pipeline's vertex stage was the material's own (gfx-86n). -
A captured cloud of Gaussians loads, sorts and draws.
parseSplatPlyreads the binary PLY a capture ships as, including the three conventions its header does not state:opacityis a logit,scale_*are logarithms andf_dc_*are the zeroth spherical-harmonic band.SplatCloudholds the splats as flat arrays andSplatContributordraws them as quads blended back to front with no depth write. Two approximations are stated in the code: the projection drops the perspective Jacobian, and the higher harmonic bands are skipped, so a splat does not change colour with the viewing angle. The CPU rebuild measured 359 ns a splat per camera move (gfx-80n). -
Cameras and projections.
OffAxisProjectiontakes four tangents, which is what a headset hands over per eye, andProjection.verticalFieldOfViewanswers null where the question does not apply;LodGroupand the shadow cascades ask it and no longer fall back to 45 degrees or 200 metres.RenderSettings.forStereo()takes out what a pair drawn side by side cannot have: ambient occlusion, reflections and bloom.TiledProjectionanswers for one tile of a frame larger than any render target, checked by a 2x2 stitch against one whole render byte for byte.OrbitControllerhas anorthoHeightkept in step with its distance,animateTo(yaw:, pitch:)withadvance(seconds), and aframeBoundsthat lowersminDistanceto a tenth of the radius it frames, so a millimetre-scale model can be framed.FreeLookturns the head and walks over the same yaw, pitch and target. -
An overlay that sits on the surface, and a skeleton on screen.
MeshOverlaydraws edges, vertex handles and a face wash in three batches and three draws whatever is in them, with handles and the nudge toward the eye sized in pixels. Design colours are converted on the way in, because the overlay is encoded inside the scene pass and the composite encodes on the way out.throughGeometrywrites into a second pass with no depth test atthroughOpacity, for a gizmo whose pivot is inside the object.DebugDrawOptions.skeletonsdraws every skinned mesh's joints throughaddSkeletonOverlay: an octahedron per bone and a cross at each leaf. -
Poses, skinning and IK without a scene.
Poseholds a hierarchy's local TRS in flat arrays, samples a clip, composesworldMatricesandjointMatricesby the formulaSkeleton.updatedocuments, andwriteTopushes it onto scene nodes.SkinBlendrepeats the skinned vertex stage on the CPU.TwoBoneIksolves from the chain's current bend toward a target and a pole, andFabrikIka chain of any length; a sign error inTwoBoneIk.solvefor a chain bent at rest was fixed with a test on a chain bent 90 degrees.AnimationPlayer.rootMotionDeltareads theflutter3dRootMotionextra of a clip, across the wrap. A layer can beAnimationBlend.additiveover a clip whoseAnimationClip.referenceTimenames its rest frame; a clip without one gets the override path. -
Picking, screen space and vertices overwritten in place.
Raycastertests a skinned mesh where its pose puts it, throughPosedMesh, andRaycaster.posedturns that off; the early box rejection uses the posed box, and weights that do not sum to one are renormalised.TriangleBvhin the geometry library is new: a tree over one mesh's triangles, in typed arrays, withraycast,refit, andforEachInAabbandforEachInFrustum, which report by a triangle's box and so may report a few extra.screenBoundsOfBoxprojects a world box to a rectangle on the glass, and answers the whole viewport for a box the eye is inside.DeviceMesh.overwriteVertices(device, firstVertex, vertexBytes)writes into a live vertex buffer throughGraphicsDevice.overwriteGeometry, growsboundsto cover the new positions and bumpsversion; the box only grows. -
A panorama lights the scene.
readHdrdecodes Radiance.hdrin both scanline encodings to floats andhdrSizeOfreads the header alone.EnvironmentMap.equirectToCubeFacescuts six faces from a 2:1 panorama andEnvironmentMap.fromPanoramauploads and prefilters them. The cube is eight bits a channel, so a sun far brighter than its sky clamps to white. -
A model is written as well as read.
GltfWriterwrites a self-contained GLB: geometry, materials and samplers, skins, animation channels in all three interpolations, morph targets with their names, lights throughKHR_lights_punctual, cameras,extrason five kinds of object, and a Basis KTX2 image throughKHR_texture_basisu.compressGeometry: trueaddsKHR_mesh_quantization, vertex cache reordering andEXT_meshopt_compression, which the reader decodes too; with the index codec's edge history the measured fixture is 3.60 times smaller, and the output was checked against meshoptimizer 1.2.0's own decoder.StlWriterwrites binary or ASCII,ObjWriterbakes each surface's world matrix, andUsdzWriterwrites geometry only, oneMeshprim per surface, in an archive macOS identifies as USDZ. Every writer haswarningsnaming what its format cannot carry.exportToGlb,exportToObj,exportToStlandexportToF3danswer anExportReportwith the files, the warnings and the differencescompareModelDocumentsfound on reading the file back, morph targets included.validateGltfExportchecks each accessor's declared bounds against its data.ModelWriter,modelWriterNamedandencodeModelInIsolatewith aModelWriteRequestare the same from above. -
STL is read, and a document holds more of its file.
StlLoaderreads both dialects and tells them apart by the file's size arithmetic, since a binary header often begins withsolid; its document has a node for its surface, which it did not at first.ModelFormat.stl,sniffModelFormatandrecognizedModelFormatknow it.ModelDocumentgainedlights,cameras, anassetof typeDocumentAssetandextras;ModelSurfacegainedmeshNameandauthoredAttributes, the attributes read from real data;EncodedImagegainedsourceUri;TextureSamplinggainedmipLinearwithtoGltfFilters(); andModelNode.lodsholdsModelLodlevels, written to.f3das section 21 and to glTF asMSFT_lod, the latter checked only against this package's own reader. Every new.f3dsection is optional on read andkF3dVersionis still 1.surfaceMaterialToJsonandsurfaceMaterialFromJsonare the one codec for a material's fields. -
KTX2: supercompressed files open, and textures are encoded.
Ktx2Texture.parseunwraps Zstandard and ZLIB levels through a decompressor written here,zstd.dart, since every Dart zstd binds the C library and a web build cannot load one. Thirty structured and sixty fuzzed streams at levels 1 to 19 decode byte for byte, and the four bugs found on the way are in the commit forgfx-78n.encodeBc1,encodeBc3,encodeEtc2Rgb8andencodeAstc4x4encode,buildMipChainhalves with a box filter, andwriteKtx2writes the container. The ASTC encoder wrote a reserved block mode that ARM's astcenc decoded as magenta; it writes mode 0x53 now and is pinned to astcenc's output, with a worst channel error of 7 over a 64x64 gradient (gfx-88n).encodeUniversalBlockswrites a 4x4 intermediate of twenty bytes a block, andtranscodeUniversalturns it into BC1, BC3, ASTC 4x4, ETC2 or RGBA8;uploadEncodedImagepicks theUniversalTargetfrom what the device samples. Against encoding directly it costs ASTC nothing, BC1 0.6 dB and ETC2 0.9 dB, and ETC2 takes about a second per 1024x1024 level because that leg decodes and re-encodes (gfx-83n). -
Images without
dart:ui. A PNG decoder and encoder, a baseline JPEG decoder, DEFLATE and inflate are in the formats library,decodeImagePurewraps the two decoders in theImageDecodershape, andsniffImageMimeTypenames PNG, JPEG, KTX2 and WebP from their first bytes. -
A UASTC KTX2 opens (
gfx-78n). Whattoktx --uastc,gltf-transform uastcandbasisu -uastcwrite was refused by name — and by guess, since any undefinedvkFormatoutside Basis-LZ was taken to be one.Ktx2Texture.parsenow reads the data format descriptor's colour model to learn which Basis Universal a file holds instead of inferring it from the supercompression scheme, and unpacks UASTC LDR 4×4 — all nineteen modes — to RGBA8 inuastc_decoder.dart, through the same Zstandard and ZLIB unwrapping the plain formats use, since a current encoder Zstandard-compresses UASTC unless told not to. Unpacked rather than repacked to BC7 or ASTC: every file opens on every device, at four bytes a texel. A glTF that ships only aKHR_texture_basisuUASTC texture keeps it. UASTC HDR and the newer intermediate colour models are refused by name. Tables transcribed from the Basis Universal reference transcoder, and checked against it the only way worth having: files its own encoder wrote, chosen until every mode appears in them, compared byte for byte with its own RGBA32 output. -
A Draco-compressed glTF opens (
gfx-82n).KHR_draco_mesh_compressionwas detected, warned about and skipped, so a compressed file opened as a model with holes in it — and since every such file names the extension as required, most were refused outright before that.decodeDraconow reads edgebreaker connectivity, which is what every encoder writes unless told otherwise, in both its standard and valence traversals; both attribute walks; and every prediction scheme a bitstream 2.2 encoder can choose — difference, parallelogram, constrained multi-parallelogram, portable texture coordinates and geometric normals.GltfLoaderdecodes the payload and puts its values behind the primitive's own accessors through the newGltfAccessorReader.supplyDecoded, so morph targets, skinning and the writer read a compressed primitive as they read any other; joints stay the integers they were stored as. A payload that does not decode costs that primitive, and the warning carries the decoder's reason. Point clouds, bitstreams before 2.2, the predictive traversal and the two retired prediction schemes are refused by name. Checked againstgltf-transform dracoat its default speed and at speed zero — which are nearly disjoint sets of code paths — on a thousand-face mesh with UV seams and on a skinned one, triangle for triangle against the uncompressed originals. One bug in what was already there, found by that comparison: the rANS end-of-stream check compared the state without first pulling in the bytes the encoder shifted out before its first symbol, so a valid stream whose first symbol was rare was refused. -
KHR_texture_transformcan be honoured, in the coordinates.sharedTextureTransformnames the one transform every texture of a material asks for, andwithTextureTransformgives a mesh with that transform applied to its texture coordinates, tangents turned and mirrored with them. That is the case an atlas export writes, and it needs no matrix at the sampler. The decoder still applies nothing, so a document written out again is the file that was read. Its warning changes: it no longer fires for every texture that names the extension, only for a material whose textures name different transforms, which one set of coordinates cannot satisfy. A file that lists the extension underextensionsRequiredis still refused. -
A morph-target warning names its primitive. The three warnings the glTF loader adds when it drops a morph target had their interpolations escaped, so each said, literally,
$label has ${targets.length} morph target(s). Nothing failed because nothing reads a warning but a person. -
What it depends on, and what the archive carries.
flutter3d_hardwareat^0.7.0andvector_math.package:flutter3d_core/geometry.dartandformats.dartimport neither the renderer nor a device. The archive carriesskills/flutter3d-core-geometry-meshes/andskills/flutter3d-core-formats-reading-models/for a coding agent, installed withdart run skills@ get.
0.1.0 #
- The rendering core leaves
flutter3d(mcp-03n). Scene graph, render list, passes, materials and animation move here with no Flutter SDK behind them;flutter3dre-exports this package and keepsrootBundle,dart:uiand the widgets for its own thin shell.