flutter3d_core 0.7.0
flutter3d_core: ^0.7.0 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.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.