Repository navigation
Conversation
A texture can take the image of another one, already loaded, without loading it again: replaceImage(source). It keeps its own settings for the materials (UV transform, coordinatesIndex, alpha, wrapping) and takes the source's loading description (url or buffer, mipmaps, format, sRGB buffer, loader options), so serializing it and loading it again gives the image it shows; the two textures share the source's GPU texture. It returns false, changing neither texture, for anything but ordinary loaded 2D image textures of the same engine with the same effective orientation. - The source's image keeps the source's gamma, also when its gamma state is not set yet. The materials are marked dirty only when the effective gamma or RGBD encoding changes; otherwise they bind the new image at their next draw. - With snapshot rendering, the snapshot is recorded again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ARD snapshots again - The images it takes and replaces must be 2D images loaded from a url or a buffer, and ready. It checked the class name, but render target, procedural and raw textures do not override getClassName, so a render target could take an image. - A STANDARD snapshot also replays the bundles it recorded: it binds the new image to the materials but draws nothing new, and once the previous image is released the boxes of a Playground showed nothing from the second frame on. The snapshot is recorded again in both modes. - The sampling mode is the image's: a later load of the texture uses the one it showed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deltakosh
left a comment
There was a problem hiding this comment.
Hello @enriqxxx,
Nice API for streaming. The guards (2D url/buffer images only, same engine, matching orientation), taking the reference before releasing the own image, and the snapshot reset look good. One issue:
- Serialization round-trip (
texture.pure.ts:661): the doc says serializing the texture and loading it again gives the image it shows, but onlyurlis updated whilenamekeeps the old image.Texture.Parseloads fromrootUrl + parsedTexture.nameby default (parsedTexture.urlis only used for data URLs or withTexture.UseSerializedUrlIfAny), so replacingold.pngwithnew.pngand round-tripping the scene reloadsold.png. Please updatenamewhen it follows the old url (or otherwise make the round-trip hold), or adjust the doc, and add a serialize/parse test.
Non-blocking: a frozen material whose UBO holds image-dependent values (for example a PBR 2D reflection texture's width in vReflectionMicrosurfaceInfos) keeps the old value when the replacement has a different size, since no material is marked dirty in that case. Worth a line in the doc.
Thanks!
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s). |
|
Snapshot stored with reference name: Test environment: To test a playground add it to the URL, for example: https://snapshots-cvgtc2eugrd3cgfd.z01.azurefd.net/refs/pull/19099/merge/index.html#WGZLGJ#4600 Links to test your changes to core in the published versions of the Babylon tools (does not contain changes you made to the tools themselves): https://playground.babylonjs.com/?snapshot=refs/pull/19099/merge To test the snapshot in the playground with a playground ID add it after the snapshot query string: https://playground.babylonjs.com/?snapshot=refs/pull/19099/merge#BCU1XR#0 If you made changes to the sandbox or playground in this PR, additional comments will be generated soon containing links to the dev versions of those tools. |
🟢 Memory Leak Test Results4 passed, 0 leaked out of 4 scenarios 🟢 All memory leak tests passed — no leaks detected. Passed Scenarios (4)
|
|
Visualization tests for WebGPU |
⚡ Performance Test Results🟢 All performance tests passed — no regressions detected. |
|
WebGL2 visualization test reporter: |
…alize/parse round-trip loads the image shown Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he root the image was loaded from, also in a parsed scene Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge, frozen ones included; engines compared by image; snapshot reset only on change - The materials that use the texture are marked with markDirty(true), which also reaches frozen materials, whenever something they read from the image besides its texels changes (gamma, RGBD, size, LOD generation, irradiance texture, type, format), so a frozen PBR material rewrites a 2D reflection texture's width and LOD in its uniform buffer. An image read the same way marks nothing. - The engines are compared on the two validated images, so a source created with an engine and no scene is taken. - A snapshot is recorded again only when the image or what the materials read from it changes. - A name that followed the url goes up from the root for a source outside it, when both urls are relative to the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…als; defines invalidate every material What the materials turn into defines (gamma, RGBD, linear specular LOD, irradiance texture) invalidates every material that has the texture: TextureDirtyFlag, or markDirty(true) for a frozen one. What they only write into uniforms or bind (size, LOD generation, LOD textures) reaches only the frozen materials, since the others write it at every bind: replacing an image with the same image at another resolution, as texture streaming does, marks no material that is not frozen. textureType and textureFormat are no longer compared: no material reads them. The snapshot is recorded again when the image or the defines change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @Popov72, all four addressed in aef6f69 and f00ad6f (on top of 7456174 and 1069819, which answered the round-trip point of the previous review and were not pushed yet when you reviewed 893768e). 1. Round trip. 2. Image-dependent material state.
3. Engine ownership. The engines are compared on the two validated images ( 4. Snapshot reset (non-blocking). The snapshot is recorded again only when the image or the defines change. The branch's tests: 14 passed. Every test that these two commits add or change fails without them (5 of 14 fail on 1069819). The one exception is the streaming test, which guards the cheap path: it passes on 1069819 and fails on aef6f69, which marked every material on a size change. All core unit tests (294 files): 4,854 passed, 1 expected fail (already marked on master). Prepared by Mema, the AI of our studio 53ON (Claude in the 53ON harness, with MemoriaBios, our long-term memory), and reviewed by me. |
deltakosh
left a comment
There was a problem hiding this comment.
Hello @enriqxxx,
Thanks, the single replacement now round-trips correctly. Consecutive replacements outside the root still lose it (texture.pure.ts:670-699):
With a texture named x.png loaded from scenes/x.png, replaceImage with a source at other/a.png sets name = "../other/a.png". A second replaceImage with other/b.png (or back to scenes/c.png) recomputes nameRoot from own.url = other/a.png, which does not end with ../other/a.png, so nameRoot is null and the name is left at ../other/a.png. Serializing and parsing then loads a.png instead of the image shown.
Please keep the root the name is relative to across replacements (e.g. store it on the first swap), and add a round-trip test with two successive swaps.
Thanks!
…placements, and in clones A name that went up from the root (../other/a.png) no longer tells the root, so the next replacement left it unchanged and a round trip loaded the previous image. The root is kept from the replacement that set the name, while the name is still the one it gave, and a clone takes it with the name it copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/azp run |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Thanks @deltakosh, fixed in 680f2d7 (on top of f00ad6f). Consecutive replacements outside the root. The texture now keeps the root its name is relative to. The replacement that sets the name stores the root, and the next replacement reuses it while the name is still the one it gave. Otherwise the root is read again from the url the own image was loaded from, as before. So a name of its own given between two replacements is still kept, and a parsed texture still finds its root from With your example, a texture named
Each round-trip loads the image shown. Clones. The same loss would hit a clone. Tests:
The first two fail on f00ad6f; the third passes on both heads. The branch's tests: 16 passed. All core unit tests (294 files): 4,856 passed, 1 expected fail (already marked on master). Prepared by Mema, the AI of our studio 53ON (Claude in the 53ON harness, with MemoriaBios, our long-term memory), and reviewed by me. |
deltakosh
left a comment
There was a problem hiding this comment.
Hello @enriqxxx,
Keeping _nameRoot (and checking it still produces the current name) fixes consecutive outside-root replacements: the second swap now resolves ../other/b.png from scenes/, and clones inherit the root. Tests cover both. Approved.
Thanks!
|
/azp run |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
…becomes linear, its conversion written whatever the image at build TextureBlock and TriPlanarBlock wrote the conversion of a texture to gamma space only when the texture was linear when the graph was built, though its define follows the texture's image. After Texture.replaceImage gave a gamma space texture a linear image, the define asked for a conversion the shader did not have, and the colors were wrong. Both blocks now write both conversions under their defines. In WGSL, TextureBlock converts a 3-component output with toGammaSpaceVec3, as TriPlanarBlock does, since WGSL has no overload of toGammaSpace for it. The test builds a node material with a gamma space texture, replaces its image with a linear one, and checks that the effect it then draws with converts the texture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting a shared image's gamma, and only frozen materials asked on uniform changes What the materials read from the image before the swap was read through the gammaSpace getter, which sets the gamma of an image that has none yet: when both wrappers already shared the image, it took this texture's gamma instead of the source's, and both then read gamma space. It is now read as the getter reads it, without setting it, so the image takes the source's gamma. The previous state is kept in named scalars, compared one by one after the swap, instead of four arrays and two closures for each replacement, with the same getSize() result and the same decisions. When only what the materials write into uniforms or bind changes, a material that is not frozen is skipped before hasTexture(): it writes them at its next bind anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @Popov72 and @deltakosh, both points are addressed in 0bbb2c6. The scene's environment texture. A PBR material without a reflection texture reads The wrappers, not only their images. Both textures must now be plain
Both fail on f42ae7d, where the replacement was accepted. As suggested for the node material post-processes and procedural textures, the doc now says they follow a change of gamma space once they are built again, as with the All the core unit tests on 0bbb2c6 (295 files): 4,869 passed, 1 expected fail (already marked on master). tsc of core reports 0 errors in a full check, prettier is clean, and eslint reports no new warning. Prepared by Mema, the AI of our studio 53ON (Claude in the 53ON harness, with MemoriaBios, our long-term memory), and reviewed by me. |
deltakosh
left a comment
There was a problem hiding this comment.
Hello @enriqxxx,
The three points of my last review are resolved (environment-texture consumers, unsupported wrappers, node post-process note). Remaining items, all at once:
- Vertex-stage conversion has no helpers -
textureBlock.pure.ts:620-633now always writes bothtoGammaSpace/toLinearSpacecalls under their defines, so a laterreplaceImagecan switch them on without rebuilding the graph. ButhelperFunctionsis emitted only in the fragment path (_buildBlock, line 717); the vertex path (_injectVertexCode->_writeOutput(..., true)) never emits it. A TextureBlock read in the vertex shader withconvertToGammaSpace = truewhose gamma-space image is replaced by a linear one (or the reverse withconvertToLinearSpace) then fails to compile, unless another block happens to bring the include. Please emit the helpers in the vertex state too and cover the swap. - StandardMaterial reflection/refraction space stays stale -
texture.pure.ts:761marks non-frozen consumers withMATERIAL_TextureDirtyFlagonly, butStandardMaterialrecomputesIS_REFLECTION_LINEAR/IS_REFRACTION_LINEARonly under_areImageProcessingDirty(standardMaterial.pure.ts:950-956). Replacing a gamma reflection/refraction image with a linear one keeps the old conversion. Please also mark image-processing dirty whengammaSpacechanges, with a StandardMaterial regression. - IBL CDF maps keep the previous environment -
IblCdfGeneratorbuilds its procedural textures withrefreshRate = 0and itsiblSourcesetter returns early on the same texture (iblCdfGenerator.ts:63-64), andreplaceImagedoes not notifyonLoadObservable. Replacing the environment image therefore leaves IBL shadows (and anything using the CDF / dominant direction) on the old light distribution, and on the old map size when dimensions differ. Please have the generator regenerate when its source image is replaced (or document the limitation explicitly). packages/dev/core/test/unit/Materials/babylon.texture.replaceImage.wrappers.test.ts:72usesengine.createEffect.bind(engine);Function.bindis prohibited underpackages/dev/**(.github/instructions/prohibited-apis.instructions.md). Please capture the method and call it with.call(engine, ...)from the arrow.
Thanks!
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
🟢 Memory Leak Test Results4 passed, 0 leaked out of 4 scenarios 🟢 All memory leak tests passed — no leaks detected. Passed Scenarios (4)
|
|
WebGL2 visualization test reporter: |
⚡ Performance Test Results🟢 All performance tests passed — no regressions detected. |
|
Visualization tests for WebGPU |
…ial reflection/refraction space, IBL CDF maps follow the new image - TextureBlock emits the helper functions in the vertex shader too, and keeps one pair of conversion defines per build for both shaders: the fragment build named new ones, so the conversions of a texture the vertex shader reads were under defines nothing set. - replaceImage also marks the image processing defines dirty when the gamma space changes: StandardMaterial computes IS_REFLECTION_LINEAR / IS_REFRACTION_LINEAR there. - Texture notifies an internal observable when replaceImage gives it another image; IblCdfGenerator regenerates its maps from it. - The wrappers test no longer uses Function.bind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Up @enriqxxx |
|
Thanks @deltakosh and @Popov72. The four points are addressed in f1fb914. 1. Helpers in the vertex stage. 2. StandardMaterial reflection and refraction space. When 3. IBL CDF maps. 4. Each new or changed test fails with the source of 0bbb2c6 (6 of them), and all 25 tests of the two Prepared by Ramon and Mema, the AI of our studio 53ON (🖐️, 👀, 🧮🔨🪛, 🧠 human memory simulation). |
deltakosh
left a comment
There was a problem hiding this comment.
Thanks @enriqxxx. All four points from 0bbb2c6 are addressed at f1fb914:
- The vertex-stage TextureBlock conversion now emits
helperFunctions. replaceImagemarks the image-processing dirty flag when the gamma space changes, so StandardMaterial recomputesIS_REFLECTION_LINEAR/IS_REFRACTION_LINEAR.- The IBL CDF generator regenerates its maps from
_onImageReplaced. - The
.bindtest is fixed.
LGTM.
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Reviewer - this PR has made changes to one or more package.json files. |
🟢 Memory Leak Test Results4 passed, 0 leaked out of 4 scenarios 🟢 All memory leak tests passed — no leaks detected. Passed Scenarios (4)
|
|
Visualization tests for WebGPU |
|
WebGL2 visualization test reporter: |
|
Up @enriqxxx |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…conversion
The two tests built a RegExp from the expected text escaping only ".",
"(" and ")", which CodeQL reports: the backslash stayed unescaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Head branch was pushed to by a user without write access
|
Thanks @deltakosh. Master is merged (8d2c342), and the diff of the PR is unchanged. The two CodeQL alerts were real, in On 5764c81, the core unit suite passes (316 files, 5,163 tests, plus the expected fail already marked on master), and core type-checks with no errors. Prepared by Ramon and Mema, the AI of our studio 53ON (🖐️, 👀, 🧮🔨🪛, 🧠 human memory simulation). |
deltakosh
left a comment
There was a problem hiding this comment.
Re-reviewed f1fb914...5764c81.
- Master merge 8d2c342: master changed none of this PR's files since the previous base, so the merge is clean on the PR's side.
- 5764c81: the
TextureBlock/TriPlanarBlocktests now escape every regex metacharacter of the expected conversion ([.*+?^${}()|[\]\\]), not just.,(and). This addresses the CodeQL incomplete-escaping report.
Forum: 64429 #5 and #6.
A streamed world swaps the images of many textures at once when it changes their levels.
Texture.updateURLreleases the old image right away, so the meshes that use it are not drawn until the new one has loaded, and for each texture it marks dirty every material of the scene that has it. This PR addsTexture.replaceImage(source): the texture takes the image of another texture, already loaded, without loading it again.What it does.
coordinatesIndex, alpha and wrapping) and adopts the source's loading description (url or buffer, mipmaps, sampling mode, format, sRGB buffer, loader options), so serializing it and loading it again gives the image it shows. The source's UV transform, which already holds the_invertVScalecorrection of its loading, is not copied.Texture.Parseloads fromrootUrl + name. A name that followed the url the image was loaded from follows the source's url, relative to the same root (going up from it for a source outside it, when both urls are relative to the page), or becomes the source's full url for an absolute or data url. The texture keeps that root for its next replacements and gives it to its clones. A name of its own is kept.environmentTexturesetter does: a PBR material without a reflection texture reads it without having it), frozen ones included, compute their defines again when something they turn into defines changes: the gamma space, the RGBD encoding, the linear specular LOD or the irradiance texture. What they only write into uniforms or bind (the size, the LOD generation, the LOD textures) reaches the frozen ones only, whichmarkDirty(true)makes write it again. The others write it at their next bind, so replacing an image with the same image at another size, as streaming does, does not even ask them whether they use the texture. The effective gamma is the getter's (the internal texture's gamma state with!_useSRGBBuffer), read without setting it. The source's image keeps the source's gamma, also when its gamma state is not set yet and the two wrappers already share it. When the gamma space changes, the materials' image processing is marked dirty as well as their textures, so aStandardMaterialcomputes again the space of its reflection and refraction textures (IS_REFLECTION_LINEAR,IS_REFRACTION_LINEAR).TextureBlockandTriPlanarBlockwrite both color space conversions under their defines, whatever the texture's gamma space when the graph is built. So a material converts a texture whose image becomes linear. In WGSL, which has no overloads,NodeMaterialBuildState._toGammaSpacepicks the function for the output's type:toGammaSpaceVec3for a 3-component output, and for a float as avec3f. When the vertex shader reads an output of the block, its helper functions are emitted in the vertex stage too, and the block names its pair of conversion defines once per build, so both stages test the same defines.IblCdfGeneratorwhose source's image is replaced generates its maps again, at the new size when it changed:replaceImagenotifies an internal observable of the texture (_onImageReplaced), which the generator subscribes to in itsiblSourcesetter and releases when the source changes and on dispose.false, changing neither texture, when the image cannot be taken: either texture is not a plainTexture(a subclass manages its image: render target, procedural, raw, dynamic and video textures, also a video texture showing its poster or a procedural texture its fallback), or the image of either texture is not a 2D image loaded from a url or a buffer and ready (render target, procedural, raw, dynamic and video textures are refused), the two belong to different engines, the source has neither a url nor a buffer, or their effective orientations differ (invertY, and whether loading inverted the V scale). Replacing a texture with itself returnstrue.Playground (on a build with this PR): https://playground.babylonjs.com/#2IS5NN#0. 600 boxes, every two sharing an unlit, frozen material, take the images of 300 textures loaded ahead, and each frame the canvas is read back at the center of each box. On this branch, all 600 boxes show their new images on the first frame after the calls, on WebGL2 and on WebGPU with
compatibilityModeon and off and with snapshot rendering in STANDARD and FAST modes; no frame shows an old image or a missing box, and no material is marked dirty (nohasTexturecall). Recording a STANDARD snapshot again is needed too: without it, the first frame still showed the old images and the next ones showed none of the boxes.Unit tests.
babylon.texture.replaceImage.test.ts, 23, all failing on master, where the method does not exist:textureBlock.test.tsandtriPlanarBlock.test.ts, 4 more each: both conversions written under their defines for a gamma space texture, for anrgband anroutput, in GLSL and in WGSL.babylon.texture.replaceImage.wrappers.test.ts(jsdom), 2: a video texture showing its poster and a procedural texture showing its fallback are refused, as the source and as the texture.Our streamed world. On the bench of our 8c644 world (272 tiles, 1,082 draws, snapshot rendering on, materials not frozen),
replaceImageswaps 16 textures every 60 frames: 368 replacements per run, none refused. In 8 runs in ABBA order, f1fb914 and 0bbb2c6 render the same image: 0 pixels differ between the two heads and between runs of the same head, except 2 pixels of the browser's first run, on the 0bbb2c6 arm. No console error in either.All the core unit tests on f1fb914 (295 files): 4,873 passed, 1 expected fail (already marked on master).
Prepared by Mema, the AI of our studio 53ON (Claude in the 53ON harness, with MemoriaBios, our long-term memory), and reviewed by me.
🤖 Generated with Claude Code