<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title><![CDATA[Esenthel Forum - All Forums]]></title>
		<link>https://esenthel.com/forum/</link>
		<description><![CDATA[Esenthel Forum - https://esenthel.com/forum]]></description>
		<pubDate>Sat, 11 Jul 2026 05:06:10 +0000</pubDate>
		<generator>MyBB</generator>
		<item>
			<title><![CDATA[RT sun shadows, RT specular reflections, Clustered/froxel lighting]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11833</link>
			<pubDate>Fri, 10 Jul 2026 02:39:06 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11833</guid>
			<description><![CDATA[RTGI+RT SHADOWS+RT Reflections<br />
<br />
<a href="https://postimg.cc/pmjg6tFT" target="_blank"><img src="https://i.postimg.cc/bwVpdyYQ/Screenshot-From-2026-07-09-22-28-11.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-11.png]" /></a><br />
<br />
<br />
No GI, CSM Shadows,<br />
<br />
<a href="https://postimg.cc/RWtjDzHS" target="_blank"><img src="https://i.postimg.cc/W1YTtNpk/Screenshot-From-2026-07-09-22-28-42.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-42.png]" /></a><br />
<br />
<br />
AI written summary:<br />
<br />
I've been extending the engine's Vulkan renderer with three new lighting features:<br />
<ul> []Ray-traced sun shadows []Ray-traced specular reflections</li>
<li>Clustered/froxel lighting </li></ul>
<br />
All three are opt-in and disabled by default. They are also pure runtime toggles, so no shader pak regeneration is needed to use them.<br />
<br />
The two ray-tracing features share a single scene acceleration structure, meaning the cost of “going RT” is paid once.<br />
<br />
Everything below was measured on an <span style="font-weight: bold;">AMD Radeon RX 6650 XT</span>.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">The Shared Foundation: One Scene TLAS, Zero Registration</span></span><br />
<br />
Both RT features use the ray-query acceleration structure originally introduced for dynamic GI.<br />
<br />
The best part is that nothing needs to be manually registered. Enable:<br />
<br />
GIRayTrace.auto_scene = true;<br />
The renderer then feeds every opaque mesh it draws into the acceleration structure automatically. Streamed world areas enter the RT scene as they load, with no additional application code.<br />
<br />
The RT world is the retained union of everything the camera has seen. Looking away from an object does not remove it from shadow, reflection, or GI rays. Meshes leave the acceleration structure only when they are actually destroyed.<br />
<br />
Acceleration-structure rebuilds are recorded directly into the frame command buffer, so there is no per-rebuild GPU stall.<br />
<br />
<span style="font-weight: bold;">Animated characters are supported too.</span><br />
<br />
GIRayTrace.addSkinnedOccluder(mesh, skel);<br />
This GPU-skins the character into its current world pose every frame and builds a per-frame BLAS. The character then:<br />
<ul> []Casts a correctly posed ray-traced shadow []Appears fully textured in ray-traced reflections</li>
<li>Costs only a fraction of a millisecond per character </li></ul>
<br />
One deliberate design choice is that dynamic characters receive GI but do not contribute to it.<br />
<br />
A deforming body inside a sparsely updated probe field causes the surrounding indirect lighting to strobe. To avoid this, the GI probes trace against a second, static-only TLAS.<br />
<br />
This is standard DDGI practice, and it eliminated a very visible “the lighting on my character keeps jumping” artifact.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Sun Shadows</span></span><br />
<br />
For each pixel, the renderer traces one or more rays toward the sun against the scene TLAS:<br />
<ul>
<li>[]Hit = shadowed []Miss = illuminated </li></ul>
<br />
The detail that makes this a drop-in replacement is that the engine's existing sun-application stage already consumes a pre-resolved full-screen visibility mask.<br />
<br />
The RT pass simply writes that mask. The light shaders remain completely unchanged.<br />
<br />
When RT shadows are enabled, the renderer skips the entire conventional shadow-map pipeline:<br />
<ul> []CSM cascade rasterization []Cascade resolve</li>
<li>Shadow-mask blur </li></ul>
<br />
<span style="font-weight: bold;">Advantages over shadow maps</span><br />
<ul>
<li><span style="font-weight: bold;">No aliasing, cascade seams, or peter-panning.</span><br />
<br />
A thin floating bar can cast a clean, visibly detached shadow that a shadow map cannot reproduce reliably at any practical cascade resolution.</li>
<li><span style="font-weight: bold;">True contact-hardening soft shadows.</span><br />
<br />
Multiple rays are cone-sampled across the sun's disc using a Fibonacci spiral. The pattern is rotated per pixel and temporally jittered so TAA converges the penumbra.<br />
<br />
The result is a shadow that remains sharp at the contact point and becomes progressively softer as the distance between the occluder and receiver increases.</li>
<li><span style="font-weight: bold;">Cost comparable to CSM.</span><br />
<br />
Because RT shadows skip cascade rasterization entirely, the net lighting-phase cost can land near parity with conventional shadow maps in typical scenes.<br />
<br />
An eight-ray, half-resolution soft-shadow pass measured approximately <span style="font-weight: bold;">0.89 ms</span>, roughly the same cost as the CSM path it replaced. </li></ul>
<br />
Full resolution is the default. Half resolution can undersample thin, moving occluders such as animated characters.<br />
<br />
Ray count is the primary performance-versus-quality control.<br />
<br />
RtShadows(true);<br />
<br />
RtShadowQuality(0.03f, 4);<br />
The first quality parameter controls sun-disc softness. A softness value of zero produces hard shadows. The second parameter controls the number of rays per pixel.<br />
<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTShadows</span><br />
<ul> []<span style="font-weight: bold;">R</span> switches live between ray-traced shadows and shadow maps []<span style="font-weight: bold;">- / =</span> adjusts shadow softness</li>
<li>An on-screen HUD displays GPU cost </li></ul>
<br />
The initial implementation supports the sun only.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Specular Reflections</span></span><br />
<br />
The reflection pass traces one:<br />
<br />
reflect(eye, normal)<br />
ray per pixel against the same scene TLAS.<br />
<br />
Reflection hits are fully lit and textured using:<br />
<ul> []Direct sunlight []A secondary shadow ray []Sky ambient lighting []GI probe irradiance evaluated at the hit point</li>
<li>The hit material's actual albedo texture </li></ul>
<br />
The result is written using the engine's existing SSR-buffer format.<br />
<br />
That allows the existing lit-color composite stage to fold the ray-traced result into the scene with no changes to the combine shader. The normal roughness-aware reflection fade is retained.<br />
<br />
<span style="font-weight: bold;">Where it beats SSR</span><br />
<br />
Screen-space reflections can only reflect geometry that is already visible on screen.<br />
<br />
The RT path can reflect:<br />
<ul>
<li>[]Content behind the camera []Objects around corners []Off-screen world geometry []The sky </li></ul>
<br />
It also avoids the characteristic smearing and disappearing reflections that occur when the camera moves or reflected geometry leaves the screen.<br />
<br />
At a demo resolution of <span style="font-weight: bold;">3072 × 1296</span>, the measured light-phase cost was approximately <span style="font-weight: bold;">0.17–0.24 ms</span>.<br />
<br />
That was inexpensive enough that I did not add a half-resolution mode.<br />
<br />
RtReflections(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTReflections</span><br />
<br />
The scene contains a mirror floor and chrome ball. Press <span style="font-weight: bold;">R</span> to switch between ray-traced reflections and the traditional SSR-plus-cubemap path for a direct comparison.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Combined Ray-Tracing Showcase</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_RTShowcase</span> runs all three ray-traced systems together:<br />
<ul> []Dynamic GI []Sun shadows</li>
<li>Specular reflections </li></ul>
<br />
All three systems share the same automatically constructed TLAS.<br />
<br />
The demo runs in a streamed world containing an animated character. Each feature can be toggled independently:<br />
<ul> []<span style="font-weight: bold;">G</span> — dynamic GI []<span style="font-weight: bold;">H</span> — ray-traced shadows</li>
<li><span style="font-weight: bold;">R</span> — ray-traced reflections </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Clustered / Froxel Lighting</span></span><br />
<br />
Clustered lighting addresses a different renderer bottleneck.<br />
<br />
The classic deferred per-light path renders each light with a separate draw call. When many point lights overlap, the renderer may read the G-buffer and blend lighting into the same pixel once for every affecting light.<br />
<br />
The clustered path targets unshadowed point lights such as:<br />
<ul>
<li>[]Torches []Projectiles []Muzzle flashes []Other scenes containing large numbers of dynamic lights </li></ul>
<br />
A compute pass bins lights into a froxel grid containing:<br />
<ul>
<li>[]32 × 18 screen-space tiles []24 exponential depth slices </li></ul>
<br />
A full-screen pass then shades all lights affecting a pixel in one operation using the exact same BRDF as the classic deferred-light path.<br />
<br />
The clustered output was verified for pixel parity against the per-light path.<br />
<br />
<span style="font-weight: bold;">Measured GPU_TIMER_LIGHT results with overlapping, unshadowed point lights:</span><br />
<ul> []64 lights: approximately <span style="font-weight: bold;">5% faster</span> []128 lights: approximately <span style="font-weight: bold;">13% faster</span></li>
<li>256 lights: approximately <span style="font-weight: bold;">19% faster</span> </li></ul>
<br />
The performance advantage grows with light count.<br />
<br />
The honest framing is that the existing per-light renderer is already depth-bounded. It is not a naive full-screen-per-light baseline.<br />
<br />
Clustered lighting is therefore a scalability feature rather than a free performance win in every scene, which is why it remains opt-in.<br />
<br />
Cases that are not handled by the clustered path automatically fall back to the classic renderer:<br />
<ul> []Shadowed lights []The sun []Water rendering []MSAA []Clear-coat materials []Mirrors</li>
<li>Sub-rectangle views </li></ul>
<br />
This keeps rendering parity from being placed at risk.<br />
<br />
ClusteredLights(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_ClusteredLights</span><br />
<br />
The demo presents a twilight field containing 256 overlapping lights.<br />
<ul> []<span style="font-weight: bold;">C</span> switches between clustered and per-light rendering []<span style="font-weight: bold;">Up / Down</span> changes the light count, up to 512</li>
<li>The light-phase GPU timer is displayed on screen </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Additional Notes</span></span><br />
<ul>
<li>All three systems are currently Vulkan-only.</li>
<li>The two ray-tracing features require ray-query-capable hardware.</li>
<li>On other rendering backends, every API call is a safe no-op. The tutorials still run, but the renderer remains on the classic paths.</li>
<li>The complete stack recently went through an adversarial, multi-pass code review and a synchronization-validation soak.</li>
<li>Synchronization validation reported zero hazards.</li>
<li>Sub-render re-entrancy, resource lifetime across streamed worlds, and thread safety of the retained RT scene received particular attention. </li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Design Documents</span></span><br />
<br />
The repository contains detailed design documents for each system:<br />
<ul>
<li>[]docs/rt-shadows-design.md []docs/rt-reflections-design.md []docs/rt-skinned-occluders-design.md []docs/clustered-froxel-lights-design.md </li></ul>
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[RTGI+RT SHADOWS+RT Reflections<br />
<br />
<a href="https://postimg.cc/pmjg6tFT" target="_blank"><img src="https://i.postimg.cc/bwVpdyYQ/Screenshot-From-2026-07-09-22-28-11.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-11.png]" /></a><br />
<br />
<br />
No GI, CSM Shadows,<br />
<br />
<a href="https://postimg.cc/RWtjDzHS" target="_blank"><img src="https://i.postimg.cc/W1YTtNpk/Screenshot-From-2026-07-09-22-28-42.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-42.png]" /></a><br />
<br />
<br />
AI written summary:<br />
<br />
I've been extending the engine's Vulkan renderer with three new lighting features:<br />
<ul> []Ray-traced sun shadows []Ray-traced specular reflections</li>
<li>Clustered/froxel lighting </li></ul>
<br />
All three are opt-in and disabled by default. They are also pure runtime toggles, so no shader pak regeneration is needed to use them.<br />
<br />
The two ray-tracing features share a single scene acceleration structure, meaning the cost of “going RT” is paid once.<br />
<br />
Everything below was measured on an <span style="font-weight: bold;">AMD Radeon RX 6650 XT</span>.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">The Shared Foundation: One Scene TLAS, Zero Registration</span></span><br />
<br />
Both RT features use the ray-query acceleration structure originally introduced for dynamic GI.<br />
<br />
The best part is that nothing needs to be manually registered. Enable:<br />
<br />
GIRayTrace.auto_scene = true;<br />
The renderer then feeds every opaque mesh it draws into the acceleration structure automatically. Streamed world areas enter the RT scene as they load, with no additional application code.<br />
<br />
The RT world is the retained union of everything the camera has seen. Looking away from an object does not remove it from shadow, reflection, or GI rays. Meshes leave the acceleration structure only when they are actually destroyed.<br />
<br />
Acceleration-structure rebuilds are recorded directly into the frame command buffer, so there is no per-rebuild GPU stall.<br />
<br />
<span style="font-weight: bold;">Animated characters are supported too.</span><br />
<br />
GIRayTrace.addSkinnedOccluder(mesh, skel);<br />
This GPU-skins the character into its current world pose every frame and builds a per-frame BLAS. The character then:<br />
<ul> []Casts a correctly posed ray-traced shadow []Appears fully textured in ray-traced reflections</li>
<li>Costs only a fraction of a millisecond per character </li></ul>
<br />
One deliberate design choice is that dynamic characters receive GI but do not contribute to it.<br />
<br />
A deforming body inside a sparsely updated probe field causes the surrounding indirect lighting to strobe. To avoid this, the GI probes trace against a second, static-only TLAS.<br />
<br />
This is standard DDGI practice, and it eliminated a very visible “the lighting on my character keeps jumping” artifact.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Sun Shadows</span></span><br />
<br />
For each pixel, the renderer traces one or more rays toward the sun against the scene TLAS:<br />
<ul>
<li>[]Hit = shadowed []Miss = illuminated </li></ul>
<br />
The detail that makes this a drop-in replacement is that the engine's existing sun-application stage already consumes a pre-resolved full-screen visibility mask.<br />
<br />
The RT pass simply writes that mask. The light shaders remain completely unchanged.<br />
<br />
When RT shadows are enabled, the renderer skips the entire conventional shadow-map pipeline:<br />
<ul> []CSM cascade rasterization []Cascade resolve</li>
<li>Shadow-mask blur </li></ul>
<br />
<span style="font-weight: bold;">Advantages over shadow maps</span><br />
<ul>
<li><span style="font-weight: bold;">No aliasing, cascade seams, or peter-panning.</span><br />
<br />
A thin floating bar can cast a clean, visibly detached shadow that a shadow map cannot reproduce reliably at any practical cascade resolution.</li>
<li><span style="font-weight: bold;">True contact-hardening soft shadows.</span><br />
<br />
Multiple rays are cone-sampled across the sun's disc using a Fibonacci spiral. The pattern is rotated per pixel and temporally jittered so TAA converges the penumbra.<br />
<br />
The result is a shadow that remains sharp at the contact point and becomes progressively softer as the distance between the occluder and receiver increases.</li>
<li><span style="font-weight: bold;">Cost comparable to CSM.</span><br />
<br />
Because RT shadows skip cascade rasterization entirely, the net lighting-phase cost can land near parity with conventional shadow maps in typical scenes.<br />
<br />
An eight-ray, half-resolution soft-shadow pass measured approximately <span style="font-weight: bold;">0.89 ms</span>, roughly the same cost as the CSM path it replaced. </li></ul>
<br />
Full resolution is the default. Half resolution can undersample thin, moving occluders such as animated characters.<br />
<br />
Ray count is the primary performance-versus-quality control.<br />
<br />
RtShadows(true);<br />
<br />
RtShadowQuality(0.03f, 4);<br />
The first quality parameter controls sun-disc softness. A softness value of zero produces hard shadows. The second parameter controls the number of rays per pixel.<br />
<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTShadows</span><br />
<ul> []<span style="font-weight: bold;">R</span> switches live between ray-traced shadows and shadow maps []<span style="font-weight: bold;">- / =</span> adjusts shadow softness</li>
<li>An on-screen HUD displays GPU cost </li></ul>
<br />
The initial implementation supports the sun only.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Specular Reflections</span></span><br />
<br />
The reflection pass traces one:<br />
<br />
reflect(eye, normal)<br />
ray per pixel against the same scene TLAS.<br />
<br />
Reflection hits are fully lit and textured using:<br />
<ul> []Direct sunlight []A secondary shadow ray []Sky ambient lighting []GI probe irradiance evaluated at the hit point</li>
<li>The hit material's actual albedo texture </li></ul>
<br />
The result is written using the engine's existing SSR-buffer format.<br />
<br />
That allows the existing lit-color composite stage to fold the ray-traced result into the scene with no changes to the combine shader. The normal roughness-aware reflection fade is retained.<br />
<br />
<span style="font-weight: bold;">Where it beats SSR</span><br />
<br />
Screen-space reflections can only reflect geometry that is already visible on screen.<br />
<br />
The RT path can reflect:<br />
<ul>
<li>[]Content behind the camera []Objects around corners []Off-screen world geometry []The sky </li></ul>
<br />
It also avoids the characteristic smearing and disappearing reflections that occur when the camera moves or reflected geometry leaves the screen.<br />
<br />
At a demo resolution of <span style="font-weight: bold;">3072 × 1296</span>, the measured light-phase cost was approximately <span style="font-weight: bold;">0.17–0.24 ms</span>.<br />
<br />
That was inexpensive enough that I did not add a half-resolution mode.<br />
<br />
RtReflections(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTReflections</span><br />
<br />
The scene contains a mirror floor and chrome ball. Press <span style="font-weight: bold;">R</span> to switch between ray-traced reflections and the traditional SSR-plus-cubemap path for a direct comparison.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Combined Ray-Tracing Showcase</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_RTShowcase</span> runs all three ray-traced systems together:<br />
<ul> []Dynamic GI []Sun shadows</li>
<li>Specular reflections </li></ul>
<br />
All three systems share the same automatically constructed TLAS.<br />
<br />
The demo runs in a streamed world containing an animated character. Each feature can be toggled independently:<br />
<ul> []<span style="font-weight: bold;">G</span> — dynamic GI []<span style="font-weight: bold;">H</span> — ray-traced shadows</li>
<li><span style="font-weight: bold;">R</span> — ray-traced reflections </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Clustered / Froxel Lighting</span></span><br />
<br />
Clustered lighting addresses a different renderer bottleneck.<br />
<br />
The classic deferred per-light path renders each light with a separate draw call. When many point lights overlap, the renderer may read the G-buffer and blend lighting into the same pixel once for every affecting light.<br />
<br />
The clustered path targets unshadowed point lights such as:<br />
<ul>
<li>[]Torches []Projectiles []Muzzle flashes []Other scenes containing large numbers of dynamic lights </li></ul>
<br />
A compute pass bins lights into a froxel grid containing:<br />
<ul>
<li>[]32 × 18 screen-space tiles []24 exponential depth slices </li></ul>
<br />
A full-screen pass then shades all lights affecting a pixel in one operation using the exact same BRDF as the classic deferred-light path.<br />
<br />
The clustered output was verified for pixel parity against the per-light path.<br />
<br />
<span style="font-weight: bold;">Measured GPU_TIMER_LIGHT results with overlapping, unshadowed point lights:</span><br />
<ul> []64 lights: approximately <span style="font-weight: bold;">5% faster</span> []128 lights: approximately <span style="font-weight: bold;">13% faster</span></li>
<li>256 lights: approximately <span style="font-weight: bold;">19% faster</span> </li></ul>
<br />
The performance advantage grows with light count.<br />
<br />
The honest framing is that the existing per-light renderer is already depth-bounded. It is not a naive full-screen-per-light baseline.<br />
<br />
Clustered lighting is therefore a scalability feature rather than a free performance win in every scene, which is why it remains opt-in.<br />
<br />
Cases that are not handled by the clustered path automatically fall back to the classic renderer:<br />
<ul> []Shadowed lights []The sun []Water rendering []MSAA []Clear-coat materials []Mirrors</li>
<li>Sub-rectangle views </li></ul>
<br />
This keeps rendering parity from being placed at risk.<br />
<br />
ClusteredLights(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_ClusteredLights</span><br />
<br />
The demo presents a twilight field containing 256 overlapping lights.<br />
<ul> []<span style="font-weight: bold;">C</span> switches between clustered and per-light rendering []<span style="font-weight: bold;">Up / Down</span> changes the light count, up to 512</li>
<li>The light-phase GPU timer is displayed on screen </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Additional Notes</span></span><br />
<ul>
<li>All three systems are currently Vulkan-only.</li>
<li>The two ray-tracing features require ray-query-capable hardware.</li>
<li>On other rendering backends, every API call is a safe no-op. The tutorials still run, but the renderer remains on the classic paths.</li>
<li>The complete stack recently went through an adversarial, multi-pass code review and a synchronization-validation soak.</li>
<li>Synchronization validation reported zero hazards.</li>
<li>Sub-render re-entrancy, resource lifetime across streamed worlds, and thread safety of the retained RT scene received particular attention. </li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Design Documents</span></span><br />
<br />
The repository contains detailed design documents for each system:<br />
<ul>
<li>[]docs/rt-shadows-design.md []docs/rt-reflections-design.md []docs/rt-skinned-occluders-design.md []docs/clustered-froxel-lights-design.md </li></ul>
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Global Illumination (GI)]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11832</link>
			<pubDate>Wed, 08 Jul 2026 12:44:32 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11832</guid>
			<description><![CDATA[It's a subtle lighting feature, but in dark scenes it can make a difference. <br />
Intensity of GI must be increased to really see it especially if there is an ambient light source. <br />
RT hardware accelerated GI has much better performance than pure software (rasterized) GI.<br />
<br />
GI ON, intensity cranked so effect is obvious:<br />
<a href="https://postimg.cc/hJFQ5BH1" target="_blank"><img src="https://i.postimg.cc/HLn0jWzR/Screenshot-From-2026-07-08-08-19-29.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-29.png]" /></a><br />
<br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/Ty8546Zq" target="_blank"><img src="https://i.postimg.cc/PqJbNfyc/Screenshot-From-2026-07-08-08-19-36.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-36.png]" /></a><br />
<br />
<br />
GI ON , yellow arrow is sun direction:<br />
<a href="https://postimg.cc/gw9LTWf8" target="_blank"><img src="https://i.postimg.cc/nLrKMV2R/Screenshot-From-2026-07-08-08-20-39.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-39.png]" /></a><br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/rDRtjmYm" target="_blank"><img src="https://i.postimg.cc/RZXQMJ7c/Screenshot-From-2026-07-08-08-20-51.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-51.png]" /></a><br />
<br />
Left Side GI ON / Right Side GI OFF split screen:<br />
<a href="https://postimg.cc/4nJKHw6c" target="_blank"><img src="https://i.postimg.cc/MZB14FX9/Screenshot-From-2026-07-08-08-38-31.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-38-31.png]" /></a><br />
<br />
AI Generated Summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Fully Dynamic Global Illumination for Esenthel — No Baking, No Authoring</span></span><br />
<br />
I added a complete real-time diffuse Global Illumination system to the engine’s deferred renderer.<br />
<br />
The headline: <span style="font-weight: bold;">zero authoring</span> — no baked lightmaps, no hand-placed probes, and no precompute step.<br />
<br />
You flip one switch and neutral surfaces start picking up colored bounce light from the walls, terrain, and objects around them. Time-of-day is fully dynamic. The system is Vulkan-only because it needs the compute path, and it is off by default. Existing scenes render byte-for-byte identically until an app explicitly opts in.<br />
<br />
There are two interchangeable producers that fill the lighting probes:<br />
<ul>
<li>[]<span style="font-weight: bold;">Rasterized cube capture</span> — works on any GPU and is always available as the fallback.<br />
[]<span style="font-weight: bold;">Hardware ray-traced capture</span> — optional ray-query path for GPUs that support it.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it works</span></span><br />
<ul>
<li><span style="font-weight: bold;">DDGI-style probe grid</span><br />
<br />
A camera-centered 3D grid of irradiance probes follows the camera. The deferred light pass swaps the old flat ambient term for trilinearly interpolated probe irradiance.</li>
<li><span style="font-weight: bold;">Toroidal scrolling</span><br />
<br />
As the camera moves, the probe volume scrolls toroidally, addressed by integer cell index, instead of re-capturing everything. This prevents a re-capture storm when moving through the world.<br />
<br />
This also makes it floating-origin and large-world safe because the GI volume effectively rebases for free.</li>
<li><span style="font-weight: bold;">Cascades</span><br />
<br />
Range comes from nested cascades at geometrically increasing spacing. Three cascades cover roughly 24 m to 216 m, with a cross-cascade seam blend so there is no hard visible ring at the cascade boundaries.</li>
<li><span style="font-weight: bold;">DDGI-standard atlases</span><br />
<br />
Each probe captures visibility, either rasterized into a small cube or ray-traced, then compute integrates the result into octahedral irradiance and Chebyshev depth-moment atlases.<br />
<br />
The depth moments suppress light leaking through thin walls, and a surface-normal bias helps as well.</li>
<li><span style="font-weight: bold;">Decoupled capture vs. relight</span><br />
<br />
Geometry capture and lighting relight are separated. Both are hard-capped per frame.<br />
<br />
The expensive geometry capture is time-sliced and budgeted, so the cost stays bounded. A static scene captures nothing, so it is free when you are not moving. Irradiance converges through temporal blending.</li>
<li><span style="font-weight: bold;">Free time-of-day relight</span><br />
<br />
An optional producer bakes a lighting-independent cube once and relights it each frame with per-bounce sun shadowing. Moving the sun costs no re-capture.</li>
<li><span style="font-weight: bold;">Analytic re-seeding</span><br />
<br />
Scrolled-in probe slots are analytically re-seeded, which kills stale-bounce ghosting during fast movement.</li>
<li><span style="font-weight: bold;">Half-resolution evaluation</span><br />
<br />
Half-res evaluation plus depth-aware bilateral upscale keeps the light-pass cost down.</li>
<li><span style="font-weight: bold;">Optional SSGI</span><br />
<br />
A screen-space near-field bounce layer can be enabled on top of the probe GI for close-range contact GI.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Hardware ray-traced path — opt-in, additive</span></span><br />
<br />
When ray-query hardware is present and a scene acceleration structure is registered, probe capture can be ray-traced instead of rasterized. This gives correct per-direction occlusion and bounce with no cube-face seams.<br />
<ul>
<li><span style="font-weight: bold;">RT occlusion moments</span><br />
<br />
GI.raytrace replaces the depth-cube producer with ray-traced occlusion moments. The light pass is unchanged.</li>
<li><span style="font-weight: bold;">RT bounce radiance</span><br />
<br />
GI.raytrace_radiance traces the diffuse bounce itself using cosine-weighted hemisphere gathering with Fibonacci-spiral rays.<br />
<br />
It supports textured and average-color albedo. Terrain with no UVs samples the texture’s mean color. It uses the correct hit-surface normal, NdotL, sun shadow rays, radiosity-style multi-bounce, and a per-frame sky-only cube so miss rays sample the real rendered sky.<br />
<br />
It reaches pixel parity with the rasterized producer at the same intensity on both Cornell and world scenes.</li>
<li><span style="font-weight: bold;">Zero-registration mode</span><br />
<br />
GIRayTrace.auto_scene = true lets the renderer feed every opaque mesh it draws into the acceleration structure automatically.<br />
<br />
Streamed world areas enter the TLAS as they draw and drop out as they stream away, with no app code required. Rebuilds are recorded into the frame command buffer, so there is no GPU stall.<br />
</li></ul>
<br />
The system is fully hardware-gated and TLAS-gated. If RT is off, unsupported, or unregistered, it falls back to rasterized capture. The software DDGI path is untouched.<br />
<br />
Verified working on both AMD RADV and NVIDIA.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demos</span></span><br />
<br />
Vulkan build, under Tutorials/Source/14 - Game Basics/<br />
<br />
<span style="font-weight: bold;">Tutorial_14_DynamicGI</span><br />
The basics: a neutral scene picking up colored bounce light, with live time-of-day. This is the color-bleed proof.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_WorldGI</span><br />
GI over a real streamed Game::World with terrain and props, plus point-light and time controls.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_LargeWorldGI</span><br />
GI running under the large-world floating origin. This proves it survives rebases without re-warming.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_GIShowcase</span><br />
The “all the knobs” demo. A Cornell room with split-screen A/B.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">V</span> — split-screen A/B<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
[]<span style="font-weight: bold;">1 / 2 / 3</span> — cascade count<br />
[]<span style="font-weight: bold;">R</span> — relight<br />
[]<span style="font-weight: bold;">B</span> — Chebyshev<br />
[]<span style="font-weight: bold;">A</span> — TAA<br />
[]<span style="font-weight: bold;">N</span> — SSGI<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">U</span> — RT radiance<br />
[]Sun, light, and wall toggles<br />
</li></ul>
<br />
<span style="font-weight: bold;">Tutorial_14_RTWorldGI</span><br />
The payoff: ray-traced GI over a playable streamed world via the zero-registration auto-scene hook.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">WASD</span> — move<br />
[]<span style="font-weight: bold;">G</span> — GI on/off, showing near-black ambient vs. bounced sunlight<br />
[]<span style="font-weight: bold;">U</span> — RT radiance vs. rasterized capture<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">T</span> — time-of-day<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it is turned off and on</span></span><br />
<br />
Everything defaults off at the engine level. These are the relevant defaults on the global GI object in Engine/H/Graphics/GI.h, plus GIRayTrace:<br />
<br />
<span style="font-weight: bold;">GI.enabled</span><br />
Default: false<br />
Turns on the whole system. Off means legacy flat ambient.<br />
<br />
<span style="font-weight: bold;">GIRayTrace.auto_scene</span><br />
Default: false<br />
Turns on zero-registration RT scene feeding.<br />
<br />
<span style="font-weight: bold;">GI.raytrace</span><br />
Default: false<br />
Turns on ray-traced occlusion moments.<br />
<br />
<span style="font-weight: bold;">GI.raytrace_radiance</span><br />
Default: false<br />
Turns on ray-traced bounce radiance.<br />
<br />
<span style="font-weight: bold;">GI.relight</span><br />
Default: false<br />
Turns on the free-time-of-day relight producer.<br />
<br />
<span style="font-weight: bold;">GI.ssgi</span><br />
Default: false<br />
Turns on screen-space near-field bounce.<br />
<br />
<span style="font-weight: bold;">GI.capture_shadows</span><br />
Default: false<br />
Turns on sun-shadowed lit-cube capture.<br />
<br />
<span style="font-weight: bold;">GI.chebyshev</span><br />
Default: true<br />
Turns on leak suppression when GI is enabled.<br />
<br />
<span style="font-weight: bold;">GI.half_res</span><br />
Default: true<br />
Turns on half-res evaluation and upscale.<br />
<br />
<span style="font-weight: bold;">GI.auto_light</span><br />
Default: true<br />
Derives sun and sky lighting from the live scene.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Canonical app opt-in path</span></span><br />
<br />
An app opts in from InitPre or Init:<br />
<br />
GI.enabled = true;<br />
GI.intensity = 2.0f;<br />
GIRayTrace.auto_scene = true;<br />
GI.raytrace_radiance = true;<br />
<br />
GI.enabled is the master switch. Nothing renders GI without it.<br />
<br />
GI.intensity controls the indirect multiplier, where 1 is physical.<br />
<br />
GIRayTrace.auto_scene is optional and lets the renderer register the scene for ray tracing automatically.<br />
<br />
GI.raytrace_radiance is optional and enables ray-traced bounce radiance. It falls back if unsupported.<br />
<br />
There are also EE_GI environment-variable overrides, such as:<br />
<br />
EE_GI=1<br />
EE_GI_RAYTRACE_RADIANCE=1<br />
EE_GI_INTENSITY=...<br />
<br />
Those exist mainly for headless A/B testing. They are not the intended shipping path.<br />
<br />
Beyond the app switch, GI is gated on backend support. GI.active() requires the Vulkan compute path. The RT path is additionally gated on ray-query hardware.<br />
<br />
So on a non-Vulkan backend, or a GPU without ray-query support, the relevant paths simply no-op or fall back.<br />
<br />
Net result: if you do not opt in, nothing about your scene changes.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Build and run one demo</span></span><br />
<br />
cmake --build cmake-build-release-vulkan --target Tutorial_14_GIShowcase -j &#36;(nproc)<br />
<br />
EE_NO_MSG_BOX=1 ./Tutorials/Tutorial_14_GIShowcase<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Documentation</span></span><br />
<br />
Full design notes and knob reference are in:<br />
<ul>
<li>[]The README’s Dynamic Global Illumination section<br />
[]docs/gi-raytracing-design.md<br />
</li></ul>
<br />
Available in this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[It's a subtle lighting feature, but in dark scenes it can make a difference. <br />
Intensity of GI must be increased to really see it especially if there is an ambient light source. <br />
RT hardware accelerated GI has much better performance than pure software (rasterized) GI.<br />
<br />
GI ON, intensity cranked so effect is obvious:<br />
<a href="https://postimg.cc/hJFQ5BH1" target="_blank"><img src="https://i.postimg.cc/HLn0jWzR/Screenshot-From-2026-07-08-08-19-29.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-29.png]" /></a><br />
<br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/Ty8546Zq" target="_blank"><img src="https://i.postimg.cc/PqJbNfyc/Screenshot-From-2026-07-08-08-19-36.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-36.png]" /></a><br />
<br />
<br />
GI ON , yellow arrow is sun direction:<br />
<a href="https://postimg.cc/gw9LTWf8" target="_blank"><img src="https://i.postimg.cc/nLrKMV2R/Screenshot-From-2026-07-08-08-20-39.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-39.png]" /></a><br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/rDRtjmYm" target="_blank"><img src="https://i.postimg.cc/RZXQMJ7c/Screenshot-From-2026-07-08-08-20-51.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-51.png]" /></a><br />
<br />
Left Side GI ON / Right Side GI OFF split screen:<br />
<a href="https://postimg.cc/4nJKHw6c" target="_blank"><img src="https://i.postimg.cc/MZB14FX9/Screenshot-From-2026-07-08-08-38-31.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-38-31.png]" /></a><br />
<br />
AI Generated Summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Fully Dynamic Global Illumination for Esenthel — No Baking, No Authoring</span></span><br />
<br />
I added a complete real-time diffuse Global Illumination system to the engine’s deferred renderer.<br />
<br />
The headline: <span style="font-weight: bold;">zero authoring</span> — no baked lightmaps, no hand-placed probes, and no precompute step.<br />
<br />
You flip one switch and neutral surfaces start picking up colored bounce light from the walls, terrain, and objects around them. Time-of-day is fully dynamic. The system is Vulkan-only because it needs the compute path, and it is off by default. Existing scenes render byte-for-byte identically until an app explicitly opts in.<br />
<br />
There are two interchangeable producers that fill the lighting probes:<br />
<ul>
<li>[]<span style="font-weight: bold;">Rasterized cube capture</span> — works on any GPU and is always available as the fallback.<br />
[]<span style="font-weight: bold;">Hardware ray-traced capture</span> — optional ray-query path for GPUs that support it.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it works</span></span><br />
<ul>
<li><span style="font-weight: bold;">DDGI-style probe grid</span><br />
<br />
A camera-centered 3D grid of irradiance probes follows the camera. The deferred light pass swaps the old flat ambient term for trilinearly interpolated probe irradiance.</li>
<li><span style="font-weight: bold;">Toroidal scrolling</span><br />
<br />
As the camera moves, the probe volume scrolls toroidally, addressed by integer cell index, instead of re-capturing everything. This prevents a re-capture storm when moving through the world.<br />
<br />
This also makes it floating-origin and large-world safe because the GI volume effectively rebases for free.</li>
<li><span style="font-weight: bold;">Cascades</span><br />
<br />
Range comes from nested cascades at geometrically increasing spacing. Three cascades cover roughly 24 m to 216 m, with a cross-cascade seam blend so there is no hard visible ring at the cascade boundaries.</li>
<li><span style="font-weight: bold;">DDGI-standard atlases</span><br />
<br />
Each probe captures visibility, either rasterized into a small cube or ray-traced, then compute integrates the result into octahedral irradiance and Chebyshev depth-moment atlases.<br />
<br />
The depth moments suppress light leaking through thin walls, and a surface-normal bias helps as well.</li>
<li><span style="font-weight: bold;">Decoupled capture vs. relight</span><br />
<br />
Geometry capture and lighting relight are separated. Both are hard-capped per frame.<br />
<br />
The expensive geometry capture is time-sliced and budgeted, so the cost stays bounded. A static scene captures nothing, so it is free when you are not moving. Irradiance converges through temporal blending.</li>
<li><span style="font-weight: bold;">Free time-of-day relight</span><br />
<br />
An optional producer bakes a lighting-independent cube once and relights it each frame with per-bounce sun shadowing. Moving the sun costs no re-capture.</li>
<li><span style="font-weight: bold;">Analytic re-seeding</span><br />
<br />
Scrolled-in probe slots are analytically re-seeded, which kills stale-bounce ghosting during fast movement.</li>
<li><span style="font-weight: bold;">Half-resolution evaluation</span><br />
<br />
Half-res evaluation plus depth-aware bilateral upscale keeps the light-pass cost down.</li>
<li><span style="font-weight: bold;">Optional SSGI</span><br />
<br />
A screen-space near-field bounce layer can be enabled on top of the probe GI for close-range contact GI.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Hardware ray-traced path — opt-in, additive</span></span><br />
<br />
When ray-query hardware is present and a scene acceleration structure is registered, probe capture can be ray-traced instead of rasterized. This gives correct per-direction occlusion and bounce with no cube-face seams.<br />
<ul>
<li><span style="font-weight: bold;">RT occlusion moments</span><br />
<br />
GI.raytrace replaces the depth-cube producer with ray-traced occlusion moments. The light pass is unchanged.</li>
<li><span style="font-weight: bold;">RT bounce radiance</span><br />
<br />
GI.raytrace_radiance traces the diffuse bounce itself using cosine-weighted hemisphere gathering with Fibonacci-spiral rays.<br />
<br />
It supports textured and average-color albedo. Terrain with no UVs samples the texture’s mean color. It uses the correct hit-surface normal, NdotL, sun shadow rays, radiosity-style multi-bounce, and a per-frame sky-only cube so miss rays sample the real rendered sky.<br />
<br />
It reaches pixel parity with the rasterized producer at the same intensity on both Cornell and world scenes.</li>
<li><span style="font-weight: bold;">Zero-registration mode</span><br />
<br />
GIRayTrace.auto_scene = true lets the renderer feed every opaque mesh it draws into the acceleration structure automatically.<br />
<br />
Streamed world areas enter the TLAS as they draw and drop out as they stream away, with no app code required. Rebuilds are recorded into the frame command buffer, so there is no GPU stall.<br />
</li></ul>
<br />
The system is fully hardware-gated and TLAS-gated. If RT is off, unsupported, or unregistered, it falls back to rasterized capture. The software DDGI path is untouched.<br />
<br />
Verified working on both AMD RADV and NVIDIA.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demos</span></span><br />
<br />
Vulkan build, under Tutorials/Source/14 - Game Basics/<br />
<br />
<span style="font-weight: bold;">Tutorial_14_DynamicGI</span><br />
The basics: a neutral scene picking up colored bounce light, with live time-of-day. This is the color-bleed proof.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_WorldGI</span><br />
GI over a real streamed Game::World with terrain and props, plus point-light and time controls.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_LargeWorldGI</span><br />
GI running under the large-world floating origin. This proves it survives rebases without re-warming.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_GIShowcase</span><br />
The “all the knobs” demo. A Cornell room with split-screen A/B.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">V</span> — split-screen A/B<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
[]<span style="font-weight: bold;">1 / 2 / 3</span> — cascade count<br />
[]<span style="font-weight: bold;">R</span> — relight<br />
[]<span style="font-weight: bold;">B</span> — Chebyshev<br />
[]<span style="font-weight: bold;">A</span> — TAA<br />
[]<span style="font-weight: bold;">N</span> — SSGI<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">U</span> — RT radiance<br />
[]Sun, light, and wall toggles<br />
</li></ul>
<br />
<span style="font-weight: bold;">Tutorial_14_RTWorldGI</span><br />
The payoff: ray-traced GI over a playable streamed world via the zero-registration auto-scene hook.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">WASD</span> — move<br />
[]<span style="font-weight: bold;">G</span> — GI on/off, showing near-black ambient vs. bounced sunlight<br />
[]<span style="font-weight: bold;">U</span> — RT radiance vs. rasterized capture<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">T</span> — time-of-day<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it is turned off and on</span></span><br />
<br />
Everything defaults off at the engine level. These are the relevant defaults on the global GI object in Engine/H/Graphics/GI.h, plus GIRayTrace:<br />
<br />
<span style="font-weight: bold;">GI.enabled</span><br />
Default: false<br />
Turns on the whole system. Off means legacy flat ambient.<br />
<br />
<span style="font-weight: bold;">GIRayTrace.auto_scene</span><br />
Default: false<br />
Turns on zero-registration RT scene feeding.<br />
<br />
<span style="font-weight: bold;">GI.raytrace</span><br />
Default: false<br />
Turns on ray-traced occlusion moments.<br />
<br />
<span style="font-weight: bold;">GI.raytrace_radiance</span><br />
Default: false<br />
Turns on ray-traced bounce radiance.<br />
<br />
<span style="font-weight: bold;">GI.relight</span><br />
Default: false<br />
Turns on the free-time-of-day relight producer.<br />
<br />
<span style="font-weight: bold;">GI.ssgi</span><br />
Default: false<br />
Turns on screen-space near-field bounce.<br />
<br />
<span style="font-weight: bold;">GI.capture_shadows</span><br />
Default: false<br />
Turns on sun-shadowed lit-cube capture.<br />
<br />
<span style="font-weight: bold;">GI.chebyshev</span><br />
Default: true<br />
Turns on leak suppression when GI is enabled.<br />
<br />
<span style="font-weight: bold;">GI.half_res</span><br />
Default: true<br />
Turns on half-res evaluation and upscale.<br />
<br />
<span style="font-weight: bold;">GI.auto_light</span><br />
Default: true<br />
Derives sun and sky lighting from the live scene.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Canonical app opt-in path</span></span><br />
<br />
An app opts in from InitPre or Init:<br />
<br />
GI.enabled = true;<br />
GI.intensity = 2.0f;<br />
GIRayTrace.auto_scene = true;<br />
GI.raytrace_radiance = true;<br />
<br />
GI.enabled is the master switch. Nothing renders GI without it.<br />
<br />
GI.intensity controls the indirect multiplier, where 1 is physical.<br />
<br />
GIRayTrace.auto_scene is optional and lets the renderer register the scene for ray tracing automatically.<br />
<br />
GI.raytrace_radiance is optional and enables ray-traced bounce radiance. It falls back if unsupported.<br />
<br />
There are also EE_GI environment-variable overrides, such as:<br />
<br />
EE_GI=1<br />
EE_GI_RAYTRACE_RADIANCE=1<br />
EE_GI_INTENSITY=...<br />
<br />
Those exist mainly for headless A/B testing. They are not the intended shipping path.<br />
<br />
Beyond the app switch, GI is gated on backend support. GI.active() requires the Vulkan compute path. The RT path is additionally gated on ray-query hardware.<br />
<br />
So on a non-Vulkan backend, or a GPU without ray-query support, the relevant paths simply no-op or fall back.<br />
<br />
Net result: if you do not opt in, nothing about your scene changes.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Build and run one demo</span></span><br />
<br />
cmake --build cmake-build-release-vulkan --target Tutorial_14_GIShowcase -j &#36;(nproc)<br />
<br />
EE_NO_MSG_BOX=1 ./Tutorials/Tutorial_14_GIShowcase<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Documentation</span></span><br />
<br />
Full design notes and knob reference are in:<br />
<ul>
<li>[]The README’s Dynamic Global Illumination section<br />
[]docs/gi-raytracing-design.md<br />
</li></ul>
<br />
Available in this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[July 2026]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11831</link>
			<pubDate>Fri, 03 Jul 2026 04:58:27 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11831</guid>
			<description><![CDATA[Updated Esenthel Source:<br />
-fixed bug in matrix inversion]]></description>
			<content:encoded><![CDATA[Updated Esenthel Source:<br />
-fixed bug in matrix inversion]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[[Elements] Addition of more formats]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11830</link>
			<pubDate>Sun, 28 Jun 2026 11:35:14 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11830</guid>
			<description><![CDATA[Hi there,<br />
<br />
Theora has been around for quite some time, and many of the newer video formats are not supported. Can we check if we can include more formats for videos? <img src="images/smilies/smile.gif" style="vertical-align: middle;" border="0" alt="smile" title="smile" /><br />
<br />
Thanks a bunch!]]></description>
			<content:encoded><![CDATA[Hi there,<br />
<br />
Theora has been around for quite some time, and many of the newer video formats are not supported. Can we check if we can include more formats for videos? <img src="images/smilies/smile.gif" style="vertical-align: middle;" border="0" alt="smile" title="smile" /><br />
<br />
Thanks a bunch!]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Titan Editor v77.0.0.0 reports projects created with newer Titan Engine ver]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11829</link>
			<pubDate>Sun, 28 Jun 2026 11:21:38 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11829</guid>
			<description><![CDATA[Hello,<br />
I'm experiencing an issue where the Titan Editor (<span style="font-weight: bold;">binary version 77.0.0.0</span>) refuses to open projects I purchased from the Esenthel Store, claiming they were created with a newer version of Titan Engine.<br />
<span style="font-weight: bold;">Affected projects</span>: <span style="font-style: italic;">Esenthel MMO (Source Code)</span> and <span style="font-style: italic;">Esenthel RPG 2 (Source Code)</span>.<br />
<span style="font-weight: bold;">Error message</span>: "This project was created with a newer version of Titan Engine. Please upgrade your Titan software and try again."<br />
<span style="font-weight: bold;">Steps already taken</span>: Re-downloaded the installer from the official source, Completely reinstalled Titan Editor, and Verified that the installed version remains 77.0.0.0<br />
<br />
I do not have access to the Titan Engine source code. the binary version appears to be outdated compared to what the purchased projects require, this prevents me from using products I have legally purchased<br />
<br />
Any guidance on how to resolve this version mismatch would be greatly appreciated.<br />
<br />
Thank you in advance for your help.<br /><!-- start: postbit_attachments_attachment -->
<br /><img src="images/attachtypes/image.gif" border="0" alt=".png" />&nbsp;&nbsp;<a href="attachment.php?aid=3333" target="_blank">Captura de tela 2026-06-28 074545.png</a> (Size: 114.73 KB / Downloads: 8)
<!-- end: postbit_attachments_attachment --><br /><!-- start: postbit_attachments_attachment -->
<br /><img src="images/attachtypes/image.gif" border="0" alt=".png" />&nbsp;&nbsp;<a href="attachment.php?aid=3334" target="_blank">Captura de tela 2026-06-28 080700.png</a> (Size: 11.98 KB / Downloads: 8)
<!-- end: postbit_attachments_attachment -->]]></description>
			<content:encoded><![CDATA[Hello,<br />
I'm experiencing an issue where the Titan Editor (<span style="font-weight: bold;">binary version 77.0.0.0</span>) refuses to open projects I purchased from the Esenthel Store, claiming they were created with a newer version of Titan Engine.<br />
<span style="font-weight: bold;">Affected projects</span>: <span style="font-style: italic;">Esenthel MMO (Source Code)</span> and <span style="font-style: italic;">Esenthel RPG 2 (Source Code)</span>.<br />
<span style="font-weight: bold;">Error message</span>: "This project was created with a newer version of Titan Engine. Please upgrade your Titan software and try again."<br />
<span style="font-weight: bold;">Steps already taken</span>: Re-downloaded the installer from the official source, Completely reinstalled Titan Editor, and Verified that the installed version remains 77.0.0.0<br />
<br />
I do not have access to the Titan Engine source code. the binary version appears to be outdated compared to what the purchased projects require, this prevents me from using products I have legally purchased<br />
<br />
Any guidance on how to resolve this version mismatch would be greatly appreciated.<br />
<br />
Thank you in advance for your help.<br /><!-- start: postbit_attachments_attachment -->
<br /><img src="images/attachtypes/image.gif" border="0" alt=".png" />&nbsp;&nbsp;<a href="attachment.php?aid=3333" target="_blank">Captura de tela 2026-06-28 074545.png</a> (Size: 114.73 KB / Downloads: 8)
<!-- end: postbit_attachments_attachment --><br /><!-- start: postbit_attachments_attachment -->
<br /><img src="images/attachtypes/image.gif" border="0" alt=".png" />&nbsp;&nbsp;<a href="attachment.php?aid=3334" target="_blank">Captura de tela 2026-06-28 080700.png</a> (Size: 11.98 KB / Downloads: 8)
<!-- end: postbit_attachments_attachment -->]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Improved Water]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11828</link>
			<pubDate>Mon, 22 Jun 2026 21:29:11 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11828</guid>
			<description><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/wfnoh9" target="_blank">https://streamable.com/wfnoh9</a><br />
<br />
Pics:<br />
<br />
<a href="https://postimg.cc/9zSBqkDZ" target="_blank"><img src="https://i.postimg.cc/ZKYsXtZ7/Screenshot-From-2026-06-22-17-05-16.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-05-16.png]" /></a><br />
<br />
<a href="https://postimg.cc/z3MkRQyV" target="_blank"><img src="https://i.postimg.cc/CLMr9TYH/Screenshot-From-2026-06-22-17-06-14.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-06-14.png]" /></a><br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Water Rendering Overhaul — Foam, Depth Color, Caustics, Gerstner Swell</span></span><br />
<br />
I've spent some time giving the water system a proper pass.<br />
<br />
The engine's water was already strong: scrolling normal maps, Fresnel, planar + cubemap + SSR reflection, depth refraction with leak detection, density absorption, underwater fog, capsule ripples, and large-world support.<br />
<br />
This work fills in the pieces that were missing or weak:<br />
<ul>
<li>Shoreline foam</li>
<li>Depth-graded surface color</li>
<li>Procedural caustics</li>
<li>Whitecap foam</li>
<li>A real Gerstner wave model with choppiness and wave-speed control<br />
</li></ul>
<br />
Everything is driven by new per-material parameters on <span style="font-weight: bold;">WaterMtrl</span>, all exposed in the <span style="font-weight: bold;">Titan Editor → Water Material</span> editor.<br />
<br />
The subtle effects — foam, depth color, and caustics — are on by default at low intensity, so existing water looks a little better out of the box without re-authoring.<br />
<br />
The heavier Gerstner swell is opt-in:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">What's New</span></span><br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Depth-Graded Surface Color</span></span><br />
<br />
The surface now blends from a <span style="font-weight: bold;">Shallow Color</span> in thin water to the base <span style="font-weight: bold;">Color</span> in deep water, using the water-column depth the shader already computes.<br />
<br />
This works especially well for carved 3–100 m procedural lakes/rivers and beaches.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Shallow Color</span></li>
<li><span style="font-weight: bold;">Depth Color Distance</span> — metres over which it blends; <span style="font-weight: bold;">0 = off</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Animated Shoreline Foam</span></span><br />
<br />
A foam band now appears where the water column gets thin — shorelines, around rocks, and anywhere water meets geometry.<br />
<br />
The animation is fully procedural value noise, so no texture asset is required. It churns organically instead of reading as a static pattern.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Foam Color</span></li>
<li><span style="font-weight: bold;">Foam Amount</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Foam Depth</span> — band width in metres<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Procedural Caustics</span></span><br />
<br />
A cheap two-layer cellular caustic network is projected onto the submerged background and faded by depth.<br />
<br />
It is strongest in clear shallows and disappears in deep or opaque water. There is no texture binding, and it is effectively free where the water is deep.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Caustic Scale</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Caustic Intensity</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Gerstner Swell — Ocean Technique</span></span><br />
<br />
The Ocean technique now supports real summed-wave vertex displacement with analytic normals.<br />
<br />
Set:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
to switch the Ocean plane from the legacy texture-bump waves to a proper Gerstner wave sum.<br />
<br />
Up to 8 waves are synthesized from a few high-level controls.<br />
<br />
Includes:<br />
<ul>
<li><span style="font-weight: bold;">Choppiness</span> — horizontal crest displacement for peaked tops and flatter troughs, which is the defining Gerstner term.</li>
<li><span style="font-weight: bold;">Wave Speed</span> — multiplier on physical deep-water dispersion. <span style="font-weight: bold;">1 = realistic</span>; lower values produce lazy swell, higher values produce faster chop.</li>
<li><span style="font-weight: bold;">Per-wavelength distance fade</span> — long swells carry to distance while short chop settles out.</li>
<li><span style="font-weight: bold;">Global horizon envelope</span> — flattens displacement before the horizon so the far ocean does not alias into shimmer or grid moiré.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Wave Count</span></li>
<li><span style="font-weight: bold;">Wave Length</span></li>
<li><span style="font-weight: bold;">Wave Amplitude</span></li>
<li><span style="font-weight: bold;">Wave Choppiness</span></li>
<li><span style="font-weight: bold;">Wave Wind</span> — direction</li>
<li><span style="font-weight: bold;">Wave Speed</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Whitecap / Crest Foam</span></span><br />
<br />
Slope-based foam now builds on steep wave faces as the sea picks up.<br />
<br />
It is biased onto the downwind, breaking face of the wave rather than appearing symmetrically on both sides.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demo</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_OceanWater</span> shows everything running on the global sea over a sloped, bumpy seabed rising to a beach.<br />
<br />
Everything is runtime-tunable:<br />
<ul>
<li><span style="font-weight: bold;">Z / X</span> — calm ↔ storm; ramps wave count, length, amplitude, choppiness, and whitecaps</li>
<li><span style="font-weight: bold;">O / P</span> — wave speed</li>
<li><span style="font-weight: bold;">, / .</span> — rotate wind</li>
<li><span style="font-weight: bold;">F / C</span> — toggle foam / caustics</li>
<li><span style="font-weight: bold;">Mouse swing + wheel</span> — camera look and zoom<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/wfnoh9" target="_blank">https://streamable.com/wfnoh9</a><br />
<br />
Pics:<br />
<br />
<a href="https://postimg.cc/9zSBqkDZ" target="_blank"><img src="https://i.postimg.cc/ZKYsXtZ7/Screenshot-From-2026-06-22-17-05-16.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-05-16.png]" /></a><br />
<br />
<a href="https://postimg.cc/z3MkRQyV" target="_blank"><img src="https://i.postimg.cc/CLMr9TYH/Screenshot-From-2026-06-22-17-06-14.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-06-14.png]" /></a><br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Water Rendering Overhaul — Foam, Depth Color, Caustics, Gerstner Swell</span></span><br />
<br />
I've spent some time giving the water system a proper pass.<br />
<br />
The engine's water was already strong: scrolling normal maps, Fresnel, planar + cubemap + SSR reflection, depth refraction with leak detection, density absorption, underwater fog, capsule ripples, and large-world support.<br />
<br />
This work fills in the pieces that were missing or weak:<br />
<ul>
<li>Shoreline foam</li>
<li>Depth-graded surface color</li>
<li>Procedural caustics</li>
<li>Whitecap foam</li>
<li>A real Gerstner wave model with choppiness and wave-speed control<br />
</li></ul>
<br />
Everything is driven by new per-material parameters on <span style="font-weight: bold;">WaterMtrl</span>, all exposed in the <span style="font-weight: bold;">Titan Editor → Water Material</span> editor.<br />
<br />
The subtle effects — foam, depth color, and caustics — are on by default at low intensity, so existing water looks a little better out of the box without re-authoring.<br />
<br />
The heavier Gerstner swell is opt-in:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">What's New</span></span><br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Depth-Graded Surface Color</span></span><br />
<br />
The surface now blends from a <span style="font-weight: bold;">Shallow Color</span> in thin water to the base <span style="font-weight: bold;">Color</span> in deep water, using the water-column depth the shader already computes.<br />
<br />
This works especially well for carved 3–100 m procedural lakes/rivers and beaches.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Shallow Color</span></li>
<li><span style="font-weight: bold;">Depth Color Distance</span> — metres over which it blends; <span style="font-weight: bold;">0 = off</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Animated Shoreline Foam</span></span><br />
<br />
A foam band now appears where the water column gets thin — shorelines, around rocks, and anywhere water meets geometry.<br />
<br />
The animation is fully procedural value noise, so no texture asset is required. It churns organically instead of reading as a static pattern.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Foam Color</span></li>
<li><span style="font-weight: bold;">Foam Amount</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Foam Depth</span> — band width in metres<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Procedural Caustics</span></span><br />
<br />
A cheap two-layer cellular caustic network is projected onto the submerged background and faded by depth.<br />
<br />
It is strongest in clear shallows and disappears in deep or opaque water. There is no texture binding, and it is effectively free where the water is deep.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Caustic Scale</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Caustic Intensity</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Gerstner Swell — Ocean Technique</span></span><br />
<br />
The Ocean technique now supports real summed-wave vertex displacement with analytic normals.<br />
<br />
Set:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
to switch the Ocean plane from the legacy texture-bump waves to a proper Gerstner wave sum.<br />
<br />
Up to 8 waves are synthesized from a few high-level controls.<br />
<br />
Includes:<br />
<ul>
<li><span style="font-weight: bold;">Choppiness</span> — horizontal crest displacement for peaked tops and flatter troughs, which is the defining Gerstner term.</li>
<li><span style="font-weight: bold;">Wave Speed</span> — multiplier on physical deep-water dispersion. <span style="font-weight: bold;">1 = realistic</span>; lower values produce lazy swell, higher values produce faster chop.</li>
<li><span style="font-weight: bold;">Per-wavelength distance fade</span> — long swells carry to distance while short chop settles out.</li>
<li><span style="font-weight: bold;">Global horizon envelope</span> — flattens displacement before the horizon so the far ocean does not alias into shimmer or grid moiré.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Wave Count</span></li>
<li><span style="font-weight: bold;">Wave Length</span></li>
<li><span style="font-weight: bold;">Wave Amplitude</span></li>
<li><span style="font-weight: bold;">Wave Choppiness</span></li>
<li><span style="font-weight: bold;">Wave Wind</span> — direction</li>
<li><span style="font-weight: bold;">Wave Speed</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Whitecap / Crest Foam</span></span><br />
<br />
Slope-based foam now builds on steep wave faces as the sea picks up.<br />
<br />
It is biased onto the downwind, breaking face of the wave rather than appearing symmetrically on both sides.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demo</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_OceanWater</span> shows everything running on the global sea over a sloped, bumpy seabed rising to a beach.<br />
<br />
Everything is runtime-tunable:<br />
<ul>
<li><span style="font-weight: bold;">Z / X</span> — calm ↔ storm; ramps wave count, length, amplitude, choppiness, and whitecaps</li>
<li><span style="font-weight: bold;">O / P</span> — wave speed</li>
<li><span style="font-weight: bold;">, / .</span> — rotate wind</li>
<li><span style="font-weight: bold;">F / C</span> — toggle foam / caustics</li>
<li><span style="font-weight: bold;">Mouse swing + wheel</span> — camera look and zoom<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[GPU Mesh Shader Grass GoT Style]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11827</link>
			<pubDate>Fri, 19 Jun 2026 11:32:44 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11827</guid>
			<description><![CDATA[<span style="font-size: x-large;">Best seen in video not pictures:<br />
<br />
<br />
<a href="https://streamable.com/hu3bzs" target="_blank">https://streamable.com/hu3bzs</a></span><br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/FkfNNN4X" target="_blank"><img src="https://i.postimg.cc/tJkTd4vJ/Screenshot-From-2026-06-19-07-09-33.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-09-33.png]" /></a><br />
<br />
Grass density can be set per terrain material in Titan Editor:<br />
<br />
<a href="https://postimg.cc/zHL8881P" target="_blank"><img src="https://i.postimg.cc/3N1Rjxf7/Screenshot-From-2026-06-19-07-25-00.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-25-00.png]" /></a><br />
<br />
AI written summary:<br />
<br />
GPU Grass (Ghost-of-Tsushima-style) for Titan Engine — feature + tutorial<br />
<br />
I've added a GPU-driven grass system to my Esenthel/Titan fork and wanted to share it. It generates grass entirely on the GPU each frame using Vulkan mesh shaders, writing straight into the deferred G-buffer — so the engine's normal lighting, shadows, SSR, and fog apply for free. It's a parallel path that bypasses the shader/material system (the shader compiler has no mesh/task stage), and it's purely additive — the legacy CPU GrassObj foliage still works untouched.<br />
<br />
What it does<br />
<br />
- Deferred, lit grass on the GPU — VK_EXT_mesh_shader; blades generated per frame, no vertex/index buffers. Vulkan-only (inert on GL/DX, which keep the legacy grass).<br />
- Terrain-following + per-material density — a camera-centered control texture samples terrain height + a density built from terrain slope × the material's grass_density. Blades drape the heightfield, thin on slopes, and grow only where you author them.<br />
- World-anchored + smooth LOD — blades are seeded by absolute world position (no "swim" as the field re-centers), and density fades smoothly with distance instead of popping.<br />
- Interaction:<br />
  - trample(pos, radius, strength) — footstep flatten that springs back; a decoupled, world-anchored field uploaded dirty-region only.<br />
  - blast(pos, radius, strength) — one-shot radial knockdown shockwave that springs back.<br />
  - Exclusion zones — addZoneCircle / addZoneBox(center, half, yaw, soft) → handle, removeZone: persistent analytic CUT footprints (e.g. building placements) where grass is removed with a soft edge, plus a per-patch "fully enclosed → skip" early-out.<br />
- Wind — analytic flow field with traveling gust fronts + a slow sway.<br />
<br />
Opt-in by material<br />
<br />
Grass density is a per-material attribute (Material::grass_density, 0..1) with an editor "Grass Density" slider. It defaults to 0, so GPU grass is opt-in: enabling the system on an existing world shows nothing until you raise density on the grass materials. Author it toward 1 on grass/dirt, leave it 0 on rock/sand/snow/paths. (This deliberately avoids retro-fitting grass onto every existing material.)<br />
<br />
The demo — Tutorial_14_GpuGrass<br />
<br />
A character standing in a GPU-grass field on real terrain. Controls:<br />
<br />
- WSAD — walk (leaves a trample trail)<br />
- B — blast knockdown<br />
- G — toggle a building CUT zone at the character<br />
- F — force full density everywhere (handy on a world whose materials aren't authored yet)<br />
- Mouse — orbit, Wheel — zoom, Esc — quit<br />
<br />
Note: because density is opt-in, the demo shows no grass until you either set a terrain material's Grass Density &gt; 0 in the editor or press F — there's an on-screen hint for this.<br />
<br />
Implementation notes / caveats<br />
<br />
- Vulkan-only, gated on VkMeshShaderSupported.<br />
- Shaders (grass.task/.mesh/.frag + grass_common.hlsli) are HLSL compiled to SPIR-V by the vendored DXC at engine build time — a parallel path, not via EERegen, so no shader tooling needed at build.<br />
- Drawn from RendererClass::opaque() (RT_DEFERRED) via VkDrawGrass; its own self-contained UBO + descriptor set (not a ShaderVK); reverse-Z + negative-height viewport matched to the deferred config.<br />
- Grass is tagged PSM_TRANSLUCENT and excluded from SSAO (tblades otherwise make screen-space AO shimmer at distance).<br />
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-size: x-large;">Best seen in video not pictures:<br />
<br />
<br />
<a href="https://streamable.com/hu3bzs" target="_blank">https://streamable.com/hu3bzs</a></span><br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/FkfNNN4X" target="_blank"><img src="https://i.postimg.cc/tJkTd4vJ/Screenshot-From-2026-06-19-07-09-33.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-09-33.png]" /></a><br />
<br />
Grass density can be set per terrain material in Titan Editor:<br />
<br />
<a href="https://postimg.cc/zHL8881P" target="_blank"><img src="https://i.postimg.cc/3N1Rjxf7/Screenshot-From-2026-06-19-07-25-00.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-25-00.png]" /></a><br />
<br />
AI written summary:<br />
<br />
GPU Grass (Ghost-of-Tsushima-style) for Titan Engine — feature + tutorial<br />
<br />
I've added a GPU-driven grass system to my Esenthel/Titan fork and wanted to share it. It generates grass entirely on the GPU each frame using Vulkan mesh shaders, writing straight into the deferred G-buffer — so the engine's normal lighting, shadows, SSR, and fog apply for free. It's a parallel path that bypasses the shader/material system (the shader compiler has no mesh/task stage), and it's purely additive — the legacy CPU GrassObj foliage still works untouched.<br />
<br />
What it does<br />
<br />
- Deferred, lit grass on the GPU — VK_EXT_mesh_shader; blades generated per frame, no vertex/index buffers. Vulkan-only (inert on GL/DX, which keep the legacy grass).<br />
- Terrain-following + per-material density — a camera-centered control texture samples terrain height + a density built from terrain slope × the material's grass_density. Blades drape the heightfield, thin on slopes, and grow only where you author them.<br />
- World-anchored + smooth LOD — blades are seeded by absolute world position (no "swim" as the field re-centers), and density fades smoothly with distance instead of popping.<br />
- Interaction:<br />
  - trample(pos, radius, strength) — footstep flatten that springs back; a decoupled, world-anchored field uploaded dirty-region only.<br />
  - blast(pos, radius, strength) — one-shot radial knockdown shockwave that springs back.<br />
  - Exclusion zones — addZoneCircle / addZoneBox(center, half, yaw, soft) → handle, removeZone: persistent analytic CUT footprints (e.g. building placements) where grass is removed with a soft edge, plus a per-patch "fully enclosed → skip" early-out.<br />
- Wind — analytic flow field with traveling gust fronts + a slow sway.<br />
<br />
Opt-in by material<br />
<br />
Grass density is a per-material attribute (Material::grass_density, 0..1) with an editor "Grass Density" slider. It defaults to 0, so GPU grass is opt-in: enabling the system on an existing world shows nothing until you raise density on the grass materials. Author it toward 1 on grass/dirt, leave it 0 on rock/sand/snow/paths. (This deliberately avoids retro-fitting grass onto every existing material.)<br />
<br />
The demo — Tutorial_14_GpuGrass<br />
<br />
A character standing in a GPU-grass field on real terrain. Controls:<br />
<br />
- WSAD — walk (leaves a trample trail)<br />
- B — blast knockdown<br />
- G — toggle a building CUT zone at the character<br />
- F — force full density everywhere (handy on a world whose materials aren't authored yet)<br />
- Mouse — orbit, Wheel — zoom, Esc — quit<br />
<br />
Note: because density is opt-in, the demo shows no grass until you either set a terrain material's Grass Density &gt; 0 in the editor or press F — there's an on-screen hint for this.<br />
<br />
Implementation notes / caveats<br />
<br />
- Vulkan-only, gated on VkMeshShaderSupported.<br />
- Shaders (grass.task/.mesh/.frag + grass_common.hlsli) are HLSL compiled to SPIR-V by the vendored DXC at engine build time — a parallel path, not via EERegen, so no shader tooling needed at build.<br />
- Drawn from RendererClass::opaque() (RT_DEFERRED) via VkDrawGrass; its own self-contained UBO + descriptor set (not a ShaderVK); reverse-Z + negative-height viewport matched to the deferred config.<br />
- Grass is tagged PSM_TRANSLUCENT and excluded from SSAO (tblades otherwise make screen-space AO shimmer at distance).<br />
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Terrain Anti-Tiling: distance dual-scale + stochastic "by-example" no asset pipeline]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11826</link>
			<pubDate>Fri, 19 Jun 2026 03:57:20 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11826</guid>
			<description><![CDATA[<span style="font-size: large;"><span style="font-weight: bold;">Terrain Anti-Tiling: distance dual-scale + stochastic “by-example” (no asset pipeline)</span></span><br />
<br />
Before anti-tile:<br />
<a href="https://postimg.cc/fSLfnkSR" target="_blank"><img src="https://i.postimg.cc/br0Cntzk/Screenshot-From-2026-06-18-23-39-08.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-39-08.png]" /></a><br />
<br />
After anti-tile:<br />
<a href="https://postimg.cc/w3TVs4fF" target="_blank"><img src="https://i.postimg.cc/44pFqrzC/Screenshot-From-2026-06-18-23-38-58.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-38-58.png]" /></a><br />
<br />
Works best when looking out over the terrain, looking straight down at terrain it doesn't help as much.<br />
<br />
<br />
<br />
Written by AI:<br />
<br />
Repeating ground textures betray themselves two different ways, and they need two different fixes:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">The far-field grid</span> — from a hill or a grazing camera, the regular <span style="font-style: italic;">periodicity</span> of the texture reads as a checkerboard receding to the horizon.</li>
<li><span style="font-weight: bold;">The close/mid feature repeat</span> — that one distinctive grass clump / rock / crack showing up again every few meters right in front of you.<br />
</li></ol>
<br />
Two complementary techniques in the deferred terrain shader (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Engine/Source/Shaders/Deferred.cpp</code></div></div>
) address each. Both run on the <span style="font-weight: bold;">single- and multi-material</span> terrain paths, up to 5 blended materials, and both are <span style="font-weight: bold;">gated to terrain only</span> (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#if HEIGHTMAP &amp;&amp; SET_POS</code></div></div>
), so buildings, props, and characters are completely untouched.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">1. Distance-based dual-scale: kills the far grid</span></span><br />
<br />
Sample the same ground texture at <span style="font-weight: bold;">two UV scales</span> and blend in the lower-frequency, larger-tile one with camera distance:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>tex = lerp(near, RTex(Col, uv * TileFarScale), fade(distance));</code></div></div>
<br />
Near the camera you get full detail. Far away, the tight repeat dissolves into a coarser, non-aligned pattern. This is the standard Unreal-landscape-style trick, and it is <span style="font-weight: bold;">mip-safe for free</span>, because the two samples use implicit derivatives, so no <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 is needed.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">2. Stochastic “by-example”: kills the near/mid feature repeat</span></span><br />
<br />
This is <span style="font-weight: bold;">Heitz–Neyret’s by-example noise, minus the histogram LUT</span>, which means it needs <span style="font-weight: bold;">no precomputed assets</span> and no per-texture bake.<br />
<ul>
<li><span style="font-weight: bold;">Triangular / “hex” grid</span> — 3 samples, no axis-aligned blend seams.</li>
<li><span style="font-weight: bold;">Per-cell random offset only, no mirror</span> — the mirror in the cheaper grid variants is what causes those directional scratches/scuffs on grass. Dropping it removes them.</li>
<li><span style="font-weight: bold;">Variance-preserving blend</span> — naive averaging washes out contrast, so instead of <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>sum(w_i * c_i)</code></div></div>
, we blend the deviation from the mean and renormalize by <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>1 / sqrt(sum(w_i^2))</code></div></div>
. The mean is read from the texture’s <span style="font-weight: bold;">smallest mip</span>, approximately its average color, so we recover contrast with zero precomputation.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 keeps mipmapping correct.<br />
</li></ul>
<br />
The result: a single tiling texture stops repeating and reads as a natural, non-periodic surface — no scratches, no muddy blend blobs.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">How to use it</span></span><br />
<br />
Everything is driven by a single <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTile</code></div></div>
 shader cbuffer, set from the public shader-param API:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet("TerrainTileEnable", 1.0f);&nbsp;&nbsp;// distance effect<br />
SPSet("TileFarScale", 0.22f);<br />
SPSet("TileFadeStart", 5.0f);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ...TileFadeRange, TileFarMax<br />
<br />
SPSet("TerrainStochastic", 1.0f);&nbsp;&nbsp;// stochastic de-tiling</code></div></div>
<br />
<span style="font-weight: bold;">It is off by default.</span> The cbuffer is zero-initialized, so nothing changes unless your app sets the params. The editor and all other content render exactly as before.<br />
<br />
The params only exist once the terrain shader has loaded, so set them <span style="font-weight: bold;">each frame before drawing the world</span>. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet</code></div></div>
 safely no-ops until then. There is no engine-wide default yet; you opt in from your render loop.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Parameters</span></span><br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTileEnable</code></div></div>
 — 0/1, distance dual-scale on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarScale</code></div></div>
 — coarse UV multiplier. Smaller means bigger far tiles and a stronger frequency break.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeStart</code></div></div>
 — meters: distance where the coarse blend begins.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeRange</code></div></div>
 — meters: distance over which it ramps to <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
 — max fraction of the coarse sample blended in at far distance.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainStochastic</code></div></div>
 — 0/1, stochastic de-tiling on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
 — performance limit: only de-tile within this distance. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>&lt;=0</code></div></div>
 means no limit.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
 — performance limit: skip materials whose blend weight is below this.  means de-tile all.<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Performance limits: opt-in, runtime</span></span><br />
<br />
Multi-material stochastic can hit up to <span style="font-weight: bold;">5 materials × 4 taps = 20 albedo taps</span>, so there are two runtime knobs:<br />
<ul>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
</span> — only run the stochastic taps within this distance. The far grid is handled by the distance blend anyway, so far pixels drop straight back to 1 tap.</li>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
</span> — skip stochastic for a material whose per-pixel blend weight is below a threshold, so a barely-visible layer does not cost 4 taps.<br />
</li></ul>
<br />
Both default to “no practical limit.”<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Large-world / floating-origin</span></span><br />
<br />
Terrain UVs are <span style="font-style: italic;">world-continuous</span>, built from the absolute area index, so far out in a big world they grow large.<br />
<br />
A <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Frac(sin(...))</code></div></div>
 hash loses precision there and the de-tiling starts aliasing, so the stochastic cell hash is a <span style="font-weight: bold;">PCG2D integer hash</span> on the integer cell ID, exact at any magnitude.<br />
<br />
The pattern stays locked to the ground and is transparent to floating-origin rebasing. UVs deliberately <span style="font-weight: bold;">do not</span> rebase. Shifting them would move the non-integer-aligned stochastic grid and pop the pattern every rebase.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Tutorials</span></span><br />
<br />
<span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_TerrainTiling</code></div></div>
</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorials/Source/14 - Game Basics/36 - Terrain Tiling.cpp</code></div></div>
<br />
The live A/B demo. It loads a textured world and lets you toggle and tune everything in real time:<br />
<ul>
<li><span style="font-weight: bold;">T</span> — distance anti-tiling on/off.</li>
<li><span style="font-weight: bold;">Y</span> — stochastic de-tiling on/off.</li>
<li><span style="font-weight: bold;">1–8</span> — distance params: strength, fade start, fade range, coarse scale.</li>
<li><span style="font-weight: bold;">G / H</span> — stochastic max distance, for performance.</li>
<li><span style="font-weight: bold;">J / K</span> — stochastic min weight, for performance.</li>
<li><span style="font-weight: bold;">9</span> — reset all to defaults.</li>
<li>Hold <span style="font-weight: bold;">RMB</span> — look.</li>
<li><span style="font-weight: bold;">Mouse wheel</span> — zoom.</li>
<li><span style="font-weight: bold;">ESC</span> — quit.<br />
</li></ul>
<br />
The HUD shows every value live, and the <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE*</code></div></div>
 env vars let you drive it headless for A/B captures:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE<br />
EE_TILE_STOCH<br />
EE_TILE_CAM_*<br />
EE_TILE_FARMAX<br />
EE_TILE_STOCHDIST</code></div></div>
<br />
Since the effect lives in the shader, it shows up on <span style="font-style: italic;">any</span> terrain. The other world tutorials are good places to see it on different content once an app enables the params:<br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_World</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_WorldWithCharacter</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_ProceduralWorld</code></div></div>
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Caveats / future work</span></span><br />
<ul>
<li>The only thing the <span style="font-weight: bold;">full Heitz histogram LUT</span> would add over this is slightly sharper blend zones. It carries a per-texture asset-bake cost, so it is left out. The variance-preserving approximation looks clean in practice.</li>
<li>One residual issue: the triangular-grid <span style="font-weight: bold;">barycentric weights</span> lose a little float precision at <span style="font-style: italic;">extreme</span> UV. The cell hash is exact, so the de-tiling holds; only the blend transitions get marginally coarser very far out.</li>
<li><span style="font-weight: bold;">Backends:</span> Vulkan and GL shader paks are regenerated. <span style="font-weight: bold;">DX still needs a Windows tree</span> to recompile, only tested on Linux+Vulkan.<br />
</li></ul>
<br />
in this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-size: large;"><span style="font-weight: bold;">Terrain Anti-Tiling: distance dual-scale + stochastic “by-example” (no asset pipeline)</span></span><br />
<br />
Before anti-tile:<br />
<a href="https://postimg.cc/fSLfnkSR" target="_blank"><img src="https://i.postimg.cc/br0Cntzk/Screenshot-From-2026-06-18-23-39-08.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-39-08.png]" /></a><br />
<br />
After anti-tile:<br />
<a href="https://postimg.cc/w3TVs4fF" target="_blank"><img src="https://i.postimg.cc/44pFqrzC/Screenshot-From-2026-06-18-23-38-58.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-38-58.png]" /></a><br />
<br />
Works best when looking out over the terrain, looking straight down at terrain it doesn't help as much.<br />
<br />
<br />
<br />
Written by AI:<br />
<br />
Repeating ground textures betray themselves two different ways, and they need two different fixes:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">The far-field grid</span> — from a hill or a grazing camera, the regular <span style="font-style: italic;">periodicity</span> of the texture reads as a checkerboard receding to the horizon.</li>
<li><span style="font-weight: bold;">The close/mid feature repeat</span> — that one distinctive grass clump / rock / crack showing up again every few meters right in front of you.<br />
</li></ol>
<br />
Two complementary techniques in the deferred terrain shader (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Engine/Source/Shaders/Deferred.cpp</code></div></div>
) address each. Both run on the <span style="font-weight: bold;">single- and multi-material</span> terrain paths, up to 5 blended materials, and both are <span style="font-weight: bold;">gated to terrain only</span> (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#if HEIGHTMAP &amp;&amp; SET_POS</code></div></div>
), so buildings, props, and characters are completely untouched.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">1. Distance-based dual-scale: kills the far grid</span></span><br />
<br />
Sample the same ground texture at <span style="font-weight: bold;">two UV scales</span> and blend in the lower-frequency, larger-tile one with camera distance:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>tex = lerp(near, RTex(Col, uv * TileFarScale), fade(distance));</code></div></div>
<br />
Near the camera you get full detail. Far away, the tight repeat dissolves into a coarser, non-aligned pattern. This is the standard Unreal-landscape-style trick, and it is <span style="font-weight: bold;">mip-safe for free</span>, because the two samples use implicit derivatives, so no <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 is needed.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">2. Stochastic “by-example”: kills the near/mid feature repeat</span></span><br />
<br />
This is <span style="font-weight: bold;">Heitz–Neyret’s by-example noise, minus the histogram LUT</span>, which means it needs <span style="font-weight: bold;">no precomputed assets</span> and no per-texture bake.<br />
<ul>
<li><span style="font-weight: bold;">Triangular / “hex” grid</span> — 3 samples, no axis-aligned blend seams.</li>
<li><span style="font-weight: bold;">Per-cell random offset only, no mirror</span> — the mirror in the cheaper grid variants is what causes those directional scratches/scuffs on grass. Dropping it removes them.</li>
<li><span style="font-weight: bold;">Variance-preserving blend</span> — naive averaging washes out contrast, so instead of <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>sum(w_i * c_i)</code></div></div>
, we blend the deviation from the mean and renormalize by <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>1 / sqrt(sum(w_i^2))</code></div></div>
. The mean is read from the texture’s <span style="font-weight: bold;">smallest mip</span>, approximately its average color, so we recover contrast with zero precomputation.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 keeps mipmapping correct.<br />
</li></ul>
<br />
The result: a single tiling texture stops repeating and reads as a natural, non-periodic surface — no scratches, no muddy blend blobs.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">How to use it</span></span><br />
<br />
Everything is driven by a single <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTile</code></div></div>
 shader cbuffer, set from the public shader-param API:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet("TerrainTileEnable", 1.0f);&nbsp;&nbsp;// distance effect<br />
SPSet("TileFarScale", 0.22f);<br />
SPSet("TileFadeStart", 5.0f);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ...TileFadeRange, TileFarMax<br />
<br />
SPSet("TerrainStochastic", 1.0f);&nbsp;&nbsp;// stochastic de-tiling</code></div></div>
<br />
<span style="font-weight: bold;">It is off by default.</span> The cbuffer is zero-initialized, so nothing changes unless your app sets the params. The editor and all other content render exactly as before.<br />
<br />
The params only exist once the terrain shader has loaded, so set them <span style="font-weight: bold;">each frame before drawing the world</span>. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet</code></div></div>
 safely no-ops until then. There is no engine-wide default yet; you opt in from your render loop.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Parameters</span></span><br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTileEnable</code></div></div>
 — 0/1, distance dual-scale on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarScale</code></div></div>
 — coarse UV multiplier. Smaller means bigger far tiles and a stronger frequency break.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeStart</code></div></div>
 — meters: distance where the coarse blend begins.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeRange</code></div></div>
 — meters: distance over which it ramps to <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
 — max fraction of the coarse sample blended in at far distance.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainStochastic</code></div></div>
 — 0/1, stochastic de-tiling on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
 — performance limit: only de-tile within this distance. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>&lt;=0</code></div></div>
 means no limit.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
 — performance limit: skip materials whose blend weight is below this.  means de-tile all.<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Performance limits: opt-in, runtime</span></span><br />
<br />
Multi-material stochastic can hit up to <span style="font-weight: bold;">5 materials × 4 taps = 20 albedo taps</span>, so there are two runtime knobs:<br />
<ul>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
</span> — only run the stochastic taps within this distance. The far grid is handled by the distance blend anyway, so far pixels drop straight back to 1 tap.</li>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
</span> — skip stochastic for a material whose per-pixel blend weight is below a threshold, so a barely-visible layer does not cost 4 taps.<br />
</li></ul>
<br />
Both default to “no practical limit.”<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Large-world / floating-origin</span></span><br />
<br />
Terrain UVs are <span style="font-style: italic;">world-continuous</span>, built from the absolute area index, so far out in a big world they grow large.<br />
<br />
A <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Frac(sin(...))</code></div></div>
 hash loses precision there and the de-tiling starts aliasing, so the stochastic cell hash is a <span style="font-weight: bold;">PCG2D integer hash</span> on the integer cell ID, exact at any magnitude.<br />
<br />
The pattern stays locked to the ground and is transparent to floating-origin rebasing. UVs deliberately <span style="font-weight: bold;">do not</span> rebase. Shifting them would move the non-integer-aligned stochastic grid and pop the pattern every rebase.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Tutorials</span></span><br />
<br />
<span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_TerrainTiling</code></div></div>
</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorials/Source/14 - Game Basics/36 - Terrain Tiling.cpp</code></div></div>
<br />
The live A/B demo. It loads a textured world and lets you toggle and tune everything in real time:<br />
<ul>
<li><span style="font-weight: bold;">T</span> — distance anti-tiling on/off.</li>
<li><span style="font-weight: bold;">Y</span> — stochastic de-tiling on/off.</li>
<li><span style="font-weight: bold;">1–8</span> — distance params: strength, fade start, fade range, coarse scale.</li>
<li><span style="font-weight: bold;">G / H</span> — stochastic max distance, for performance.</li>
<li><span style="font-weight: bold;">J / K</span> — stochastic min weight, for performance.</li>
<li><span style="font-weight: bold;">9</span> — reset all to defaults.</li>
<li>Hold <span style="font-weight: bold;">RMB</span> — look.</li>
<li><span style="font-weight: bold;">Mouse wheel</span> — zoom.</li>
<li><span style="font-weight: bold;">ESC</span> — quit.<br />
</li></ul>
<br />
The HUD shows every value live, and the <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE*</code></div></div>
 env vars let you drive it headless for A/B captures:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE<br />
EE_TILE_STOCH<br />
EE_TILE_CAM_*<br />
EE_TILE_FARMAX<br />
EE_TILE_STOCHDIST</code></div></div>
<br />
Since the effect lives in the shader, it shows up on <span style="font-style: italic;">any</span> terrain. The other world tutorials are good places to see it on different content once an app enables the params:<br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_World</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_WorldWithCharacter</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_ProceduralWorld</code></div></div>
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Caveats / future work</span></span><br />
<ul>
<li>The only thing the <span style="font-weight: bold;">full Heitz histogram LUT</span> would add over this is slightly sharper blend zones. It carries a per-texture asset-bake cost, so it is left out. The variance-preserving approximation looks clean in practice.</li>
<li>One residual issue: the triangular-grid <span style="font-weight: bold;">barycentric weights</span> lose a little float precision at <span style="font-style: italic;">extreme</span> UV. The cell hash is exact, so the de-tiling holds; only the blend transitions get marginally coarser very far out.</li>
<li><span style="font-weight: bold;">Backends:</span> Vulkan and GL shader paks are regenerated. <span style="font-weight: bold;">DX still needs a Windows tree</span> to recompile, only tested on Linux+Vulkan.<br />
</li></ul>
<br />
in this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[[Bug] TitanEditor EditMaterial::fixOldReflect makes old materials metallic → dark]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11825</link>
			<pubDate>Fri, 19 Jun 2026 02:33:33 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11825</guid>
			<description><![CDATA[I noticed when I would change any parameter of the "Path" material in the Tutorials in Titan Editor the material would immediately become darker.<br />
<br />
AI solved it, I'm not sure if this is because of some other change I made in the engine or not but here was the solution..<br />
<br />
[Bug] TitanEditor EditMaterial::fixOldReflect makes old materials metallic → renders dark (esp. on terrain)<br />
<br />
Summary<br />
<br />
When an old material (one saved with a single reflect value, before the reflect_min/reflect_max model) is loaded into TitanEditor, EditMaterial::fixOldReflect() can wrongly set reflect_max = 1, turning a dielectric material into a metallic one. On terrain (multi-material rendering) this makes the material render much darker than its texture, because the high reflectivity reads the packed metal channel and metals have ~no diffuse albedo.<br />
<br />
Affected code<br />
<br />
Editor/.../Shared/Elements/Material.cpp — EditMaterial::fixOldReflect(flt reflect):<br />
<br />
void EditMaterial::fixOldReflect(flt reflect)<br />
{<br />
   if(metal_map.is())<br />
   {<br />
      reflect_min=MATERIAL_REFLECT;<br />
      reflect_max=reflect;<br />
   }else<br />
   {<br />
      reflect_min=reflect;<br />
      reflect_max=1;      // &lt;-- bug<br />
   }<br />
}<br />
<br />
Introduced in commit 60929bf7708b2ff18e92032411928947bf4b74ca (2022-02-21), when the reflect_min/reflect_max model was added — so it's long-standing, not platform-specific.<br />
<br />
Root cause<br />
<br />
An old single-reflect material had constant reflectivity (it predates per-texture metalness). The no-metal-map branch sets reflect_max = 1, and on publish Material::reflect(min, max) computes:<br />
<br />
reflect_add = min;  reflect_mul = (1 - min) * max;<br />
<br />
So reflect(0.04, 1.0) → reflect_mul ≈ 0.96. The intent appears to be "harmless when there's no metal map," but the multi-material / terrain shader samples the metal channel of the packed base_2 (Ext) texture regardless of whether a separate metal_map was assigned. With reflect_mul ≈ 0.96, reflectivity becomes 0.04 + 0.96 × base_2.metal, i.e. metallic wherever that channel is bright → the surface renders dark. The engine's own old-material Material::loadData conversion, by contrast, uses reflect_mul = 0 (pure dielectric), so the editor was inconsistent with the engine.<br />
<br />
Observed effects / how it presents (very confusing!)<br />
<br />
- A material's tile preview looks correct/light (single-material path doesn't sample that metal channel), which masks the problem.<br />
- Painted onto terrain it renders dark, and it's per-material (paint the dark material anywhere → dark; the light one → light).<br />
- A raw copy of an old material stays correct (still the old single-reflect format, dielectric). Editing it (tweaking any slider) republishes it through fixOldReflect → it becomes metallic → goes dark and stays dark, even though the color (1,1,1,1) and all visible sliders look normal. The only tell is ReflectivityMax = 1.000 in the material editor.<br />
<br />
Reproduction<br />
<br />
1. Take an old-format material that has packed base textures and no separate metal_map, with low/constant reflectivity.<br />
2. Assign it to terrain — looks fine.<br />
3. Edit any slider (forces a republish through fixOldReflect).<br />
4. It renders noticeably darker on terrain; the tile preview still looks light.<br />
<br />
Fix<br />
<br />
Make the no-metal-map branch dielectric (matching the engine and how a normal dielectric material loads, where reflectMax() == 0):<br />
<br />
   }else<br />
   {<br />
      reflect_min=reflect;<br />
      reflect_max=0;   // dielectric: old single-'reflect' materials have constant reflectivity; 0 =&gt; reflect_mul=0 (matches Material::loadData)<br />
   }]]></description>
			<content:encoded><![CDATA[I noticed when I would change any parameter of the "Path" material in the Tutorials in Titan Editor the material would immediately become darker.<br />
<br />
AI solved it, I'm not sure if this is because of some other change I made in the engine or not but here was the solution..<br />
<br />
[Bug] TitanEditor EditMaterial::fixOldReflect makes old materials metallic → renders dark (esp. on terrain)<br />
<br />
Summary<br />
<br />
When an old material (one saved with a single reflect value, before the reflect_min/reflect_max model) is loaded into TitanEditor, EditMaterial::fixOldReflect() can wrongly set reflect_max = 1, turning a dielectric material into a metallic one. On terrain (multi-material rendering) this makes the material render much darker than its texture, because the high reflectivity reads the packed metal channel and metals have ~no diffuse albedo.<br />
<br />
Affected code<br />
<br />
Editor/.../Shared/Elements/Material.cpp — EditMaterial::fixOldReflect(flt reflect):<br />
<br />
void EditMaterial::fixOldReflect(flt reflect)<br />
{<br />
   if(metal_map.is())<br />
   {<br />
      reflect_min=MATERIAL_REFLECT;<br />
      reflect_max=reflect;<br />
   }else<br />
   {<br />
      reflect_min=reflect;<br />
      reflect_max=1;      // &lt;-- bug<br />
   }<br />
}<br />
<br />
Introduced in commit 60929bf7708b2ff18e92032411928947bf4b74ca (2022-02-21), when the reflect_min/reflect_max model was added — so it's long-standing, not platform-specific.<br />
<br />
Root cause<br />
<br />
An old single-reflect material had constant reflectivity (it predates per-texture metalness). The no-metal-map branch sets reflect_max = 1, and on publish Material::reflect(min, max) computes:<br />
<br />
reflect_add = min;  reflect_mul = (1 - min) * max;<br />
<br />
So reflect(0.04, 1.0) → reflect_mul ≈ 0.96. The intent appears to be "harmless when there's no metal map," but the multi-material / terrain shader samples the metal channel of the packed base_2 (Ext) texture regardless of whether a separate metal_map was assigned. With reflect_mul ≈ 0.96, reflectivity becomes 0.04 + 0.96 × base_2.metal, i.e. metallic wherever that channel is bright → the surface renders dark. The engine's own old-material Material::loadData conversion, by contrast, uses reflect_mul = 0 (pure dielectric), so the editor was inconsistent with the engine.<br />
<br />
Observed effects / how it presents (very confusing!)<br />
<br />
- A material's tile preview looks correct/light (single-material path doesn't sample that metal channel), which masks the problem.<br />
- Painted onto terrain it renders dark, and it's per-material (paint the dark material anywhere → dark; the light one → light).<br />
- A raw copy of an old material stays correct (still the old single-reflect format, dielectric). Editing it (tweaking any slider) republishes it through fixOldReflect → it becomes metallic → goes dark and stays dark, even though the color (1,1,1,1) and all visible sliders look normal. The only tell is ReflectivityMax = 1.000 in the material editor.<br />
<br />
Reproduction<br />
<br />
1. Take an old-format material that has packed base textures and no separate metal_map, with low/constant reflectivity.<br />
2. Assign it to terrain — looks fine.<br />
3. Edit any slider (forces a republish through fixOldReflect).<br />
4. It renders noticeably darker on terrain; the tile preview still looks light.<br />
<br />
Fix<br />
<br />
Make the no-metal-map branch dielectric (matching the engine and how a normal dielectric material loads, where reflectMax() == 0):<br />
<br />
   }else<br />
   {<br />
      reflect_min=reflect;<br />
      reflect_max=0;   // dielectric: old single-'reflect' materials have constant reflectivity; 0 =&gt; reflect_mul=0 (matches Material::loadData)<br />
   }]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Shader Cache, Azgaar Map Import,  Large Worlds Editor, Procedural Terrain, Portals]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11824</link>
			<pubDate>Sat, 13 Jun 2026 03:39:07 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11824</guid>
			<description><![CDATA[<span style="font-style: italic;">Please note I have only been testing Linux Vulkan RADV in my repo, not sure if any of this runs on other platforms/renderers/drivers.</span><br />
<br />
Added a large set of features focused on large-world support, procedural terrain, and streaming.<br />
<br />
Titan Editor can now import and edit blended worlds that combine procedural terrain with hand-authored tiled areas. A 250 km x 250 km procedural world is under 100 MB on disk, compared to over 3 TB for an equivalent fully tiled world at the same resolution.<br />
<br />
The engine now supports 64-bit large worlds through rebasing and tile-relative storage. Object rendering and PhysX simulation remain accurate and stable at world coordinates like 250,000,250,000 , just as they are near 0,0.<br />
<br />
Azgaar (<a href="https://azgaar.github.io/Fantasy-Map-Generator/" target="_blank">https://azgaar.github.io/Fantasy-Map-Generator/</a>) procedural map import now supports biome data, lake generation, river carving, and water mesh generation. Runtime procedural terrain also supports ring-LOD generation so distant terrain can stream and render efficiently.<br />
<br />
Titan Editor fully supports 64-bit worlds by rebasing the editor view and storing all relevant world data relative to tiles.<br />
<br />
Portal support was also added. Portals are placed through waypoints in Titan Editor and allow real-time movement to disconnected areas of the large world without a loading screen. As the player approaches a portal, the engine pre-streams tiled areas and generates procedural areas on the destination side so the transition is ready before crossing.<br />
<br />
<br />
<br />
AI Generated repo review of engine changes:<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">1. Linux-native shader toolchain + EERegen</span></span><br />
<ul>
<li>Vendored DXC v1.9.2602 on Linux, with SPIRV-Cross built from source. Vulkan and GL shader paks can now be regenerated natively on Linux without a Windows machine.</li>
<li><span style="font-weight: bold;">EERegen</span>: a single headless tool that performs shader compilation and Engine.pak / Editor.pak rebundling in one process. This replaces the old two-tool chain of shader-compile build + Engine Builder GUI.</li>
<li>Added a content-hash shader variant cache. Warm regen time dropped from roughly <span style="font-weight: bold;">40 minutes to about 30 seconds</span>. Editing one HLSL file now rebuilds only its affected variants.</li>
<li>Fixed a DXC RelaxedPrecision / OpPhi issue that was causing <span style="font-weight: bold;">544 compile failures</span> in Motion Blur and Temporal shaders.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">2. Compiler bug fixed: -fshort-wchar removed</span></span><br />
<br />
Clang's loop-idiom recognizer was rewriting the engine's string-length loop into a glibc wcslen call built for 32-bit wchar_t. This caused path truncation and memory over-reads at -O2.<br />
<br />
The -fshort-wchar flag was removed, which fixed the issue.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">3. Vulkan backend fixes and improvements</span></span><br />
<br />
Fixed <span style="font-weight: bold;">15 tracked Vulkan issues</span>, including:<br />
<ul>
<li>Device loss from cube-texture views bound to 2D descriptor slots.</li>
<li>Crashes from in-flight buffer/image destruction, fixed with a new deferred-destruction system.</li>
<li>Scene corruption under heavy draw load caused by per-draw UBO ring wrapping. This was replaced with a byte-budget ring.</li>
<li>Eye-adaptation black frame caused by cross-stage image slot collisions.</li>
<li>Per-draw descriptor allocation was O(draws). Switched to dynamic UBO offsets.</li>
<li>Terrain “sparkle” caused by the BRDF LUT being deleted on every render-target reset.</li>
<li>Added per-pipeline descriptor sets.</li>
<li>Added a disk-persistent pipeline cache.</li>
<li>Reduced sync-validation errors from <span style="font-weight: bold;">10 to 0</span>.</li>
<li>Added env-gated abort-on-error debugging tools.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">4. Upstream Esenthel sync</span></span><br />
<br />
Merged upstream main back in, regenerated affected shader paks, and fixed a .gitattributes issue that was causing bogus whole-file merge conflicts.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">5. Large World: 64-bit floating origin</span></span><br />
<ul>
<li>Added runtime floating-origin support in Game::World.</li>
<li>Added camera rebase support.</li>
<li>Added tile-relative area storage.</li>
<li>Added water, decal, and navmesh support for large worlds.</li>
<li>Added local-frame pathfinding wrappers.</li>
<li>Added editor support for large worlds, including jitter-free terrain editing and object placement far from origin.</li>
<li>Updated undo/redo so it survives rebasing.</li>
<li>Added world-coordinate display panels.</li>
<li>Added tile-relative game export.</li>
<li>Added an area relocate/convert tool for existing worlds.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">6. Procedural terrain + Azgaar world import</span></span><br />
<ul>
<li>Titan Editor supports Import of Azgaar Maps and seamless blending of Procedural+Tiled areas</li>
<li>Added runtime streamed procedural terrain generation.</li>
<li>Generation is multithreaded and prioritizes areas nearest the camera.</li>
<li>Added a far-LOD geo-clipmap backdrop ring so terrain reaches the horizon.</li>
<li>Added climate-driven biomes with per-biome terrain shapes: flat, rolling, rugged, and dunes.</li>
<li>Added three blended ground materials to reduce visible tiling.</li>
<li>Added terrain multi-texturing by slope and altitude.</li>
<li>Added object scatter for trees and rocks, including static collision.</li>
<li>Added sea level and ocean support.</li>
<li>Added rivers carved from Azgaar map data with mitred winding channels.</li>
<li>Added above-sea-level lake basins.</li>
<li>Water and terrain now render at full detail to the horizon.</li>
<li>Added Azgaar Fantasy Map Generator import. Heightmap and biome control maps drive the generated world.</li>
<li>Added an in-game minimap using M, with player position, facing arrow, and click-to-teleport.</li>
<li>Added editor tools: Procedural Terrain dialog, Biome Editor, and Rivers &amp; Lakes dialog.</li>
<li>Procedural config is included with the exported world.</li>
<li>Added per-area navmesh tiles for generated terrain.</li>
<li>Added a tutorial: <span style="font-weight: bold;">“Procedural World”</span>. 250 km x 250 km Azgaar Map Demo.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">7. Portals</span></span><br />
<ul>
<li>Added portal authoring in the editor.</li>
<li>Added runtime portal crossing.</li>
<li>Added a secondary-streaming-foci API so the destination side can pre-stream before the player crosses.</li>
<li>Added tutorial <span style="font-weight: bold;">“21 - Portals”</span> with a sample world.</li>
<li>Portal crossing is seamless, with no flash.</li>
<li>The streaming ring on the departed side is preserved during crossing.</li>
<li>Added a streaming-status HUD.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Honorable mentions</span></span><br />
<ul>
<li>RmlUi improvements: icon: texture loading from the engine image cache, plus same-texture draw batching.</li>
<li>Fixed stale input-action state on popped or masked input maps.</li>
<li>Added memory hardening from ASAN runs, including out-of-bounds guards and completion of the global new/delete family.</li>
<li>Made sizeof(Image) renderer-independent.<br />
</li></ul>
<br />
Source is in this fork: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
<a href="https://postimg.cc/QHx7vK6v" target="_blank"><img src="https://i.postimg.cc/wTDkyhDx/Screenshot-From-2026-06-12-21-24-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpW" target="_blank"><img src="https://i.postimg.cc/XY3kwpg5/Screenshot-From-2026-06-12-21-24-39.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-39.png]" /></a><br />
<br />
<a href="https://postimg.cc/w7fNzxZ9" target="_blank"><img src="https://i.postimg.cc/cLZBQrct/Screenshot-From-2026-06-12-21-25-50.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-25-50.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpR" target="_blank"><img src="https://i.postimg.cc/9QVYGrBZ/Screenshot-From-2026-06-12-21-27-49.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-27-49.png]" /></a><br />
<br />
<a href="https://postimg.cc/PP3DHf9r" target="_blank"><img src="https://i.postimg.cc/6pwfdyhR/Screenshot-From-2026-06-12-21-28-00.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-28-00.png]" /></a><br />
<br />
<a href="https://postimg.cc/sv6WzfqW" target="_blank"><img src="https://i.postimg.cc/13PrpfKv/Screenshot-From-2026-06-12-23-08-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fCFSR2w" target="_blank"><img src="https://i.postimg.cc/YCkz1hxn/Screenshot-From-2026-06-12-23-08-23.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-23.png]" /></a><br />
<br />
<a href="https://postimg.cc/qzbKrBSG" target="_blank"><img src="https://i.postimg.cc/8zDmhjbx/Screenshot-From-2026-06-12-23-09-07.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-09-07.png]" /></a>]]></description>
			<content:encoded><![CDATA[<span style="font-style: italic;">Please note I have only been testing Linux Vulkan RADV in my repo, not sure if any of this runs on other platforms/renderers/drivers.</span><br />
<br />
Added a large set of features focused on large-world support, procedural terrain, and streaming.<br />
<br />
Titan Editor can now import and edit blended worlds that combine procedural terrain with hand-authored tiled areas. A 250 km x 250 km procedural world is under 100 MB on disk, compared to over 3 TB for an equivalent fully tiled world at the same resolution.<br />
<br />
The engine now supports 64-bit large worlds through rebasing and tile-relative storage. Object rendering and PhysX simulation remain accurate and stable at world coordinates like 250,000,250,000 , just as they are near 0,0.<br />
<br />
Azgaar (<a href="https://azgaar.github.io/Fantasy-Map-Generator/" target="_blank">https://azgaar.github.io/Fantasy-Map-Generator/</a>) procedural map import now supports biome data, lake generation, river carving, and water mesh generation. Runtime procedural terrain also supports ring-LOD generation so distant terrain can stream and render efficiently.<br />
<br />
Titan Editor fully supports 64-bit worlds by rebasing the editor view and storing all relevant world data relative to tiles.<br />
<br />
Portal support was also added. Portals are placed through waypoints in Titan Editor and allow real-time movement to disconnected areas of the large world without a loading screen. As the player approaches a portal, the engine pre-streams tiled areas and generates procedural areas on the destination side so the transition is ready before crossing.<br />
<br />
<br />
<br />
AI Generated repo review of engine changes:<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">1. Linux-native shader toolchain + EERegen</span></span><br />
<ul>
<li>Vendored DXC v1.9.2602 on Linux, with SPIRV-Cross built from source. Vulkan and GL shader paks can now be regenerated natively on Linux without a Windows machine.</li>
<li><span style="font-weight: bold;">EERegen</span>: a single headless tool that performs shader compilation and Engine.pak / Editor.pak rebundling in one process. This replaces the old two-tool chain of shader-compile build + Engine Builder GUI.</li>
<li>Added a content-hash shader variant cache. Warm regen time dropped from roughly <span style="font-weight: bold;">40 minutes to about 30 seconds</span>. Editing one HLSL file now rebuilds only its affected variants.</li>
<li>Fixed a DXC RelaxedPrecision / OpPhi issue that was causing <span style="font-weight: bold;">544 compile failures</span> in Motion Blur and Temporal shaders.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">2. Compiler bug fixed: -fshort-wchar removed</span></span><br />
<br />
Clang's loop-idiom recognizer was rewriting the engine's string-length loop into a glibc wcslen call built for 32-bit wchar_t. This caused path truncation and memory over-reads at -O2.<br />
<br />
The -fshort-wchar flag was removed, which fixed the issue.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">3. Vulkan backend fixes and improvements</span></span><br />
<br />
Fixed <span style="font-weight: bold;">15 tracked Vulkan issues</span>, including:<br />
<ul>
<li>Device loss from cube-texture views bound to 2D descriptor slots.</li>
<li>Crashes from in-flight buffer/image destruction, fixed with a new deferred-destruction system.</li>
<li>Scene corruption under heavy draw load caused by per-draw UBO ring wrapping. This was replaced with a byte-budget ring.</li>
<li>Eye-adaptation black frame caused by cross-stage image slot collisions.</li>
<li>Per-draw descriptor allocation was O(draws). Switched to dynamic UBO offsets.</li>
<li>Terrain “sparkle” caused by the BRDF LUT being deleted on every render-target reset.</li>
<li>Added per-pipeline descriptor sets.</li>
<li>Added a disk-persistent pipeline cache.</li>
<li>Reduced sync-validation errors from <span style="font-weight: bold;">10 to 0</span>.</li>
<li>Added env-gated abort-on-error debugging tools.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">4. Upstream Esenthel sync</span></span><br />
<br />
Merged upstream main back in, regenerated affected shader paks, and fixed a .gitattributes issue that was causing bogus whole-file merge conflicts.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">5. Large World: 64-bit floating origin</span></span><br />
<ul>
<li>Added runtime floating-origin support in Game::World.</li>
<li>Added camera rebase support.</li>
<li>Added tile-relative area storage.</li>
<li>Added water, decal, and navmesh support for large worlds.</li>
<li>Added local-frame pathfinding wrappers.</li>
<li>Added editor support for large worlds, including jitter-free terrain editing and object placement far from origin.</li>
<li>Updated undo/redo so it survives rebasing.</li>
<li>Added world-coordinate display panels.</li>
<li>Added tile-relative game export.</li>
<li>Added an area relocate/convert tool for existing worlds.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">6. Procedural terrain + Azgaar world import</span></span><br />
<ul>
<li>Titan Editor supports Import of Azgaar Maps and seamless blending of Procedural+Tiled areas</li>
<li>Added runtime streamed procedural terrain generation.</li>
<li>Generation is multithreaded and prioritizes areas nearest the camera.</li>
<li>Added a far-LOD geo-clipmap backdrop ring so terrain reaches the horizon.</li>
<li>Added climate-driven biomes with per-biome terrain shapes: flat, rolling, rugged, and dunes.</li>
<li>Added three blended ground materials to reduce visible tiling.</li>
<li>Added terrain multi-texturing by slope and altitude.</li>
<li>Added object scatter for trees and rocks, including static collision.</li>
<li>Added sea level and ocean support.</li>
<li>Added rivers carved from Azgaar map data with mitred winding channels.</li>
<li>Added above-sea-level lake basins.</li>
<li>Water and terrain now render at full detail to the horizon.</li>
<li>Added Azgaar Fantasy Map Generator import. Heightmap and biome control maps drive the generated world.</li>
<li>Added an in-game minimap using M, with player position, facing arrow, and click-to-teleport.</li>
<li>Added editor tools: Procedural Terrain dialog, Biome Editor, and Rivers &amp; Lakes dialog.</li>
<li>Procedural config is included with the exported world.</li>
<li>Added per-area navmesh tiles for generated terrain.</li>
<li>Added a tutorial: <span style="font-weight: bold;">“Procedural World”</span>. 250 km x 250 km Azgaar Map Demo.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">7. Portals</span></span><br />
<ul>
<li>Added portal authoring in the editor.</li>
<li>Added runtime portal crossing.</li>
<li>Added a secondary-streaming-foci API so the destination side can pre-stream before the player crosses.</li>
<li>Added tutorial <span style="font-weight: bold;">“21 - Portals”</span> with a sample world.</li>
<li>Portal crossing is seamless, with no flash.</li>
<li>The streaming ring on the departed side is preserved during crossing.</li>
<li>Added a streaming-status HUD.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Honorable mentions</span></span><br />
<ul>
<li>RmlUi improvements: icon: texture loading from the engine image cache, plus same-texture draw batching.</li>
<li>Fixed stale input-action state on popped or masked input maps.</li>
<li>Added memory hardening from ASAN runs, including out-of-bounds guards and completion of the global new/delete family.</li>
<li>Made sizeof(Image) renderer-independent.<br />
</li></ul>
<br />
Source is in this fork: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
<a href="https://postimg.cc/QHx7vK6v" target="_blank"><img src="https://i.postimg.cc/wTDkyhDx/Screenshot-From-2026-06-12-21-24-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpW" target="_blank"><img src="https://i.postimg.cc/XY3kwpg5/Screenshot-From-2026-06-12-21-24-39.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-39.png]" /></a><br />
<br />
<a href="https://postimg.cc/w7fNzxZ9" target="_blank"><img src="https://i.postimg.cc/cLZBQrct/Screenshot-From-2026-06-12-21-25-50.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-25-50.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpR" target="_blank"><img src="https://i.postimg.cc/9QVYGrBZ/Screenshot-From-2026-06-12-21-27-49.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-27-49.png]" /></a><br />
<br />
<a href="https://postimg.cc/PP3DHf9r" target="_blank"><img src="https://i.postimg.cc/6pwfdyhR/Screenshot-From-2026-06-12-21-28-00.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-28-00.png]" /></a><br />
<br />
<a href="https://postimg.cc/sv6WzfqW" target="_blank"><img src="https://i.postimg.cc/13PrpfKv/Screenshot-From-2026-06-12-23-08-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fCFSR2w" target="_blank"><img src="https://i.postimg.cc/YCkz1hxn/Screenshot-From-2026-06-12-23-08-23.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-23.png]" /></a><br />
<br />
<a href="https://postimg.cc/qzbKrBSG" target="_blank"><img src="https://i.postimg.cc/8zDmhjbx/Screenshot-From-2026-06-12-23-09-07.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-09-07.png]" /></a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[June 2026]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11823</link>
			<pubDate>Tue, 02 Jun 2026 06:01:04 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11823</guid>
			<description><![CDATA[Updated Esenthel Source:<br />
-updated Sound system to operate on VecD positions (64-bit float for high precision huge worlds)]]></description>
			<content:encoded><![CDATA[Updated Esenthel Source:<br />
-updated Sound system to operate on VecD positions (64-bit float for high precision huge worlds)]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[[UI Editor] Feature requests]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11822</link>
			<pubDate>Thu, 28 May 2026 09:41:25 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11822</guid>
			<description><![CDATA[Hi,<br />
<br />
I'd like to propose two additions to the GUI editor window:<br />
<br />
<span style="font-weight: bold;">1. Multi-Selection Editing</span><br />
When multiple UI elements are selected, params that are unique per element (Name, Description, and Text) should be greyed out and non-editable. Params that are common across the selection should remain editable. Where selected elements have differing values for a shared param, display "–" as a placeholder to indicate the values are not uniform amongst the selected elements<br />
<br />
This would significantly reduce friction when working on complex interfaces. Currently, when adjusting shared properties like scale or size requires selecting all elements, applying the change, deselecting, then selecting an individual element to verify if the param is correct, and repeating. Multi-selection editing would eliminate that process.<br />
<br />
<span style="font-weight: bold;">2. Configurable Insert Templates for UI Elements</span><br />
Add a right-click context menu to entries in the standard UI element list. This menu would open a template configuration window where the default parameters for that element type can be set (width, height, text alignment, text size, and skin override).<br />
<br />
Buttons are currently inserted with a default width of 0.270, height of 0.060, text alignment 0, text size 1, and an empty skin override. These defaults probably won't match the design of a custom button, meaning every newly placed button requires manual adjustment (although the skin could be configured in an application element). With configurable templates, a developer could define defaults that match their project's design once, rather than correcting values on each new element placed in different GUI's.<br />
<br />
Thanks for considering these! Would love to hear feedback!]]></description>
			<content:encoded><![CDATA[Hi,<br />
<br />
I'd like to propose two additions to the GUI editor window:<br />
<br />
<span style="font-weight: bold;">1. Multi-Selection Editing</span><br />
When multiple UI elements are selected, params that are unique per element (Name, Description, and Text) should be greyed out and non-editable. Params that are common across the selection should remain editable. Where selected elements have differing values for a shared param, display "–" as a placeholder to indicate the values are not uniform amongst the selected elements<br />
<br />
This would significantly reduce friction when working on complex interfaces. Currently, when adjusting shared properties like scale or size requires selecting all elements, applying the change, deselecting, then selecting an individual element to verify if the param is correct, and repeating. Multi-selection editing would eliminate that process.<br />
<br />
<span style="font-weight: bold;">2. Configurable Insert Templates for UI Elements</span><br />
Add a right-click context menu to entries in the standard UI element list. This menu would open a template configuration window where the default parameters for that element type can be set (width, height, text alignment, text size, and skin override).<br />
<br />
Buttons are currently inserted with a default width of 0.270, height of 0.060, text alignment 0, text size 1, and an empty skin override. These defaults probably won't match the design of a custom button, meaning every newly placed button requires manual adjustment (although the skin could be configured in an application element). With configurable templates, a developer could define defaults that match their project's design once, rather than correcting values on each new element placed in different GUI's.<br />
<br />
Thanks for considering these! Would love to hear feedback!]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[May 2026]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11819</link>
			<pubDate>Tue, 05 May 2026 05:03:20 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11819</guid>
			<description><![CDATA[Updated Esenthel Source:<br />
-fixed physics tutorials not compiling due to updated Box constructor<br />
-added more Edge2 functions<br />
-"Behind" shader now correctly processes "Blend" Materials]]></description>
			<content:encoded><![CDATA[Updated Esenthel Source:<br />
-fixed physics tutorials not compiling due to updated Box constructor<br />
-added more Edge2 functions<br />
-"Behind" shader now correctly processes "Blend" Materials]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Lag compensation]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11816</link>
			<pubDate>Wed, 22 Apr 2026 01:28:27 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11816</guid>
			<description><![CDATA[I had Claude Code improve and bring into the engine the lag compensation I use in my project, it makes FPS and Chivalry/Mount and Blade weapon trail swing games playable and fair even with some clients having 200+ ms pings (overseas players). I use ENET in my case but I kept it transport layer agnostic here.<br />
<br />
<span style="font-weight: bold;">Networking: Lag Compensation + Physics Rollback</span><br />
<span style="font-style: italic;">Transport-agnostic server-authoritative simulation layer (lag-compensation branch)</span><br />
<br />
A commercial-grade networking substrate built from scratch under *Engine/Net/*. Two independent tiers of temporal correction, built using shipped-game references:<br />
<ul>
<li><span style="font-weight: bold;">Tier 1 — selective hitbox rewind</span> (Photon Fusion 2 / Source / CS:GO model). Each server tick, every replicated actor contributes a flat list of hitbox samples into a 256-tick ring buffer (~4.27 s @ 60 Hz). Spatial queries take a *(tick, frac)* pair, linearly interpolate the two bracketing snapshots, and test against the interpolated hitboxes. The live gameplay scene is never touched — perfect for hitscan / melee where you only need “where was this limb when the client fired”.</li>
<li><span style="font-weight: bold;">Tier 2 — full PhysX rewind + resimulation</span> (UE5 networked-physics model). Snapshots pose / linear + angular velocity / kinematic / sleeping flags for every tracked *Actor* each tick; on demand restores them and drives *Physics.stepOnce(dt)* forward to catch up to the current tick. A *RollbackGuard* (TLS depth counter) is held active for the resim window so gameplay / animation / audio code that checks *RollbackGuard::active()* can suppress side-effects (event fire, notifies, SFX) during replay.<br />
</li></ul>
<br />
All modules are <span style="font-weight: bold;">unconditionally compiled</span> — no CMake flag, no opt-out, no extra link cost (the dependency surface is zero — pure C++, no third-party libraries). The transport layer is <span style="font-weight: bold;">abstract</span>: the engine ships *LoopbackTransport* (in-process queue) and *FakeLatencyTransport* (wraps any transport with configurable latency / jitter / loss), plus sketches for *ENetTransport* and a multi-hop *ForwardingTransport* in *Engine/H/Net/NetTransport.h* header comments. User projects subclass *INetTransport* and forward to ENet / Steam Sockets / WebRTC / whatever fits their deployment.<br />
<br />
<span style="font-weight: bold;">Module layout:</span><br />
<ul>
<li>*Net/NetTick.h/cpp* — *TickSeq* (*ULong*) + *NetTick{seq, frac}* shared tick + sub-tick type</li>
<li>*Net/NetClock.h/cpp* — NTP-style clock sync; rolling 5-sample median-of-offset + RTT + jitter; monotonic server timeline</li>
<li>*Net/NetTransport.h/cpp* — *INetTransport* abstract + *LoopbackTransport* + *FakeLatencyTransport* + ENet / multi-hop sketches</li>
<li>*Net/NetObject.h/cpp* — *INetReplicated* interface, *NetBase* convenience, *NET_REGISTER_CLASS* factory, *HitboxSample*, *NetWriter* / *NetReader* byte-buffer codecs with opt-in *putUIntPacked* (varint) / *putPosQ16* (quantized position, 6 B vs 12 B) compression helpers</li>
<li>*Net/InputCommand.h/cpp* — *InputCommand* POD + *InputBuffer* (client redundancy — re-sends last N unacked cmds) + *InputQueue* (server per-connection, wrap-safe *UShort* seq compare)</li>
<li>*Net/NetReplicator.h/cpp* — spawn / despawn / snapshot build + apply / input routing / ping-pong / per-connection relevance callback hook</li>
<li>*Net/LagCompensation.h/cpp* — <span style="font-weight: bold;">Tier 1</span> 256-tick hitbox ring; *ray* / *sweep* / *sweepArc* / *overlap* / *fetchInterp* CPU-analytic queries (sphere + capsule + box) with *(tick, frac)* OR *(real_time)* addressing for clustered servers</li>
<li>*Net/RollbackGuard.h/cpp* — thread-local depth counter; *RollbackGuard::active()* callable from gameplay / anim code</li>
<li>*Net/NetRollback.h/cpp* — <span style="font-weight: bold;">Tier 2</span> snapshot / restore *Actor* state, *Physics.stepOnce*-driven resim, 8-tick default cap (user-configurable), *InputReplayFn* hook, *AnimCaptureFn* / *AnimRestoreFn* hooks for bone-sourced hitboxes<br />
</li></ul>
<br />
~3,200 lines of new engine code across 18 files, zero third-party links, zero *#ifdef*s outside platform branches.<br />
<br />
<span style="font-weight: bold;">Quick start (single-process, both sides in one *Update()* — this is how every category-20 tutorial is structured):</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>// --- Replicated actor ---<br />
struct Target : NetBase<br />
{<br />
Vec pos;<br />
NetClassID netClassId()C override { return 0x54475431u; /* 'TGT1' */ }<br />
void netSerialize&nbsp;&nbsp;(NetWriter &amp;w, UInt fields)C override { w.putVec(pos); }<br />
void netDeserialize(NetReader &amp;r, UInt fields)&nbsp;&nbsp;override { pos = r.getVec(); }<br />
void netHitboxes(Memc&lt;HitboxSample&gt; &amp;out)C override<br />
{<br />
HitboxSample &amp;s = out.New();<br />
s.shape = HITBOX_SPHERE;<br />
s.group = 0x0001;<br />
s.half_ext.set(0.3f, 0.3f, 0.3f);<br />
s.xform.identity(); s.xform.pos = pos;<br />
}<br />
};<br />
NET_REGISTER_CLASS(Target, 0x54475431u);<br />
<br />
// --- Server ---<br />
NetReplicator SvrRep;&nbsp;&nbsp;NetClock SvrClk;&nbsp;&nbsp;LagComp SvrLag;<br />
SvrClk.initServer(60);<br />
SvrRep.serverInit(&amp;transport, &amp;SvrClk);<br />
<br />
each_tick:<br />
SvrRep.serverDrainInputs();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // apply client inputs via INetReplicated::netApplyInput<br />
// ...gameplay simulation...<br />
SvrLag.recordTick(SvrRep, tick, Time.curTime());<br />
if(tick % 3 == 0) SvrRep.sendSnapshots(tick);&nbsp;&nbsp; // 20 Hz @ 60 tick<br />
<br />
on_client_fire(from, to, client_tick):<br />
LagHit h;<br />
if(SvrLag.ray(client_tick, 0.0f, from, to, h))<br />
apply_damage(h.owner, h.owner_idx);<br />
<br />
// --- Client ---<br />
NetReplicator CliRep;&nbsp;&nbsp;NetClock CliClk;&nbsp;&nbsp;InputBuffer CliInputs;<br />
CliClk.initClient(60);<br />
CliRep.clientInit(&amp;transport, &amp;CliClk, /*server_conn*/1);<br />
<br />
each_frame:<br />
CliRep.sendPingMaybe();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // periodic NTP sync<br />
CliRep.update(Time.curTime());&nbsp;&nbsp;&nbsp;&nbsp;// drains spawn/despawn/snapshot/pong<br />
Vec input = sample_keyboard();<br />
pawn-&gt;predict(input);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // local prediction on owned pawn<br />
CliInputs.push(input, buttons, CliClk.tick());<br />
CliInputs.sendTo(CliRep);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // redundant — includes last N unacked<br />
CliInputs.ack(CliRep.lastAckedSeq());<br />
<br />
on_fire(from, to):<br />
// Ship the compensation tick with the fire cmd so the server can rewind<br />
// to exactly what this client was seeing.<br />
fire_cmd.compensation_tick = CliRep.lastSnapshotTick();</code></div></div>
<br />
<span style="font-weight: bold;">Tier 2 (physics rewind) — only when you actually need resim; most games won't:</span><br />
<br />
<span style="font-style: italic;">NetRollback SvrRoll;</span><br />
<span style="font-style: italic;">SvrRoll.init(1.0f/60.0f, /*max_resim=*/8);</span><br />
<span style="font-style: italic;">for(Actor *a : dynamic_physics_actors) SvrRoll.track(*a);</span><br />
<br />
<span style="font-style: italic;">on_physics_step_completed(tick):</span><br />
<span style="font-style: italic;">SvrRoll.recordTick(tick);</span><br />
<br />
<span style="font-style: italic;">on_authoritative_correction(target_tick):</span><br />
<span style="font-style: italic;">SvrRoll.rewindAndResimulate(target_tick, currentTick, replayInputs, user);</span><br />
<br />
<span style="font-weight: bold;">Physics engine touch points</span> — two small, surgical additions; everything else is pure add-on:<br />
<ul>
<li>*Physics.stepOnce(Flt dt)* — direct *simulate* + *fetchResults* with no accumulator, no *simulation_step_completed* callback. This is what drives Tier 2 resim. Safe only between frames (after *stopSimulation()* or inside *WorldManager::physics_update()*).</li>
<li>*Time.rollbackDt(Flt dt)* — thread-local override for *Time.d()*. When *&gt; 0*, *Time.d()* returns it instead of the real frame delta. Set automatically by *NetRollback::rewindAndResimulate* per resim tick to the recorded *dt_used*, so animation code (*Motion::updateAuto*, *AnimatedSkeleton::animate*) replays at the exact dt the forward sim used — deterministic bone poses during rewind.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Animation sync hooks for bone-driven hitboxes</span> — if your game's hitboxes are attached to *AnimatedSkeleton* bones (limbs parented to bone slots), both forward-sim animation and rollback-resim animation must reproduce the same per-tick bone poses. Two engine hooks pair them:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Forward sim</span> — set *Time.rollbackDt(icf.timeDeltaPhysics)* before animating each player tick, so *Motion::updateAuto* uses the physics dt instead of the variable frame dt. Restore with *Time.rollbackDt(0)*.</li>
<li><span style="font-weight: bold;">Rollback resim</span> — pass *NetRollback::setAnimCallbacks(cap, rest, user)*. *AnimCaptureFn* serialises every tracked *Motion* (via *Motion::save* or direct field writes) into a small opaque blob per tick (~64 B per motion × N motions × 256 ticks — fits easily). *AnimRestoreFn* does the inverse <span style="font-weight: bold;">before</span> the user's *InputReplayFn* fires, so replayed animation code starts from the correct *Motion.time* / *Motion.blend*. Pair this with *if(RollbackGuard::active()) return;* at the top of anim-event consumers (weapon clash SFX, hit sparks) so they don't re-fire during replay.<br />
</li></ol>
<br />
<br />
Includes 7 more tutorials.<br />
<a href="https://postimg.cc/hhh408RZ" target="_blank"><img src="https://i.postimg.cc/gcHwm4Sp/Screenshot-from-2026-04-21-21-12-54.png" border="0" alt="[Image: Screenshot-from-2026-04-21-21-12-54.png]" /></a><br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[I had Claude Code improve and bring into the engine the lag compensation I use in my project, it makes FPS and Chivalry/Mount and Blade weapon trail swing games playable and fair even with some clients having 200+ ms pings (overseas players). I use ENET in my case but I kept it transport layer agnostic here.<br />
<br />
<span style="font-weight: bold;">Networking: Lag Compensation + Physics Rollback</span><br />
<span style="font-style: italic;">Transport-agnostic server-authoritative simulation layer (lag-compensation branch)</span><br />
<br />
A commercial-grade networking substrate built from scratch under *Engine/Net/*. Two independent tiers of temporal correction, built using shipped-game references:<br />
<ul>
<li><span style="font-weight: bold;">Tier 1 — selective hitbox rewind</span> (Photon Fusion 2 / Source / CS:GO model). Each server tick, every replicated actor contributes a flat list of hitbox samples into a 256-tick ring buffer (~4.27 s @ 60 Hz). Spatial queries take a *(tick, frac)* pair, linearly interpolate the two bracketing snapshots, and test against the interpolated hitboxes. The live gameplay scene is never touched — perfect for hitscan / melee where you only need “where was this limb when the client fired”.</li>
<li><span style="font-weight: bold;">Tier 2 — full PhysX rewind + resimulation</span> (UE5 networked-physics model). Snapshots pose / linear + angular velocity / kinematic / sleeping flags for every tracked *Actor* each tick; on demand restores them and drives *Physics.stepOnce(dt)* forward to catch up to the current tick. A *RollbackGuard* (TLS depth counter) is held active for the resim window so gameplay / animation / audio code that checks *RollbackGuard::active()* can suppress side-effects (event fire, notifies, SFX) during replay.<br />
</li></ul>
<br />
All modules are <span style="font-weight: bold;">unconditionally compiled</span> — no CMake flag, no opt-out, no extra link cost (the dependency surface is zero — pure C++, no third-party libraries). The transport layer is <span style="font-weight: bold;">abstract</span>: the engine ships *LoopbackTransport* (in-process queue) and *FakeLatencyTransport* (wraps any transport with configurable latency / jitter / loss), plus sketches for *ENetTransport* and a multi-hop *ForwardingTransport* in *Engine/H/Net/NetTransport.h* header comments. User projects subclass *INetTransport* and forward to ENet / Steam Sockets / WebRTC / whatever fits their deployment.<br />
<br />
<span style="font-weight: bold;">Module layout:</span><br />
<ul>
<li>*Net/NetTick.h/cpp* — *TickSeq* (*ULong*) + *NetTick{seq, frac}* shared tick + sub-tick type</li>
<li>*Net/NetClock.h/cpp* — NTP-style clock sync; rolling 5-sample median-of-offset + RTT + jitter; monotonic server timeline</li>
<li>*Net/NetTransport.h/cpp* — *INetTransport* abstract + *LoopbackTransport* + *FakeLatencyTransport* + ENet / multi-hop sketches</li>
<li>*Net/NetObject.h/cpp* — *INetReplicated* interface, *NetBase* convenience, *NET_REGISTER_CLASS* factory, *HitboxSample*, *NetWriter* / *NetReader* byte-buffer codecs with opt-in *putUIntPacked* (varint) / *putPosQ16* (quantized position, 6 B vs 12 B) compression helpers</li>
<li>*Net/InputCommand.h/cpp* — *InputCommand* POD + *InputBuffer* (client redundancy — re-sends last N unacked cmds) + *InputQueue* (server per-connection, wrap-safe *UShort* seq compare)</li>
<li>*Net/NetReplicator.h/cpp* — spawn / despawn / snapshot build + apply / input routing / ping-pong / per-connection relevance callback hook</li>
<li>*Net/LagCompensation.h/cpp* — <span style="font-weight: bold;">Tier 1</span> 256-tick hitbox ring; *ray* / *sweep* / *sweepArc* / *overlap* / *fetchInterp* CPU-analytic queries (sphere + capsule + box) with *(tick, frac)* OR *(real_time)* addressing for clustered servers</li>
<li>*Net/RollbackGuard.h/cpp* — thread-local depth counter; *RollbackGuard::active()* callable from gameplay / anim code</li>
<li>*Net/NetRollback.h/cpp* — <span style="font-weight: bold;">Tier 2</span> snapshot / restore *Actor* state, *Physics.stepOnce*-driven resim, 8-tick default cap (user-configurable), *InputReplayFn* hook, *AnimCaptureFn* / *AnimRestoreFn* hooks for bone-sourced hitboxes<br />
</li></ul>
<br />
~3,200 lines of new engine code across 18 files, zero third-party links, zero *#ifdef*s outside platform branches.<br />
<br />
<span style="font-weight: bold;">Quick start (single-process, both sides in one *Update()* — this is how every category-20 tutorial is structured):</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>// --- Replicated actor ---<br />
struct Target : NetBase<br />
{<br />
Vec pos;<br />
NetClassID netClassId()C override { return 0x54475431u; /* 'TGT1' */ }<br />
void netSerialize&nbsp;&nbsp;(NetWriter &amp;w, UInt fields)C override { w.putVec(pos); }<br />
void netDeserialize(NetReader &amp;r, UInt fields)&nbsp;&nbsp;override { pos = r.getVec(); }<br />
void netHitboxes(Memc&lt;HitboxSample&gt; &amp;out)C override<br />
{<br />
HitboxSample &amp;s = out.New();<br />
s.shape = HITBOX_SPHERE;<br />
s.group = 0x0001;<br />
s.half_ext.set(0.3f, 0.3f, 0.3f);<br />
s.xform.identity(); s.xform.pos = pos;<br />
}<br />
};<br />
NET_REGISTER_CLASS(Target, 0x54475431u);<br />
<br />
// --- Server ---<br />
NetReplicator SvrRep;&nbsp;&nbsp;NetClock SvrClk;&nbsp;&nbsp;LagComp SvrLag;<br />
SvrClk.initServer(60);<br />
SvrRep.serverInit(&amp;transport, &amp;SvrClk);<br />
<br />
each_tick:<br />
SvrRep.serverDrainInputs();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // apply client inputs via INetReplicated::netApplyInput<br />
// ...gameplay simulation...<br />
SvrLag.recordTick(SvrRep, tick, Time.curTime());<br />
if(tick % 3 == 0) SvrRep.sendSnapshots(tick);&nbsp;&nbsp; // 20 Hz @ 60 tick<br />
<br />
on_client_fire(from, to, client_tick):<br />
LagHit h;<br />
if(SvrLag.ray(client_tick, 0.0f, from, to, h))<br />
apply_damage(h.owner, h.owner_idx);<br />
<br />
// --- Client ---<br />
NetReplicator CliRep;&nbsp;&nbsp;NetClock CliClk;&nbsp;&nbsp;InputBuffer CliInputs;<br />
CliClk.initClient(60);<br />
CliRep.clientInit(&amp;transport, &amp;CliClk, /*server_conn*/1);<br />
<br />
each_frame:<br />
CliRep.sendPingMaybe();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // periodic NTP sync<br />
CliRep.update(Time.curTime());&nbsp;&nbsp;&nbsp;&nbsp;// drains spawn/despawn/snapshot/pong<br />
Vec input = sample_keyboard();<br />
pawn-&gt;predict(input);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // local prediction on owned pawn<br />
CliInputs.push(input, buttons, CliClk.tick());<br />
CliInputs.sendTo(CliRep);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // redundant — includes last N unacked<br />
CliInputs.ack(CliRep.lastAckedSeq());<br />
<br />
on_fire(from, to):<br />
// Ship the compensation tick with the fire cmd so the server can rewind<br />
// to exactly what this client was seeing.<br />
fire_cmd.compensation_tick = CliRep.lastSnapshotTick();</code></div></div>
<br />
<span style="font-weight: bold;">Tier 2 (physics rewind) — only when you actually need resim; most games won't:</span><br />
<br />
<span style="font-style: italic;">NetRollback SvrRoll;</span><br />
<span style="font-style: italic;">SvrRoll.init(1.0f/60.0f, /*max_resim=*/8);</span><br />
<span style="font-style: italic;">for(Actor *a : dynamic_physics_actors) SvrRoll.track(*a);</span><br />
<br />
<span style="font-style: italic;">on_physics_step_completed(tick):</span><br />
<span style="font-style: italic;">SvrRoll.recordTick(tick);</span><br />
<br />
<span style="font-style: italic;">on_authoritative_correction(target_tick):</span><br />
<span style="font-style: italic;">SvrRoll.rewindAndResimulate(target_tick, currentTick, replayInputs, user);</span><br />
<br />
<span style="font-weight: bold;">Physics engine touch points</span> — two small, surgical additions; everything else is pure add-on:<br />
<ul>
<li>*Physics.stepOnce(Flt dt)* — direct *simulate* + *fetchResults* with no accumulator, no *simulation_step_completed* callback. This is what drives Tier 2 resim. Safe only between frames (after *stopSimulation()* or inside *WorldManager::physics_update()*).</li>
<li>*Time.rollbackDt(Flt dt)* — thread-local override for *Time.d()*. When *&gt; 0*, *Time.d()* returns it instead of the real frame delta. Set automatically by *NetRollback::rewindAndResimulate* per resim tick to the recorded *dt_used*, so animation code (*Motion::updateAuto*, *AnimatedSkeleton::animate*) replays at the exact dt the forward sim used — deterministic bone poses during rewind.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Animation sync hooks for bone-driven hitboxes</span> — if your game's hitboxes are attached to *AnimatedSkeleton* bones (limbs parented to bone slots), both forward-sim animation and rollback-resim animation must reproduce the same per-tick bone poses. Two engine hooks pair them:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Forward sim</span> — set *Time.rollbackDt(icf.timeDeltaPhysics)* before animating each player tick, so *Motion::updateAuto* uses the physics dt instead of the variable frame dt. Restore with *Time.rollbackDt(0)*.</li>
<li><span style="font-weight: bold;">Rollback resim</span> — pass *NetRollback::setAnimCallbacks(cap, rest, user)*. *AnimCaptureFn* serialises every tracked *Motion* (via *Motion::save* or direct field writes) into a small opaque blob per tick (~64 B per motion × N motions × 256 ticks — fits easily). *AnimRestoreFn* does the inverse <span style="font-weight: bold;">before</span> the user's *InputReplayFn* fires, so replayed animation code starts from the correct *Motion.time* / *Motion.blend*. Pair this with *if(RollbackGuard::active()) return;* at the top of anim-event consumers (weapon clash SFX, hit sparks) so they don't re-fire during replay.<br />
</li></ol>
<br />
<br />
Includes 7 more tutorials.<br />
<a href="https://postimg.cc/hhh408RZ" target="_blank"><img src="https://i.postimg.cc/gcHwm4Sp/Screenshot-from-2026-04-21-21-12-54.png" border="0" alt="[Image: Screenshot-from-2026-04-21-21-12-54.png]" /></a><br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[RmlUi]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11815</link>
			<pubDate>Mon, 20 Apr 2026 13:58:58 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11815</guid>
			<description><![CDATA[Claude Code was able to one-shot RmlUi integration into Esenthel.<br />
<br />
I'm not a web-dev person, but with Vibe Designing coming soon<br />
( e.g. <a href="https://claude.ai/design" target="_blank">https://claude.ai/design</a> ) , I think there is value in having a UI<br />
system defined by a declarative language like HTML/CSS.<br />
<br />
This also makes it much easier for users to mod or skin the UI if they<br />
want. But really, it's about making it easier for the AI to do it.<br />
Claude Code really seems to struggle with building Esenthel GUIs<br />
programmatically that look good and have the correct font size, etc.,<br />
but with HTML/CSS this problem doesn't exist.<br />
<br />
I haven't profiled this, the implementation has not been optimized,<br />
and I'm sure there is a performance penalty. That said, RmlUi is<br />
highly regarded and is designed for games; it is not a full WebKit.<br />
<br />
An integration of <span style="font-weight: bold;"><a href="https://github.com/mikke89/RmlUi" target="_blank">RmlUi</a> v6.2</span> (MIT) as<br />
a retained-mode HTML/CSS UI library that <span style="font-weight: bold;">coexists</span> with Esenthel's built-in<br />
<span style="font-style: italic;">Gui</span> system without touching it. Both run in the same frame — the native<br />
widget set is still available for lightweight 3D-integrated controls, while<br />
RmlUi handles HTML/CSS/animation-heavy UI (menus, HUDs, settings screens,<br />
chat windows). A single RmlUi render backend lives inside the Engine static<br />
library and drives RmlUi's vertex stream through the engine's existing 2D<br />
batcher, so every renderer backend (DX11, DX12, Vulkan, OpenGL) gets RmlUi<br />
for free with zero backend-specific code.<br />
<br />
Game HUD tutorial<br />
<a href="https://pixeldrain.com/api/file/nWMwv7tq" target="_blank"><img src="https://pixeldrain.com/api/file/nWMwv7tq" width="800" height="339" border="0" alt="[Image: nWMwv7tq]" /></a><br />
<br />
<br />
Using Esenthel GUI elements and RmlUi elements side-by-side tutorial<br />
<a href="https://pixeldrain.com/api/file/3cB1Nk65" target="_blank"><img src="https://pixeldrain.com/api/file/3cB1Nk65" width="800" height="339" border="0" alt="[Image: 3cB1Nk65]" /></a><br />
<br />
<br />
<span style="font-weight: bold;">Quick start:</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#include "Gui/RmlUi.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// EE::RmlUi::* facade<br />
<br />
// In a single .cpp where you author Rml event listeners, DOM manipulation,<br />
// etc. — wrap the RmlUi includes so Esenthel macros and X11 defines don't<br />
// collide with RmlUi's template parameters and enum names:<br />
#include "../../ThirdPartyLibs/begin.h"<br />
#undef T1&nbsp;&nbsp;#undef T2&nbsp;&nbsp;#undef T3&nbsp;&nbsp;#undef T4&nbsp;&nbsp;#undef T5<br />
#undef T6&nbsp;&nbsp;#undef T7&nbsp;&nbsp;#undef T8&nbsp;&nbsp;#undef T9&nbsp;&nbsp;#undef T10&nbsp;&nbsp;#undef T11<br />
#undef Always&nbsp;&nbsp;#undef NotUseful<br />
#include &lt;RmlUi/Core.h&gt;<br />
#include "../../ThirdPartyLibs/end.h"<br />
<br />
static Rml::Context&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Ctx=null;<br />
static Rml::ElementDocument *Doc=null;<br />
<br />
Bool Init()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Init();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // install render + system interfaces<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::LoadFontFace("Data/RmlUi/LatoLatin-Regular.ttf");<br />
&nbsp;&nbsp;&nbsp;&nbsp;Ctx=EE::RmlUi::CreateContext();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// sized to D.resW()/D.resH()<br />
&nbsp;&nbsp;&nbsp;&nbsp;Doc=Ctx-&gt;LoadDocument("Data/RmlUi/demo.rml");<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Doc)Doc-&gt;Show();<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Shut()&nbsp;&nbsp; { EE::RmlUi::Shut(); }<br />
Bool Update() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Kb.bp(KB_ESC))return false;<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.update();<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::PumpInput(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Ms/Kb -&gt; context<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Update&nbsp;&nbsp; (Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // events, animations, dirty geometry<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Draw()&nbsp;&nbsp; {<br />
&nbsp;&nbsp;&nbsp;&nbsp;Renderer(RenderScene);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // optional 3D world<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Esenthel Gui above 3D<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Render(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// RmlUi on top of everything<br />
}</code></div></div>
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
<li><span style="font-weight: bold;">Single-TU backend</span> — <span style="font-style: italic;">Engine/Source/Gui/RmlUi.cpp</span> is the only engine<br />
source file that includes RmlUi headers. It subclasses<br />
<span style="font-style: italic;">Rml::RenderInterface</span> and <span style="font-style: italic;">Rml::SystemInterface</span>, owns the 1×1 white<br />
fallback texture, and exports the <span style="font-style: italic;">EE::RmlUi::</span> facade used by app code.<br />
Macro pollution is neutralised locally: Esenthel's template-shortcut<br />
macros <span style="font-style: italic;">T1..T11</span> (from <span style="font-style: italic;">Engine/H/_/defines.h</span>, they collide with<br />
<span style="font-style: italic;">robin_hood.h</span>'s template parameters) and X11's <span style="font-style: italic;">Always</span> / <span style="font-style: italic;">NotUseful</span><br />
(not stripped by <span style="font-style: italic;">Engine/H/_/headers.h</span>) are undefined inside the<br />
<span style="font-style: italic;">ThirdPartyLibs/begin.h</span> / <span style="font-style: italic;">end.h</span> bracket just for this one TU.</li>
<li><span style="font-weight: bold;">API-agnostic rendering</span> — <span style="font-style: italic;">RenderGeometry</span> feeds RmlUi's pixel-space<br />
vertex stream into the engine's existing 2D path. Per-vertex<br />
conversion goes through <span style="font-style: italic;">D.pixelToScreen(Vec2)</span> (canonical helper; no<br />
hand-rolled Y-flip). <span style="font-style: italic;">D.alpha(ALPHA_MERGE)</span> handles RmlUi's<br />
premultiplied RGBA (which it emits since v5). Scissor goes through<br />
<span style="font-style: italic;">D.clip(&amp;engine_rect)</span> / <span style="font-style: italic;">D.pixelToScreen(RectI)</span>. Works identically on<br />
DX11, DX12, Vulkan, OpenGL.</li>
<li><span style="font-weight: bold;">Untextured-path fallback</span> — RmlUi calls <span style="font-style: italic;">RenderGeometry</span> with<br />
<span style="font-style: italic;">texture=0</span> for solid-colour geometry. The backend binds a pre-built<br />
1×1 opaque-white <span style="font-style: italic;">Image</span> so the textured shader path effectively<br />
degenerates to plain vertex colour. This avoids needing a separate<br />
colour-only code path.</li>
<li><span style="font-weight: bold;">FreeType sharing</span> — RmlUi's default FreeType font engine is pointed<br />
at Esenthel's prebuilt <span style="font-style: italic;">EE_FreeType</span> static lib via<br />
<span style="font-style: italic;">FREETYPE_INCLUDE_DIRS</span> / <span style="font-style: italic;">FREETYPE_LIBRARY</span> cache vars in<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/CMakeLists.txt</span>, so one copy of FreeType ships in<br />
the final link. <span style="font-style: italic;">EE_RMLUI_USE_BUNDLED_FREETYPE=ON</span> is an escape hatch<br />
if the prebuilt's config flags ever diverge from what RmlUi expects.</li>
<li><span style="font-weight: bold;">Texture upload</span> — <span style="font-style: italic;">LoadTexture</span> routes to <span style="font-style: italic;">Image.Import</span>, accepting<br />
every format Esenthel's asset pipeline supports (BMP/PNG/JPG/JXL/WEBP/<br />
AVIF/HEIF/TGA/TIF/DDS/PSD/ICO/HDR). <span style="font-style: italic;">GenerateTexture</span> uploads raw<br />
RGBA8 via a soft image, lock/write, row-copy, unlock, and GPU upload<br />
(used for the font atlas and procedural decorator textures).</li>
<li><span style="font-weight: bold;">Dual-mode input pump</span> — <span style="font-style: italic;">EE::RmlUi::PumpInput(ctx)</span> polls<br />
mouse position, mouse buttons, and wheel every frame, walks all<br />
256 <span style="font-style: italic;">KB_KEY</span> values firing key down/up events on edge changes, and<br />
pumps text characters out of <span style="font-style: italic;">Kb.k.c</span>. When the focused RmlUi element<br />
is an input field or textarea the key queue is drained so fast typing<br />
never drops characters; otherwise the head of the queue is peeked<br />
non-destructively so the built-in Gui still sees the same keys on the<br />
same frame.</li>
<li><span style="font-weight: bold;">Opt-in via CMake</span> — <span style="font-style: italic;">option(EE_RMLUI "... " ON)</span> in the root<br />
<span style="font-style: italic;">CMakeLists.txt</span>, default ON on Linux x64 + Windows x64, fatal on iOS<br />
/ Android (deferred until those platforms are tested). With<br />
<span style="font-style: italic;">EE_RMLUI=OFF</span> every integration TU is empty, the<br />
<span style="font-style: italic;">rmlui_core</span> / <span style="font-style: italic;">rmlui_debugger</span> targets are not added to<br />
the build graph, and the tutorial targets are skipped in<br />
<span style="font-style: italic;">Tutorials/CMakeLists.txt</span>.</li>
<li><span style="font-weight: bold;">Built from source</span> — RmlUi v6.2 is vendored at<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/lib/</span> and compiled via subdirectory inclusion so<br />
<span style="font-style: italic;">rmlui_core</span> + <span style="font-style: italic;">rmlui_debugger</span> become Engine interface link deps.<br />
No prebuilt library like the other third-parties, because upstream<br />
ships clean CMake and the library is small. ABI-visible flags are<br />
forced onto both targets so they match Engine's ABI.</li>
<li><span style="font-weight: bold;">Debugger overlay</span> — <span style="font-style: italic;">rmlui_debugger</span> is linked automatically when<br />
<span style="font-style: italic;">EE_RMLUI=ON</span>. Call <span style="font-style: italic;">Rml::Debugger::Initialise(ctx)</span> once, then<br />
<span style="font-style: italic;">Rml::Debugger::SetVisible(true)</span> (or bind to a key) to pop up the live<br />
inspector — Event Log, Element Info, Outlines, Data Models tabs.</li>
<li><span style="font-weight: bold;">Thin facade</span> — <span style="font-style: italic;">Engine/H/Gui/RmlUi.h</span> exposes only the<br />
<span style="font-style: italic;">EE::RmlUi::</span> lifecycle and helper functions. It forward-declares<br />
<span style="font-style: italic;">Rml::Context</span>, <span style="font-style: italic;">ElementDocument</span>, <span style="font-style: italic;">RenderInterface</span>, and<br />
<span style="font-style: italic;">SystemInterface</span>, so app translation units that just drive lifecycle<br />
do not pay to parse all of <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span>. App translation units<br />
that want event listeners, DOM mutation, or typed element access can<br />
include <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span> themselves.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration</span> — zero changes to <span style="font-style: italic;">Engine/Source/Gui/Gui.cpp</span>,<br />
<span style="font-style: italic;">Gui.draw</span>, <span style="font-style: italic;">Gui.update</span>, no new state in <span style="font-style: italic;">DisplayClass</span>, no new<br />
alpha mode, no new shaders. The pre-existing <span style="font-style: italic;">VI</span> + <span style="font-style: italic;">D</span> 2D path is the<br />
only rendering surface used; the existing 2D shaders are the only ones<br />
touched. The tutorial harness (<span style="font-style: italic;">Tutorials/TutorialAuto.{h,cpp}</span>,<br />
<span style="font-style: italic;">Tutorials/stdafx.h</span>) is unchanged. <span style="font-style: italic;">EE_RMLUI=OFF</span> builds are<br />
pixel-identical to pre-integration builds (verified against<br />
<span style="font-style: italic;">Tutorial_05_Bars</span>, <span style="font-style: italic;">Tutorial_04_BatchedDrawing</span>,<br />
<span style="font-style: italic;">Tutorial_12_RenderToTexture</span>).<br />
<br />
<span style="font-weight: bold;">RCSS gotcha</span> — RmlUi's transition/animation parser recognises tween<br />
names as defined in <span style="font-style: italic;">Source/Core/PropertyParserAnimation.cpp</span>. Unlike<br />
standard CSS, it does <span style="font-weight: bold;">not</span> accept bare <span style="font-style: italic;">linear</span> or the CSS keyword<br />
<span style="font-style: italic;">ease</span>; use <span style="font-style: italic;">linear-in-out</span> and <span style="font-style: italic;">cubic-in-out</span> (or any of the<br />
supported <span style="font-style: italic;">{back,bounce,circular,cubic,elastic,exponential,linear,quadratic,quartic,quintic&#8203;,sine}-{in,out,in-out}</span><br />
variants). Multi-transitions are comma-separated.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Custom <span style="font-style: italic;">Rml::FontEngineInterface</span> on top of Esenthel's <span style="font-style: italic;">Font</span> class, so<br />
<span style="font-style: italic;">.pak</span>-packaged fonts could feed RmlUi. Upstream's default FreeType<br />
engine is sufficient for now; TTF files ship alongside the tutorials.</li>
<li><span style="font-style: italic;">Rml::FileInterface</span> on <span style="font-style: italic;">EE::File</span> — would let RmlUi resolve<br />
<span style="font-style: italic;">.rml</span> / <span style="font-style: italic;">.rcss</span> / texture paths through engine <span style="font-style: italic;">.pak</span> archives. The<br />
stdio <span style="font-style: italic;">fopen</span> default is used currently; tutorials ship plain files<br />
under <span style="font-style: italic;">Tutorials/Data/RmlUi/</span>.</li>
<li>Data Binding (MVC layer via <span style="font-style: italic;">Rml::DataModelConstructor</span>) — supported by<br />
the linked <span style="font-style: italic;">rmlui_core</span> but not yet demonstrated by a tutorial.</li>
<li>Lua bindings (<span style="font-style: italic;">rmlui_lua</span>) — disabled in the wrapper (<span style="font-style: italic;">RMLUI_LUA_BINDINGS=OFF</span>);<br />
enabling would require wiring an Esenthel Lua runtime into the engine<br />
first.</li>
<li>Windows smoke sweep via <span style="font-style: italic;">run_smoke_test.ps1</span>. Linux is covered<br />
(all five tutorials + three regression targets pass).<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Claude Code was able to one-shot RmlUi integration into Esenthel.<br />
<br />
I'm not a web-dev person, but with Vibe Designing coming soon<br />
( e.g. <a href="https://claude.ai/design" target="_blank">https://claude.ai/design</a> ) , I think there is value in having a UI<br />
system defined by a declarative language like HTML/CSS.<br />
<br />
This also makes it much easier for users to mod or skin the UI if they<br />
want. But really, it's about making it easier for the AI to do it.<br />
Claude Code really seems to struggle with building Esenthel GUIs<br />
programmatically that look good and have the correct font size, etc.,<br />
but with HTML/CSS this problem doesn't exist.<br />
<br />
I haven't profiled this, the implementation has not been optimized,<br />
and I'm sure there is a performance penalty. That said, RmlUi is<br />
highly regarded and is designed for games; it is not a full WebKit.<br />
<br />
An integration of <span style="font-weight: bold;"><a href="https://github.com/mikke89/RmlUi" target="_blank">RmlUi</a> v6.2</span> (MIT) as<br />
a retained-mode HTML/CSS UI library that <span style="font-weight: bold;">coexists</span> with Esenthel's built-in<br />
<span style="font-style: italic;">Gui</span> system without touching it. Both run in the same frame — the native<br />
widget set is still available for lightweight 3D-integrated controls, while<br />
RmlUi handles HTML/CSS/animation-heavy UI (menus, HUDs, settings screens,<br />
chat windows). A single RmlUi render backend lives inside the Engine static<br />
library and drives RmlUi's vertex stream through the engine's existing 2D<br />
batcher, so every renderer backend (DX11, DX12, Vulkan, OpenGL) gets RmlUi<br />
for free with zero backend-specific code.<br />
<br />
Game HUD tutorial<br />
<a href="https://pixeldrain.com/api/file/nWMwv7tq" target="_blank"><img src="https://pixeldrain.com/api/file/nWMwv7tq" width="800" height="339" border="0" alt="[Image: nWMwv7tq]" /></a><br />
<br />
<br />
Using Esenthel GUI elements and RmlUi elements side-by-side tutorial<br />
<a href="https://pixeldrain.com/api/file/3cB1Nk65" target="_blank"><img src="https://pixeldrain.com/api/file/3cB1Nk65" width="800" height="339" border="0" alt="[Image: 3cB1Nk65]" /></a><br />
<br />
<br />
<span style="font-weight: bold;">Quick start:</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#include "Gui/RmlUi.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// EE::RmlUi::* facade<br />
<br />
// In a single .cpp where you author Rml event listeners, DOM manipulation,<br />
// etc. — wrap the RmlUi includes so Esenthel macros and X11 defines don't<br />
// collide with RmlUi's template parameters and enum names:<br />
#include "../../ThirdPartyLibs/begin.h"<br />
#undef T1&nbsp;&nbsp;#undef T2&nbsp;&nbsp;#undef T3&nbsp;&nbsp;#undef T4&nbsp;&nbsp;#undef T5<br />
#undef T6&nbsp;&nbsp;#undef T7&nbsp;&nbsp;#undef T8&nbsp;&nbsp;#undef T9&nbsp;&nbsp;#undef T10&nbsp;&nbsp;#undef T11<br />
#undef Always&nbsp;&nbsp;#undef NotUseful<br />
#include &lt;RmlUi/Core.h&gt;<br />
#include "../../ThirdPartyLibs/end.h"<br />
<br />
static Rml::Context&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Ctx=null;<br />
static Rml::ElementDocument *Doc=null;<br />
<br />
Bool Init()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Init();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // install render + system interfaces<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::LoadFontFace("Data/RmlUi/LatoLatin-Regular.ttf");<br />
&nbsp;&nbsp;&nbsp;&nbsp;Ctx=EE::RmlUi::CreateContext();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// sized to D.resW()/D.resH()<br />
&nbsp;&nbsp;&nbsp;&nbsp;Doc=Ctx-&gt;LoadDocument("Data/RmlUi/demo.rml");<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Doc)Doc-&gt;Show();<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Shut()&nbsp;&nbsp; { EE::RmlUi::Shut(); }<br />
Bool Update() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Kb.bp(KB_ESC))return false;<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.update();<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::PumpInput(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Ms/Kb -&gt; context<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Update&nbsp;&nbsp; (Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // events, animations, dirty geometry<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Draw()&nbsp;&nbsp; {<br />
&nbsp;&nbsp;&nbsp;&nbsp;Renderer(RenderScene);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // optional 3D world<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Esenthel Gui above 3D<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Render(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// RmlUi on top of everything<br />
}</code></div></div>
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
<li><span style="font-weight: bold;">Single-TU backend</span> — <span style="font-style: italic;">Engine/Source/Gui/RmlUi.cpp</span> is the only engine<br />
source file that includes RmlUi headers. It subclasses<br />
<span style="font-style: italic;">Rml::RenderInterface</span> and <span style="font-style: italic;">Rml::SystemInterface</span>, owns the 1×1 white<br />
fallback texture, and exports the <span style="font-style: italic;">EE::RmlUi::</span> facade used by app code.<br />
Macro pollution is neutralised locally: Esenthel's template-shortcut<br />
macros <span style="font-style: italic;">T1..T11</span> (from <span style="font-style: italic;">Engine/H/_/defines.h</span>, they collide with<br />
<span style="font-style: italic;">robin_hood.h</span>'s template parameters) and X11's <span style="font-style: italic;">Always</span> / <span style="font-style: italic;">NotUseful</span><br />
(not stripped by <span style="font-style: italic;">Engine/H/_/headers.h</span>) are undefined inside the<br />
<span style="font-style: italic;">ThirdPartyLibs/begin.h</span> / <span style="font-style: italic;">end.h</span> bracket just for this one TU.</li>
<li><span style="font-weight: bold;">API-agnostic rendering</span> — <span style="font-style: italic;">RenderGeometry</span> feeds RmlUi's pixel-space<br />
vertex stream into the engine's existing 2D path. Per-vertex<br />
conversion goes through <span style="font-style: italic;">D.pixelToScreen(Vec2)</span> (canonical helper; no<br />
hand-rolled Y-flip). <span style="font-style: italic;">D.alpha(ALPHA_MERGE)</span> handles RmlUi's<br />
premultiplied RGBA (which it emits since v5). Scissor goes through<br />
<span style="font-style: italic;">D.clip(&amp;engine_rect)</span> / <span style="font-style: italic;">D.pixelToScreen(RectI)</span>. Works identically on<br />
DX11, DX12, Vulkan, OpenGL.</li>
<li><span style="font-weight: bold;">Untextured-path fallback</span> — RmlUi calls <span style="font-style: italic;">RenderGeometry</span> with<br />
<span style="font-style: italic;">texture=0</span> for solid-colour geometry. The backend binds a pre-built<br />
1×1 opaque-white <span style="font-style: italic;">Image</span> so the textured shader path effectively<br />
degenerates to plain vertex colour. This avoids needing a separate<br />
colour-only code path.</li>
<li><span style="font-weight: bold;">FreeType sharing</span> — RmlUi's default FreeType font engine is pointed<br />
at Esenthel's prebuilt <span style="font-style: italic;">EE_FreeType</span> static lib via<br />
<span style="font-style: italic;">FREETYPE_INCLUDE_DIRS</span> / <span style="font-style: italic;">FREETYPE_LIBRARY</span> cache vars in<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/CMakeLists.txt</span>, so one copy of FreeType ships in<br />
the final link. <span style="font-style: italic;">EE_RMLUI_USE_BUNDLED_FREETYPE=ON</span> is an escape hatch<br />
if the prebuilt's config flags ever diverge from what RmlUi expects.</li>
<li><span style="font-weight: bold;">Texture upload</span> — <span style="font-style: italic;">LoadTexture</span> routes to <span style="font-style: italic;">Image.Import</span>, accepting<br />
every format Esenthel's asset pipeline supports (BMP/PNG/JPG/JXL/WEBP/<br />
AVIF/HEIF/TGA/TIF/DDS/PSD/ICO/HDR). <span style="font-style: italic;">GenerateTexture</span> uploads raw<br />
RGBA8 via a soft image, lock/write, row-copy, unlock, and GPU upload<br />
(used for the font atlas and procedural decorator textures).</li>
<li><span style="font-weight: bold;">Dual-mode input pump</span> — <span style="font-style: italic;">EE::RmlUi::PumpInput(ctx)</span> polls<br />
mouse position, mouse buttons, and wheel every frame, walks all<br />
256 <span style="font-style: italic;">KB_KEY</span> values firing key down/up events on edge changes, and<br />
pumps text characters out of <span style="font-style: italic;">Kb.k.c</span>. When the focused RmlUi element<br />
is an input field or textarea the key queue is drained so fast typing<br />
never drops characters; otherwise the head of the queue is peeked<br />
non-destructively so the built-in Gui still sees the same keys on the<br />
same frame.</li>
<li><span style="font-weight: bold;">Opt-in via CMake</span> — <span style="font-style: italic;">option(EE_RMLUI "... " ON)</span> in the root<br />
<span style="font-style: italic;">CMakeLists.txt</span>, default ON on Linux x64 + Windows x64, fatal on iOS<br />
/ Android (deferred until those platforms are tested). With<br />
<span style="font-style: italic;">EE_RMLUI=OFF</span> every integration TU is empty, the<br />
<span style="font-style: italic;">rmlui_core</span> / <span style="font-style: italic;">rmlui_debugger</span> targets are not added to<br />
the build graph, and the tutorial targets are skipped in<br />
<span style="font-style: italic;">Tutorials/CMakeLists.txt</span>.</li>
<li><span style="font-weight: bold;">Built from source</span> — RmlUi v6.2 is vendored at<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/lib/</span> and compiled via subdirectory inclusion so<br />
<span style="font-style: italic;">rmlui_core</span> + <span style="font-style: italic;">rmlui_debugger</span> become Engine interface link deps.<br />
No prebuilt library like the other third-parties, because upstream<br />
ships clean CMake and the library is small. ABI-visible flags are<br />
forced onto both targets so they match Engine's ABI.</li>
<li><span style="font-weight: bold;">Debugger overlay</span> — <span style="font-style: italic;">rmlui_debugger</span> is linked automatically when<br />
<span style="font-style: italic;">EE_RMLUI=ON</span>. Call <span style="font-style: italic;">Rml::Debugger::Initialise(ctx)</span> once, then<br />
<span style="font-style: italic;">Rml::Debugger::SetVisible(true)</span> (or bind to a key) to pop up the live<br />
inspector — Event Log, Element Info, Outlines, Data Models tabs.</li>
<li><span style="font-weight: bold;">Thin facade</span> — <span style="font-style: italic;">Engine/H/Gui/RmlUi.h</span> exposes only the<br />
<span style="font-style: italic;">EE::RmlUi::</span> lifecycle and helper functions. It forward-declares<br />
<span style="font-style: italic;">Rml::Context</span>, <span style="font-style: italic;">ElementDocument</span>, <span style="font-style: italic;">RenderInterface</span>, and<br />
<span style="font-style: italic;">SystemInterface</span>, so app translation units that just drive lifecycle<br />
do not pay to parse all of <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span>. App translation units<br />
that want event listeners, DOM mutation, or typed element access can<br />
include <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span> themselves.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration</span> — zero changes to <span style="font-style: italic;">Engine/Source/Gui/Gui.cpp</span>,<br />
<span style="font-style: italic;">Gui.draw</span>, <span style="font-style: italic;">Gui.update</span>, no new state in <span style="font-style: italic;">DisplayClass</span>, no new<br />
alpha mode, no new shaders. The pre-existing <span style="font-style: italic;">VI</span> + <span style="font-style: italic;">D</span> 2D path is the<br />
only rendering surface used; the existing 2D shaders are the only ones<br />
touched. The tutorial harness (<span style="font-style: italic;">Tutorials/TutorialAuto.{h,cpp}</span>,<br />
<span style="font-style: italic;">Tutorials/stdafx.h</span>) is unchanged. <span style="font-style: italic;">EE_RMLUI=OFF</span> builds are<br />
pixel-identical to pre-integration builds (verified against<br />
<span style="font-style: italic;">Tutorial_05_Bars</span>, <span style="font-style: italic;">Tutorial_04_BatchedDrawing</span>,<br />
<span style="font-style: italic;">Tutorial_12_RenderToTexture</span>).<br />
<br />
<span style="font-weight: bold;">RCSS gotcha</span> — RmlUi's transition/animation parser recognises tween<br />
names as defined in <span style="font-style: italic;">Source/Core/PropertyParserAnimation.cpp</span>. Unlike<br />
standard CSS, it does <span style="font-weight: bold;">not</span> accept bare <span style="font-style: italic;">linear</span> or the CSS keyword<br />
<span style="font-style: italic;">ease</span>; use <span style="font-style: italic;">linear-in-out</span> and <span style="font-style: italic;">cubic-in-out</span> (or any of the<br />
supported <span style="font-style: italic;">{back,bounce,circular,cubic,elastic,exponential,linear,quadratic,quartic,quintic&#8203;,sine}-{in,out,in-out}</span><br />
variants). Multi-transitions are comma-separated.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Custom <span style="font-style: italic;">Rml::FontEngineInterface</span> on top of Esenthel's <span style="font-style: italic;">Font</span> class, so<br />
<span style="font-style: italic;">.pak</span>-packaged fonts could feed RmlUi. Upstream's default FreeType<br />
engine is sufficient for now; TTF files ship alongside the tutorials.</li>
<li><span style="font-style: italic;">Rml::FileInterface</span> on <span style="font-style: italic;">EE::File</span> — would let RmlUi resolve<br />
<span style="font-style: italic;">.rml</span> / <span style="font-style: italic;">.rcss</span> / texture paths through engine <span style="font-style: italic;">.pak</span> archives. The<br />
stdio <span style="font-style: italic;">fopen</span> default is used currently; tutorials ship plain files<br />
under <span style="font-style: italic;">Tutorials/Data/RmlUi/</span>.</li>
<li>Data Binding (MVC layer via <span style="font-style: italic;">Rml::DataModelConstructor</span>) — supported by<br />
the linked <span style="font-style: italic;">rmlui_core</span> but not yet demonstrated by a tutorial.</li>
<li>Lua bindings (<span style="font-style: italic;">rmlui_lua</span>) — disabled in the wrapper (<span style="font-style: italic;">RMLUI_LUA_BINDINGS=OFF</span>);<br />
enabling would require wiring an Esenthel Lua runtime into the engine<br />
first.</li>
<li>Windows smoke sweep via <span style="font-style: italic;">run_smoke_test.ps1</span>. Linux is covered<br />
(all five tutorials + three regression targets pass).<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Improved Destruction]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11814</link>
			<pubDate>Mon, 20 Apr 2026 02:21:50 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11814</guid>
			<description><![CDATA[<span style="font-weight: bold;">Fracture Destruction System</span> (improve-destruction-v3 branch)<br />
<br />
A production-grade destructible-object system located at Engine/Source/Physics/Fracture*.cpp plus Engine/Source/Game/Objects/Fractured.cpp.<br />
<br />
The existing DestructMesh / Game::Destructible implementation from Tutorial 23 is left untouched. The new system ships alongside it under new type names.<br />
<br />
<span style="font-weight: bold;">Core idea:</span><br />
<br />
A <span style="font-weight: bold;">FractureMesh asset</span> pre-bakes a mesh into chunks, a cluster hierarchy, and a connection graph that records which chunks share faces, with per-edge breaking strain.<br />
<br />
A <span style="font-weight: bold;">Game::Fractured runtime object</span> owns the per-instance state. At runtime, it exists as either a single intact-cluster Actor (cluster proxy) or an array of per-chunk Actors connected by breakable PhysX joints.<br />
<br />
Damage accumulates per node, or on the cluster as a whole. Once the threshold is crossed, the object transitions from <span style="font-weight: bold;">INTACT</span> to <span style="font-weight: bold;">SHATTERED</span> by destroying the proxy and spawning per-chunk actors.<br />
<br />
Mass is force-set from bake-time chunk volume so the convex-hull proxy’s mass overestimate does not cause a visible pop at the moment of breakup.<br />
<br />
Strain then propagates along the connection graph, so a hit on one chunk weakens its neighbours. This reproduces “spider cracks” style breakage without requiring a full spatial-field solver.<br />
<br />
<a href="https://postimg.cc/34G95frD" target="_blank"><img src="https://i.postimg.cc/j5g1fBGv/Screenshot-from-2026-04-19-22-14-24.png" border="0" alt="[Image: Screenshot-from-2026-04-19-22-14-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/SnZMZL4n" target="_blank"><img src="https://i.postimg.cc/Jh85zKd5/Screenshot-from-2026-04-19-21-51-13.png" border="0" alt="[Image: Screenshot-from-2026-04-19-21-51-13.png]" /></a><br />
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
[]<span style="font-weight: bold;">Five bake patterns</span> (FRACTURE_PATTERN_) —<br />
PLANAR_SLICES uses random plane cuts and mirrors the classic DestructMesh algorithm.<br />
UNIFORM_VORONOI and CLUSTERED_VORONOI use iterative SplitMeshSolid with perpendicular-bisector planes; the older CSG-based spike was dropped because it was numerically unstable.<br />
RADIAL produces true pie-slice wedges radiating from an impact_point through the wall’s thickness axis.<br />
BRICK uses a dedicated axis-aligned 6-plane-per-cell path (BakeBrickChunks) with <span style="font-weight: bold;">shared-boundary jitter</span>: adjacent bricks read the same boundary entry for their shared seam, so jitter never produces gaps between neighbours.</li>
<li><span style="font-weight: bold;">Cluster proxy</span> (FractureSettings::build_cluster_proxy) — the bake computes a single convex hull over all chunks and stores it on the root Cluster, plus the sum of per-chunk volumes as mass_sum. At spawn, the Fractured object creates one kinematic Actor using this proxy instead of N per-chunk actors. 500 intact crates cost 500 bodies, not 10,000.</li>
<li><span style="font-weight: bold;">Localised release cascade</span> — when damage crosses cluster_break_threshold, Fractured::releaseRootCluster(impact_pos, velocity_delta) destroys the proxy and spawns per-chunk actors, inheriting the proxy’s linear and angular velocity and force-setting each chunk’s mass to the bake-time volume estimate. A <span style="font-weight: bold;">distance-falloff</span> kick is then applied only to chunks within 1.5 m of the impact (mass-scaled, velocity-capped at 4 m/s), so a hit near the bottom of a wall does not catapult distant chunks and make anchored sections rubber-band through joints.</li>
<li><span style="font-weight: bold;">Deferred release</span> — applyPointDamage is called from inside PhysX’s onContact callback; destroying and creating actors there corrupts the scene and crashes the next PxRigidActor::is&lt;&gt;() cast. Instead, threshold crossing sets a pending flag plus the impact position and velocity, and the next update() tick flushes the release outside any callback.</li>
<li><span style="font-weight: bold;">Connection-graph strain propagation</span> — Fractured::propagateStrain walks live_edges each frame when damage_dirty. Damage above the weakest-link elastic limit (0.9 * break_threshold) bleeds across edges; smaller hits dissipate via damage_decay_tau without creeping. Broken joints are removed by BFS; any connected component that does not reach an anchor becomes dynamic.</li>
<li><span style="font-weight: bold;">Bake-time anchors</span> — FractureSettings::anchor_volume marks chunks whose centroid is inside the box with FRAC_FLAG_ANCHORED_AT_BAKE. They spawn kinematic, or the cluster proxy spawns kinematic if any member is anchored. Runtime Fracture::setAnchor(obj, chunk_index, anchored) can flip the anchor state live.</li>
<li><span style="font-weight: bold;">Damage API</span> —<br />
Fracture::applyRadialDamage(pos, radius, strength, impulse, type)<br />
applyContactDamage(a, b, contacts, n, type)<br />
breakNode(obj, node_index, dir, type)<br />
setAnchor(...)<br />
<br />
All accept a DAMAGE_TYPE enum (KINETIC, EXPLOSIVE, PIERCING, THERMAL, CUSTOM) scaled by a global damage_type_scale[5] table, so game code can do things like make grenades 1.5x stronger against concrete with a single tweak.</li>
<li><span style="font-weight: bold;">Multi-subscriber BreakEventCallback</span> — Fracture::addBreakEventCallback is void-return and never consumes the event. addBreakEventCallbackB is bool-return; returning true consumes the event and stops the chain. Legacy setBreakEventCallback is preserved as a single-slot wrapper.</li>
<li><span style="font-weight: bold;">Debris pool</span> — Fracture::setVfxPoolCap, setSfxVoicesCap, debrisActiveCount, plus low-energy sleep and debris-pool caps in updateDebrisHeuristics. Chunks at rest for debris_sleep_timeout seconds are put to sleep so PhysX stops simulating them.</li>
<li><span style="font-weight: bold;">Save / Load</span> — FractureMesh::save(path) and load(path) round-trip the whole asset (chunks, cooked PhysX convex hulls, cluster, and connection graph) through a versioned CC4('F','R','A','C') binary format.<br />
FractureMesh::adjustStorage(universal, physx, bullet) matches the PhysBody contract and allows the asset to be reloaded on a different backend. A cache (Cache&lt;FractureMesh&gt; FractureMeshes("Fracture Mesh")) is preserved so UID-addressed baked assets can live inside pak files like any other engine asset.<br />
<br />
[]<span style="font-weight: bold;">Instance sharing</span> — many Game::Fractured instances can point at the same FractureMesh. Geometry and cooked hulls are shared; only runtime state (damage, actor transforms, live joints) is per-instance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration:</span><br />
<br />
Zero changes to DestructMesh, Game::Destructible, or Tutorial 23.<br />
<br />
PhysX’s Physics.reportContact is <span style="font-weight: bold;">not</span> hijacked. The host app registers its own contact callback and can optionally call Fracture::applyContactDamage from inside it.<br />
<br />
Contact damage is PhysX-only, since Bullet does not deliver contact events in this engine. Radial damage works on both backends.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Multi-level hierarchical fracture (more than 2 levels deep, with per-level break thresholds and cluster-tree view-level LOD). The data model already supports it via Cluster::parent and Cluster::children; the runtime currently walks one level.</li>
<li>Runtime on-the-fly fracture (CSG against an unbaked mesh). Pre-bake only is the shipping path.</li>
<li>View-distance simulation LOD (far objects deferred until the player enters a bubble). The cluster proxy is the foundation; bubble activation logic is planned as a later follow-up.</li>
<li>Multi-platform cooked-hull round-trip. Saved files are currently PhysX-specific; cross-platform support needs adjustStorage(universal=true, ...) before save.</li>
<li>Titan Editor UI for anchor-volume painting, per-chunk material picking, and interactive fracture preview. This branch is intentionally code-only.</li>
<li>Auto-dispatch BreakPreset VFX/SFX from a material tag, so Tutorial 31’s 40-line OnBreak puff could collapse to 5 lines. The scaffolding is already in place (multi-subscriber callbacks, damage types), but the preset registry is deferred.<br />
</li></ul>
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-weight: bold;">Fracture Destruction System</span> (improve-destruction-v3 branch)<br />
<br />
A production-grade destructible-object system located at Engine/Source/Physics/Fracture*.cpp plus Engine/Source/Game/Objects/Fractured.cpp.<br />
<br />
The existing DestructMesh / Game::Destructible implementation from Tutorial 23 is left untouched. The new system ships alongside it under new type names.<br />
<br />
<span style="font-weight: bold;">Core idea:</span><br />
<br />
A <span style="font-weight: bold;">FractureMesh asset</span> pre-bakes a mesh into chunks, a cluster hierarchy, and a connection graph that records which chunks share faces, with per-edge breaking strain.<br />
<br />
A <span style="font-weight: bold;">Game::Fractured runtime object</span> owns the per-instance state. At runtime, it exists as either a single intact-cluster Actor (cluster proxy) or an array of per-chunk Actors connected by breakable PhysX joints.<br />
<br />
Damage accumulates per node, or on the cluster as a whole. Once the threshold is crossed, the object transitions from <span style="font-weight: bold;">INTACT</span> to <span style="font-weight: bold;">SHATTERED</span> by destroying the proxy and spawning per-chunk actors.<br />
<br />
Mass is force-set from bake-time chunk volume so the convex-hull proxy’s mass overestimate does not cause a visible pop at the moment of breakup.<br />
<br />
Strain then propagates along the connection graph, so a hit on one chunk weakens its neighbours. This reproduces “spider cracks” style breakage without requiring a full spatial-field solver.<br />
<br />
<a href="https://postimg.cc/34G95frD" target="_blank"><img src="https://i.postimg.cc/j5g1fBGv/Screenshot-from-2026-04-19-22-14-24.png" border="0" alt="[Image: Screenshot-from-2026-04-19-22-14-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/SnZMZL4n" target="_blank"><img src="https://i.postimg.cc/Jh85zKd5/Screenshot-from-2026-04-19-21-51-13.png" border="0" alt="[Image: Screenshot-from-2026-04-19-21-51-13.png]" /></a><br />
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
[]<span style="font-weight: bold;">Five bake patterns</span> (FRACTURE_PATTERN_) —<br />
PLANAR_SLICES uses random plane cuts and mirrors the classic DestructMesh algorithm.<br />
UNIFORM_VORONOI and CLUSTERED_VORONOI use iterative SplitMeshSolid with perpendicular-bisector planes; the older CSG-based spike was dropped because it was numerically unstable.<br />
RADIAL produces true pie-slice wedges radiating from an impact_point through the wall’s thickness axis.<br />
BRICK uses a dedicated axis-aligned 6-plane-per-cell path (BakeBrickChunks) with <span style="font-weight: bold;">shared-boundary jitter</span>: adjacent bricks read the same boundary entry for their shared seam, so jitter never produces gaps between neighbours.</li>
<li><span style="font-weight: bold;">Cluster proxy</span> (FractureSettings::build_cluster_proxy) — the bake computes a single convex hull over all chunks and stores it on the root Cluster, plus the sum of per-chunk volumes as mass_sum. At spawn, the Fractured object creates one kinematic Actor using this proxy instead of N per-chunk actors. 500 intact crates cost 500 bodies, not 10,000.</li>
<li><span style="font-weight: bold;">Localised release cascade</span> — when damage crosses cluster_break_threshold, Fractured::releaseRootCluster(impact_pos, velocity_delta) destroys the proxy and spawns per-chunk actors, inheriting the proxy’s linear and angular velocity and force-setting each chunk’s mass to the bake-time volume estimate. A <span style="font-weight: bold;">distance-falloff</span> kick is then applied only to chunks within 1.5 m of the impact (mass-scaled, velocity-capped at 4 m/s), so a hit near the bottom of a wall does not catapult distant chunks and make anchored sections rubber-band through joints.</li>
<li><span style="font-weight: bold;">Deferred release</span> — applyPointDamage is called from inside PhysX’s onContact callback; destroying and creating actors there corrupts the scene and crashes the next PxRigidActor::is&lt;&gt;() cast. Instead, threshold crossing sets a pending flag plus the impact position and velocity, and the next update() tick flushes the release outside any callback.</li>
<li><span style="font-weight: bold;">Connection-graph strain propagation</span> — Fractured::propagateStrain walks live_edges each frame when damage_dirty. Damage above the weakest-link elastic limit (0.9 * break_threshold) bleeds across edges; smaller hits dissipate via damage_decay_tau without creeping. Broken joints are removed by BFS; any connected component that does not reach an anchor becomes dynamic.</li>
<li><span style="font-weight: bold;">Bake-time anchors</span> — FractureSettings::anchor_volume marks chunks whose centroid is inside the box with FRAC_FLAG_ANCHORED_AT_BAKE. They spawn kinematic, or the cluster proxy spawns kinematic if any member is anchored. Runtime Fracture::setAnchor(obj, chunk_index, anchored) can flip the anchor state live.</li>
<li><span style="font-weight: bold;">Damage API</span> —<br />
Fracture::applyRadialDamage(pos, radius, strength, impulse, type)<br />
applyContactDamage(a, b, contacts, n, type)<br />
breakNode(obj, node_index, dir, type)<br />
setAnchor(...)<br />
<br />
All accept a DAMAGE_TYPE enum (KINETIC, EXPLOSIVE, PIERCING, THERMAL, CUSTOM) scaled by a global damage_type_scale[5] table, so game code can do things like make grenades 1.5x stronger against concrete with a single tweak.</li>
<li><span style="font-weight: bold;">Multi-subscriber BreakEventCallback</span> — Fracture::addBreakEventCallback is void-return and never consumes the event. addBreakEventCallbackB is bool-return; returning true consumes the event and stops the chain. Legacy setBreakEventCallback is preserved as a single-slot wrapper.</li>
<li><span style="font-weight: bold;">Debris pool</span> — Fracture::setVfxPoolCap, setSfxVoicesCap, debrisActiveCount, plus low-energy sleep and debris-pool caps in updateDebrisHeuristics. Chunks at rest for debris_sleep_timeout seconds are put to sleep so PhysX stops simulating them.</li>
<li><span style="font-weight: bold;">Save / Load</span> — FractureMesh::save(path) and load(path) round-trip the whole asset (chunks, cooked PhysX convex hulls, cluster, and connection graph) through a versioned CC4('F','R','A','C') binary format.<br />
FractureMesh::adjustStorage(universal, physx, bullet) matches the PhysBody contract and allows the asset to be reloaded on a different backend. A cache (Cache&lt;FractureMesh&gt; FractureMeshes("Fracture Mesh")) is preserved so UID-addressed baked assets can live inside pak files like any other engine asset.<br />
<br />
[]<span style="font-weight: bold;">Instance sharing</span> — many Game::Fractured instances can point at the same FractureMesh. Geometry and cooked hulls are shared; only runtime state (damage, actor transforms, live joints) is per-instance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration:</span><br />
<br />
Zero changes to DestructMesh, Game::Destructible, or Tutorial 23.<br />
<br />
PhysX’s Physics.reportContact is <span style="font-weight: bold;">not</span> hijacked. The host app registers its own contact callback and can optionally call Fracture::applyContactDamage from inside it.<br />
<br />
Contact damage is PhysX-only, since Bullet does not deliver contact events in this engine. Radial damage works on both backends.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Multi-level hierarchical fracture (more than 2 levels deep, with per-level break thresholds and cluster-tree view-level LOD). The data model already supports it via Cluster::parent and Cluster::children; the runtime currently walks one level.</li>
<li>Runtime on-the-fly fracture (CSG against an unbaked mesh). Pre-bake only is the shipping path.</li>
<li>View-distance simulation LOD (far objects deferred until the player enters a bubble). The cluster proxy is the foundation; bubble activation logic is planned as a later follow-up.</li>
<li>Multi-platform cooked-hull round-trip. Saved files are currently PhysX-specific; cross-platform support needs adjustStorage(universal=true, ...) before save.</li>
<li>Titan Editor UI for anchor-volume painting, per-chunk material picking, and interactive fracture preview. This branch is intentionally code-only.</li>
<li>Auto-dispatch BreakPreset VFX/SFX from a material tag, so Tutorial 31’s 40-line OnBreak puff could collapse to 5 lines. The scaffolding is already in place (multi-subscriber callbacks, damage types), but the preset registry is deferred.<br />
</li></ul>
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Cloth Physics]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11813</link>
			<pubDate>Sat, 18 Apr 2026 12:56:10 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11813</guid>
			<description><![CDATA[Cloth Physics (XPBD)<br />
A first-party CPU cloth solver built into the engine at Engine/Source/Physics/Cloth.{h,cpp} + Engine/Source/Physics/Cloth Solver.{h,cpp}, sitting behind the existing Cloth / ClothMesh public API. Replaces NVIDIA's deprecated PhysX 3.x cloth (removed in PhysX 4+) with an XPBD (Extended Position-Based Dynamics) implementation — stable, deterministic, tunable, and owned entirely by the engine.<br />
<br />
Core idea — each cloth is a set of mass-weighted particles connected by constraints. Per simulate(dt) the solver substeps (default 8) Jakobsen-style: semi-implicit-Euler predict → XPBD Gauss-Seidel constraint solve (Δλ = (-C - α̃λ)/(w_i+w_j+α̃), α̃ = compliance/dt²) → collision projection against author-placed balls/capsules → derive velocity from position delta. Pinned particles (inv_mass == 0) follow skeleton bones each sub-step via smooth prev↔cur lerp so fast character motion doesn't yank the cape.<br />
<br />
<a href="https://postimg.cc/RW665C3M" target="_blank"><img src="https://i.postimg.cc/GmzkDHds/Screenshot-from-2026-04-18-08-13-11.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-13-11.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fYnjVzg" target="_blank"><img src="https://i.postimg.cc/x19vqKDP/Screenshot-from-2026-04-18-08-15-29.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-15-29.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See README.md for more info.]]></description>
			<content:encoded><![CDATA[Cloth Physics (XPBD)<br />
A first-party CPU cloth solver built into the engine at Engine/Source/Physics/Cloth.{h,cpp} + Engine/Source/Physics/Cloth Solver.{h,cpp}, sitting behind the existing Cloth / ClothMesh public API. Replaces NVIDIA's deprecated PhysX 3.x cloth (removed in PhysX 4+) with an XPBD (Extended Position-Based Dynamics) implementation — stable, deterministic, tunable, and owned entirely by the engine.<br />
<br />
Core idea — each cloth is a set of mass-weighted particles connected by constraints. Per simulate(dt) the solver substeps (default 8) Jakobsen-style: semi-implicit-Euler predict → XPBD Gauss-Seidel constraint solve (Δλ = (-C - α̃λ)/(w_i+w_j+α̃), α̃ = compliance/dt²) → collision projection against author-placed balls/capsules → derive velocity from position delta. Pinned particles (inv_mass == 0) follow skeleton bones each sub-step via smooth prev↔cur lerp so fast character motion doesn't yank the cape.<br />
<br />
<a href="https://postimg.cc/RW665C3M" target="_blank"><img src="https://i.postimg.cc/GmzkDHds/Screenshot-from-2026-04-18-08-13-11.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-13-11.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fYnjVzg" target="_blank"><img src="https://i.postimg.cc/x19vqKDP/Screenshot-from-2026-04-18-08-15-29.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-15-29.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See README.md for more info.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Behavior Trees, Animation Graph, IK]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11812</link>
			<pubDate>Sat, 18 Apr 2026 00:06:09 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11812</guid>
			<description><![CDATA[Behavior Trees (Bt*, animation-state-machine branch)<br />
A data-oriented behavior tree framework for AI decision-making, built into the engine at Engine/H/Game/AI/BehaviorTree.h + Engine/Source/Game/AI/BehaviorTree.cpp. Designed for attachment to Game::Chr subclasses: build a tree of BtNode objects once in Chr::create(), call bt.tick(Time.d()) from Chr::update() each frame.<br />
<br />
<a href="https://postimg.cc/ThH741M7" target="_blank"><img src="https://i.postimg.cc/TYTzh5k6/Screenshot-from-2026-04-17-19-48-04.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-48-04.png]" /></a><br />
<br />
<br />
Animation Graph (Ag*) and IK (animation-state-machine branch)<br />
A production-grade composable pose-graph animation system, built into the engine at Engine/H/Animation/AnimGraph.h + Engine/Source/Animation/AnimGraph.cpp. Sits on top of the existing AnimatedSkeleton::animate() primitives without modifying them, stylistically parallels the BehaviorTree framework on the same branch (Bt* prefix, namespace Game{}, non-virtual update() wrapper + protected doUpdate() dispatch, tree-walking virtuals for viz).<br />
<br />
<a href="https://postimg.cc/JthYxJgf" target="_blank"><img src="https://i.postimg.cc/QNgLXJqd/Screenshot-from-2026-04-17-19-49-22.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-49-22.png]" /></a><br />
<br />
IK On:<br />
<a href="https://postimg.cc/4n32Ptqk" target="_blank"><img src="https://i.postimg.cc/xjGr03PC/Screenshot-from-2026-04-17-16-02-10.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-10.png]" /></a><br />
<br />
IK Off:<br />
<a href="https://postimg.cc/XXv1LFmM" target="_blank"><img src="https://i.postimg.cc/y6hqVyTN/Screenshot-from-2026-04-17-16-02-24.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-24.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See the README.md for more info.]]></description>
			<content:encoded><![CDATA[Behavior Trees (Bt*, animation-state-machine branch)<br />
A data-oriented behavior tree framework for AI decision-making, built into the engine at Engine/H/Game/AI/BehaviorTree.h + Engine/Source/Game/AI/BehaviorTree.cpp. Designed for attachment to Game::Chr subclasses: build a tree of BtNode objects once in Chr::create(), call bt.tick(Time.d()) from Chr::update() each frame.<br />
<br />
<a href="https://postimg.cc/ThH741M7" target="_blank"><img src="https://i.postimg.cc/TYTzh5k6/Screenshot-from-2026-04-17-19-48-04.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-48-04.png]" /></a><br />
<br />
<br />
Animation Graph (Ag*) and IK (animation-state-machine branch)<br />
A production-grade composable pose-graph animation system, built into the engine at Engine/H/Animation/AnimGraph.h + Engine/Source/Animation/AnimGraph.cpp. Sits on top of the existing AnimatedSkeleton::animate() primitives without modifying them, stylistically parallels the BehaviorTree framework on the same branch (Bt* prefix, namespace Game{}, non-virtual update() wrapper + protected doUpdate() dispatch, tree-walking virtuals for viz).<br />
<br />
<a href="https://postimg.cc/JthYxJgf" target="_blank"><img src="https://i.postimg.cc/QNgLXJqd/Screenshot-from-2026-04-17-19-49-22.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-49-22.png]" /></a><br />
<br />
IK On:<br />
<a href="https://postimg.cc/4n32Ptqk" target="_blank"><img src="https://i.postimg.cc/xjGr03PC/Screenshot-from-2026-04-17-16-02-10.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-10.png]" /></a><br />
<br />
IK Off:<br />
<a href="https://postimg.cc/XXv1LFmM" target="_blank"><img src="https://i.postimg.cc/y6hqVyTN/Screenshot-from-2026-04-17-16-02-24.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-24.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See the README.md for more info.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Frame profiler and Input Action System]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11811</link>
			<pubDate>Fri, 17 Apr 2026 01:51:30 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11811</guid>
			<description><![CDATA[In-Engine Frame Profiler<br />
A lightweight CPU scope profiler with an on-screen stats overlay, built into the engine at Engine/H/Misc/Profiler.h + Engine/Source/Misc/Profiler.cpp. Works on all backends (DX11, DX12, Vulkan, GL). Zero overhead when disabled (~1 ns per PROFILE_SCOPE site -- one predictable branch on a cached bool).<br />
<br />
Tutorial_04_FrameProfiler (Tutorials/Source/04 - Graphics/Frame Profiler.cpp<br />
<br />
<a href="https://postimg.cc/216yymw6" target="_blank"><img src="https://i.postimg.cc/y8hZf83F/Screenshot-from-2026-04-15-12-49-24.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/RWFhh9R0" target="_blank"><img src="https://i.postimg.cc/rwGrhwR5/Screenshot-from-2026-04-15-12-49-34.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-34.png]" /></a><br />
<br />
<br />
Input Action System (rebindable, savable, stacked)<br />
An Unreal/Unity-style action abstraction over Esenthel's raw polling API, built into the engine at Engine/H/Input/Input Action.h + Engine/Source/Input/Input Action.cpp. 100% additive -- no changes to Kb/Ms/Joypads/Touches. Works on all backends and platforms.<br />
<br />
Tutorial_05_InputRebinding (Tutorials/Source/05 - Gui/20 - Input Rebinding.cpp)<br />
<br />
<a href="https://postimg.cc/xXC88YRf" target="_blank"><img src="https://i.postimg.cc/K87M983M/Screenshot-from-2026-04-16-21-41-02.png" border="0" alt="[Image: Screenshot-from-2026-04-16-21-41-02.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[In-Engine Frame Profiler<br />
A lightweight CPU scope profiler with an on-screen stats overlay, built into the engine at Engine/H/Misc/Profiler.h + Engine/Source/Misc/Profiler.cpp. Works on all backends (DX11, DX12, Vulkan, GL). Zero overhead when disabled (~1 ns per PROFILE_SCOPE site -- one predictable branch on a cached bool).<br />
<br />
Tutorial_04_FrameProfiler (Tutorials/Source/04 - Graphics/Frame Profiler.cpp<br />
<br />
<a href="https://postimg.cc/216yymw6" target="_blank"><img src="https://i.postimg.cc/y8hZf83F/Screenshot-from-2026-04-15-12-49-24.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/RWFhh9R0" target="_blank"><img src="https://i.postimg.cc/rwGrhwR5/Screenshot-from-2026-04-15-12-49-34.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-34.png]" /></a><br />
<br />
<br />
Input Action System (rebindable, savable, stacked)<br />
An Unreal/Unity-style action abstraction over Esenthel's raw polling API, built into the engine at Engine/H/Input/Input Action.h + Engine/Source/Input/Input Action.cpp. 100% additive -- no changes to Kb/Ms/Joypads/Touches. Works on all backends and platforms.<br />
<br />
Tutorial_05_InputRebinding (Tutorials/Source/05 - Gui/20 - Input Rebinding.cpp)<br />
<br />
<a href="https://postimg.cc/xXC88YRf" target="_blank"><img src="https://i.postimg.cc/K87M983M/Screenshot-from-2026-04-16-21-41-02.png" border="0" alt="[Image: Screenshot-from-2026-04-16-21-41-02.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[DirectX 12 Ultimate rendering backend]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11810</link>
			<pubDate>Tue, 14 Apr 2026 12:12:36 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11810</guid>
			<description><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
DX12 seems working nicely on Windows 11 nVidia A2000 GPU.<br />
<br />
Lines added (by Claude Code) 11,566<br />
<br />
<a href="https://postimages.org/" target="_blank"><img src="https://i.postimg.cc/jSgX3ZwX/dx12meshshaders.png" border="0" alt="[Image: dx12meshshaders.png]" /></a><br />
<br />
See README.md for info on the added DX12 specific tutorials.]]></description>
			<content:encoded><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
DX12 seems working nicely on Windows 11 nVidia A2000 GPU.<br />
<br />
Lines added (by Claude Code) 11,566<br />
<br />
<a href="https://postimages.org/" target="_blank"><img src="https://i.postimg.cc/jSgX3ZwX/dx12meshshaders.png" border="0" alt="[Image: dx12meshshaders.png]" /></a><br />
<br />
See README.md for info on the added DX12 specific tutorials.]]></content:encoded>
		</item>
	</channel>
</rss>