Repository navigation
Sync fork develop with upstream: per-eye shading, LOD hidden parts, IBL diffuse (#1285, #1324, #1329) - #72
Merged
Merged
Conversation
untoldengine#1316) * [Patch] Add animation playback helpers for synchronized gameplay Adds restartAnimation, getAnimationPlaybackTime, and sampleAnimationJointPosition so callers can replay a one-shot clip from time zero, read its current playback time, and sample a named joint in world space at an arbitrary time without changing the displayed pose. ClipSampler.sample() also unconditionally wrapped time via fmod, even for clips whose channels were flagged non-repeating, defeating the per-channel hold-last-pose logic and causing a completed one-shot clip to jump back to its initial pose. It now clamps to [0, duration] when no animated channel repeats, and only wraps for repeating clips. restartAnimation preserves warmed compiled-pose caches unless the clip's looping configuration actually changes. Fixes untoldengine#1314. * [Bugfix] Stop root motion from wrapping at a one-shot clip's duration applyRootMotion computed its own channelTime via fmod(currentTime, clipDuration), unconditionally, regardless of whether the clip's channels repeat. Once currentTime passed a non-repeating clip's duration, this wrapped back near zero, which wrappedChannelTime then misread as a fresh loop and injected a full loop's root displacement at that instant -- a visible snap right as a one-shot animation (e.g. a kick) finished, even though ClipSampler already holds the displayed pose correctly past that point. Match ClipSampler: clamp to [0, duration] when no animated channel repeats, only wrap when at least one does. * [Patch] Update animation clip sampler and root motion
…he mesh's default (untoldengine#1320) * [Bugfix] Export the material a slot linked to the object shows, not the mesh's default A multi-material object is split into one mesh per material before export. The split took each piece's material from the mesh data, so a slot linked to the object (Blender's slot link Object) exported the mesh's default instead of the material the object shows. An IFC import gives hundreds of objects one shared mesh and links each one's own material over it: in the BIM site 771 of 811 object-linked slots differ from their mesh's material, and every classroom chair came out with the red material its shared mesh holds where Blender shows wood. Both split paths now ask the object's slot first (slot_material), and a rigged piece's slot is left reading from its one data material. Documented in UsingTheExporter.md, checked in scripts/tests/blender/material_slot_checks.py: the plain split, linked duplicates with their own materials, the export preparation path and a rigged object. * [Patch] Resolve a material slot in one place: mesh_object_material goes through _slot_material The split paths and mesh_object_material resolved a slot the same way in two places (the object's slot first, the mesh data for what the slots do not cover). mesh_object_material now takes its primary lookup from _slot_material and only keeps its own fallback to the first material; the helper carries the file's underscore convention, since only the split paths use it. --------- Co-authored-by: Javier Segura <javier@miolabs.com>
…ith (untoldengine#1323) The occlusion test drops the entities that the depth pyramid of the previous frame hides. It projected this frame's entities with this frame's camera into a pyramid rendered from the previous frame's camera, with a fixed depth bias as the only allowance for the difference. Whenever a step uncovered what a near surface had covered (rising above the ground, backing away from a wall, strafing past a trunk), far entities landed on texels that still held the near depth and were dropped until the view settled: on a large site the trees vanished while the camera flew and came back when it stopped. The pyramid now records the camera it was built from (RenderInfo.hzbFrame: the view-projection with the scene root and the camera's position; in stereo the eye rendered last), and the test projects with that camera. An entity seen from both places is tested where the pyramid holds its own depth, however the camera moved in between; an entity the old camera could not see is kept; what a step uncovers from behind a near surface shows one frame late. After a step beyond HZBOcclusionCulling.maxCameraStep (0.5 units by default, setRendering(.occlusionCullingMaxCameraStep(_:))) the pyramid says too little about the frame and the test is skipped: the frame draws what the frustum keeps. The kernel and the bias are unchanged; the unused hzbPyramidOverride parameter is gone. HZBOcclusionReprojectionTests: a box seen over a near wall from both places stays visible when the camera rises (dropped before this change), a box behind the wall stays hidden across a small step, the test is skipped for the frame after a jump and runs again from the new place, the camera of a view matrix, the pyramid's record, the setting. CullingTest's direct runs of the pass set the pyramid's camera. Co-authored-by: Javier Segura <javier@miolabs.com>
…toldengine#1321) * [Performance] A snapshot of the scene for the render passes A pass asks the same few things about thousands of entities: is the entity still there, which of a dozen components does it have, where are its render and transform components. Asked through `scene`, each question takes the scene lock, copies the scene and looks a component id up. RenderSceneSnapshot reads the scene once, looks up the ids and the storage of the components the passes use once, and answers from what it took: - entity(_:) says whether an id is a live entity and gives its traits (RenderEntityTraits, from the entity's component mask) - drawComponents(of:) and the other accessors hand out the scene's own component objects, so their values are always the current ones - forEachEntity(with:) walks the entities that have a set of components, in the order of their indices - SceneChannelRenderModeMemo asks for the render mode of a channel set only when it differs from the one of the entity before A snapshot lives for one pass. Nothing uses it yet. * [Performance] The cull and the shadow passes read the scene through a snapshot The culling gather, the fit of the shadow cascades and the choice of the shadow casters walked every render entity through `scene`: about a dozen locked lookups per entity, per pass and per cascade. They now build a RenderSceneSnapshot when they start. - the two culling gathers walk the snapshot, so the list sent to the GPU cull follows the entity indices instead of the order of a hash set - the casters of a cascade, of the spot light and of the point light are chosen from the snapshot and keep their components for the draw loop - the shadow draw loops read the camera position and the projection once - a mesh's vertex streams are bound from the deformation component and the skeleton flag the pass already has, and MTKMesh.vertexBuffers, which builds an array on every read, is read once per mesh What is drawn does not change. * [Performance] The model, transparency and wireframe passes read the scene through a snapshot For every visible entity the opaque passes asked `scene` about fifteen things before the first draw, and five more for every draw (two fades, the ghost opacity, the deformation buffers, the skeleton), besides settings that sit behind a lock of their own. - an entity's draws start from one read of the snapshot: its traits answer what it is (camera, light, gizmo, batched, fading) and its components come from the storage the snapshot took - what every draw of a pass shares is read when the pass starts: view and projection, camera position, parallax quality, the debug colour switches - what every draw of an entity shares is read once per entity: its fades, its deformation buffers, its opacity on a ghost channel - an entity whose LOD is not fading draws its meshes without building the list of fade draws - the wireframe and occluder shell filters and the tile statistics reject an entity from its traits What is drawn does not change: the reference images score the same, and new render tests pin the per-entity decisions (hidden and wireframe channels, ids that are gone, tile and mesh fades). On a site of 21,576 render entities (release build, 1920x1080, M4 Max), with the two commits before this one: culling gather 7.5-8.7 ms to 1.5-1.7 ms, encoding 69-117 ms to 17-28 ms, frame 93-137 ms to 30-46 ms. * [Performance] The render entity traits no longer declare Sendable SwiftFormat 0.60.1 (the version CI runs) rejects an explicit Sendable on a non-public struct, and an internal option set of one UInt32 is Sendable by inference anyway. --------- Co-authored-by: Javier Segura <javier@miolabs.com>
…ldengine#1322) * [Feature] Select LOD levels by the size of the entity on screen The LOD chain of a pack model gives each level a screen size, and the loader turned it into a distance for each placement as the pack loaded: right for the scale and the field of view of that moment only. A placement scaled afterwards, or a zoom, kept switching at the old distances. A LOD component can now select by screen size (LODComponent.selectsByScreenSize): each level ends at the distance where the entity covers the screenPercentage of the next one. The size is read as each LOD pass runs, from the sphere around the entity's bounds under its world scale and from the projection in use. It is a share of the viewport height whatever the resolution, so a denser display draws the same levels. The bias and the hysteresis act on these distances as they do on maxDistance. The placements of a pack select this way. The parts of a model with several nodes carry the radius of the whole model (LODComponent.screenSizeRadius) and change level together. The distance of the load stays in maxDistance: it serves an orthographic view, and a level without a screen size. LODLevel.screenPercentage was stored and never read. A component selects by it only when told to, so entities that were given distances keep them. * [Patch] In stereo, take the LOD reach from the eyes' projections of the last frame The LOD update runs before the eyes are rendered, so in stereo the reach it took from renderInfo.perspectiveSpace was the second eye's of the previous frame, and on the first pass of a headset session the window's projection. The update now takes, in stereo, the larger of the two eye projections' reaches from the frame rendered last, and until an eye has been rendered (the projections are still the identity) the stored distances apply, as under an orthographic projection. A headset's field of view does not change between frames. --------- Co-authored-by: Javier Segura <javier@miolabs.com>
…ngine#1327) Covers dependency setup, registering the backend before the renderer, collider sizing (including the mesh-scale-vs-collider-radius pitfall), automatic stepping, resetting a body via the backend's resetBody side channel, and the current damping limitation, cross-checked against the CoolBasket and ImmersiveGol demos.
…rer (untoldengine#1328) Xcode's bundled Python ships pip 21.2.4, which rejects --break-system-packages (added in pip 23.0.1) and aborts the target before it can even upgrade pip. Detect flag support via pip --help instead of hardcoding it, and honor UNTOLD_PYTHON so the installs target the same interpreter the tests use. Fixes untoldengine#1325 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* [Bugfix] Shade each XR eye from its own position In XR the lighting, model, batch, transparency, sky and render-extension passes took their view vector from CameraComponent.localPosition, which UntoldEngineXR sets to the head centre once a frame for streaming (setXRCameraWorldPosition). Both eyes were shaded as if seen from between them, so specular highlights and Fresnel had no stereo disparity and read as painted on the surface up close. renderXR now stores the position of the eye it draws, taken from that eye's view matrix, in renderInfo.xrEyeCameraPosition, and the shading passes read shadingCameraPosition(_:), which returns it (through the scene root, like before) or the camera's position outside XR; the non-XR draw clears it. Streaming, LOD, octree and culling keep the head centre. Gaussian splats keep it too: their colour is evaluated once per frame for both eyes in the shared preprocess. * [Patch] Write the xrEyeCameraPosition comment as a doc comment SwiftFormat 0.60.1 (the version CI pins) wants a doc comment on a lone property declaration; the lint job failed on it. --------- Co-authored-by: Javier Segura <javier@miolabs.com>
…ntoldengine#1324) * [Bugfix] Keep a model's hidden parts hidden at its coarse LOD levels A model's meshes are simplified one by one, each within the allowance of the level, and a part a short way behind another (a car's inner skin 0.8 mm under its roof, its door panels behind its doors) shows through once the allowance exceeds the gap: the parked cars of a BIM site grew black patches with the distance. The cook now sinks what is hidden before each level is simplified (UntoldMeshLODHiddenParts). A vertex is hidden when its own side is covered (the rays along its normal and in a cone round it all meet another face of the model) and a seen face of another, solid mesh is in front of it within three allowances; it is moved away from that face, along the face's normal, by twice the allowance where it touches the face and by less up to the edge of reach, so that the two cannot cross. What is seen is never moved; nothing is sunk away from glass; a sink stops two allowances short of any seen face in its way; the sinks are averaged over each connected piece, with the seen vertices held still, so that a piece hidden in one part and seen in another bends instead of tearing; and a piece that is sunk for the most part is simplified on its own share of its mesh's target, since bent by its sinking it would otherwise keep its triangles and take those of the pieces beside it. A level also gives every vertex whose normal is more than 30 degrees from the faces around it (a vertex kept from a groove that is gone) the normal of those faces, turned to the side of the vertex's own so that a mesh wound the other way round keeps its normals pointing out; a hard edge keeps both. On the site's pack (4,779 models) the cook takes 21 s instead of 6, the coarsest levels hold 3.4 % more triangles, the levels' errors do not change, and the cars are clean at every level. * [Chores] Satisfy the CI lint and the strict-concurrency guardrail CI's SwiftFormat 0.60.1 wants a key path for the trivial map closure and reads the `self.` of `let rings = self.rings(for:)` as redundant, where it told the local from the method: the method is now `cellRings(for:)`. CI's Swift 6.3.3 warns of a `var` captured by the closure `concurrentPerform` runs, which the guardrail counts as a failure: the covered vertices are now a `let` built by a filter. No behaviour changes; the cooker suite passes as before. --------- Co-authored-by: Javier Segura <javier@miolabs.com>
…ation (untoldengine#1329) * [Bugfix] Use single-bounce reflectance for IBL diffuse energy conservation computeIBLContribution attenuated diffuse by the multi-bounce-compensated reflectance from environmentReflectance, which conflates two different things: the energy specular's own approximation needs restored via later bounces, and the share a surface diverts away from diffuse in the first place. Factor the single-bounce term out into singleBounceReflectance and use that for the diffuse term instead, keeping the compensated value for specular only. Fixes untoldengine#1296 * [Chores] fixed formatting
…keleton (untoldengine#1330) A clip whose channels never resolve against the target skeleton's joint paths (namespace prefix or hierarchy mismatch) previously registered and played silently: currentAnimation set, currentTime advancing, every joint sampling its rest pose, with no error at all. Adds .animationClipNoMatchingJoints and a diagnostic check wired into changeAnimation, restartAnimation, and the two auto-select-on-registration paths; registration and playback still succeed, this only makes the failure visible. Fixes untoldengine#1318
… LOD levels, IBL diffuse energy (untoldengine#1285, untoldengine#1324, untoldengine#1329)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the fork's
developup to upstreamdevelopat1085580c("[Bugfix] Report animation clips that match zero joints on the bound skeleton (untoldengine#1330)"). The fork was eleven commits behind; after this merge it differs from upstream by the 13 proposal documents underdocs/proposalsonly.What it brings
make testrendererpip detection.Merge
One merge commit of
upstream/develop, no conflicts: the fork's side since the last sync (#71) is the proposal documents only.Verified locally on the merge commit
python3 -m unittest discover -s scripts/tests -t .).UntoldEngineTests1,693 tests, 2 skipped, 0 failures (run from a checkout folder namedUntoldEngine, so the external render extension package test runs too).GlassShadingTests,BaseColorShadingTests,EmissiveShadingTests,EnvironmentReflectionShadingTests,NormalMapShadingTests,HeightMaterialAPITests,NativeFormatMaterialTests): 54 tests, 0 failures, PSNR threshold 33.5. The rest of the render suite is upstream's code and passed upstream's CI at1085580c.scripts/strict-concurrency-guardrails.sh): build passes, 0 warnings.developat its own sync head (miolabs/UntoldEditor, the sync opened alongside) built against this merge commit through a local path dependency: builds, 1,312 tests, 1 skipped, 0 failures.The user's editor gets the engine with Xcode's "Update to Latest Package Versions" after the merge.