Repository navigation
Draw the end portal and end gateway surfaces - #68
Conversation
End portals and end gateways had no surface: the layer was dropped before capture, so the player saw straight through the frame. Vanilla draws both with position-only vertices and a shader that sums up to sixteen scaled, rotated and tinted layers of end_portal.png over end_sky.png in screen space. RenderLayerUtils now maps the two shader programs to texture ids of their own, and EndPortalSurface bakes the same sum once, on a worker thread, into a 1024-texel tile laid over eight blocks. Each layer's scale and rotation is rounded to an integer lattice step over the tile so the tile repeats without a seam; no layer moves by more than a third of its scale or 15 degrees. The bake reads the textures the resource packs supply and is redone on reload. Vanilla's slow drift with the time of day is not reproduced. The capturing consumer gives those quads texture coordinates from their position on the quad's own axis plane, offset by the block the provider was told about, so neighbouring portal blocks continue one texture. The tile is the material's albedo and its emission, at an intensity of 10 rather than the entity default of 100, which turned the whole surface pale blue under a dark scene's exposure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Junyu Tao <jimmyfa@jimmyfa.xyz>
sambvfx
left a comment
There was a problem hiding this comment.
Thanks for this, Jimmy. The bake is a neat way around a shader that only works in screen space, and rounding the layer lattices so the tile stays seamless is clever and well explained.
A few things before merge:
- Material look: the tile is the albedo as well as the emission, so the scene lights it. In your noon shot it reads as a grey slab, and at night as bright cyan, where vanilla stays a dark void with specks. Could you try a black albedo with the tile only as emission, retune
EMISSIVE_INTENSITY, and add a vanilla/rtx offshot from the same spot to compare? - Surface mode: the tile is opaque everywhere, so mapping the two programs to
OPAQUErather thanCUTOUTskips the alpha test. Heads-up: #66 also changesclassifySurfaceMode, so whichever lands second will need a small rebase. - Docs: a sentence in
docs/RESOURCE_PACKS.mdunder "Entities And Runtime Atlases" would help pack authors. It should say the portal and gateway are baked fromend_portal.pngandend_sky.png, take no sidecars, and emit at 10.
Smaller things are inline. None of them block.
| textures.destroy(); | ||
| return null; | ||
| } | ||
| textures.emissive = textures.albedo; |
There was a problem hiding this comment.
This is what makes the surface pick up sunlight. Vanilla draws it unlit, so I think a black or dimmed albedo with the tile as emission only would match better across day and night.
| materialBuilder.emissiveTexture(emissive.getHandle()) | ||
| .emissiveIntensity(EMISSIVE_INTENSITY); | ||
| .emissiveIntensity(EndPortalSurface.isSurfaceTexture(key.textureId()) | ||
| ? EndPortalSurface.EMISSIVE_INTENSITY : EMISSIVE_INTENSITY); |
There was a problem hiding this comment.
Small one: could EntityTextureSet carry its own emissive intensity, set in createEndPortalTextureSet? That keeps the texture-id check out of createMaterial.
| RenderLayer.MultiPhaseParameters phases = multiPhase.getPhases(); | ||
| // The end portal and gateway programs combine two textures in screen space | ||
| // and their vertices carry position only; they draw with a baked texture. | ||
| if (phases.program == RenderPhase.END_PORTAL_PROGRAM) { |
There was a problem hiding this comment.
These layers end up CUTOUT, so the material is alpha-tested even though the tile is fully opaque. OPAQUE would avoid that.
| * on worker threads. A reload drops them all, so a bake that finishes for the | ||
| * previous packs is never read. | ||
| */ | ||
| private final Map<Identifier, CompletableFuture<byte[]>> bakes = new HashMap<>(); |
There was a problem hiding this comment.
reloadMaterials() clears this map and textureSets together, so the finished pixels are never reused once uploaded. Dropping the entry after poll returns them saves about 8 MiB of heap.
| * <p>For the tile to repeat without a seam, every layer must itself repeat a whole | ||
| * number of times across it. Vanilla's layers are rotated by arbitrary angles, so | ||
| * each layer's scale and rotation is rounded to the nearest integer lattice step | ||
| * over the tile; a layer of a random speckle texture that comes out a few percent |
There was a problem hiding this comment.
"A few percent" undersells layer 15. It rotates by a whole number of turns, and its 1.5 scale rounds to (2, 0), which is 33% larger. Layers 9, 11 and 13 are around 10%. Maybe just say "up to a third", to match the test.
| return (unorm(r) << 16) | (unorm(g) << 8) | unorm(b); | ||
| } | ||
|
|
||
| private static int sample(Image image, double u, double v) { |
There was a problem hiding this comment.
Optional: layer 1 repeats every ~120 tile texels against the 256-px texture, so nearest sampling skips a lot of single-texel specks. A 2×2 supersample per output texel would keep more of them.
| */ | ||
| static int[] layerLattice(int layer) { | ||
| double scale = (4.5 - layer / 4.0) * SCREENS_PER_TILE; | ||
| double angle = Math.toRadians((layer * layer * 4321.0 + layer * 9.0) * 2.0); |
There was a problem hiding this comment.
Optional: reducing (l*l*4321 + 9*l) * 2 mod 360 in integer math before toRadians keeps the rounding exact, rather than relying on cos of a ~2M-degree angle.
| /** | ||
| * Sets the block whose local space the next captures are in. Only surfaces whose | ||
| * texture coordinates are projected from position read it, so neighbouring end | ||
| * portal blocks continue one texture; a capture that never sets it projects as if |
There was a problem hiding this comment.
The origin isn't reset in beginEntity(), so a capture that never calls this gets the previous block entity's origin, not (0, 0, 0). Zeroing it there would make this comment true.
| void everyLayerLatticeStaysCloseToVanillasScaleAndRotation() { | ||
| for (int layer = 1; layer <= EndPortalSurface.GATEWAY_LAYERS; layer++) { | ||
| int[] lattice = EndPortalSurface.layerLattice(layer); | ||
| double scale = (4.5 - layer / 4.0) * EndPortalSurface.SCREENS_PER_TILE; |
There was a problem hiding this comment.
This recomputes the same formula as layerLattice, so a misread of the shader would still pass. Could you pin a few literal lattices instead, for example layer 1 → (8, 3), layer 2 → (6, 6), layer 15 → (2, 0)?
| assertEquals(4 * 4 * 4, rgba.length); | ||
| for (int i = 0; i < rgba.length; i += 4) { | ||
| assertEquals(82, rgba[i] & 0xFF); | ||
| assertEquals(150, rgba[i + 1] & 0xFF); |
There was a problem hiding this comment.
Tiny: blue isn't asserted.
The baked tile was the surfaces' albedo as well as their emission, so scene light lit them: a grey slab at noon and bright cyan at night, where vanilla draws a dark field with specks. The albedo is now one black texel and the tile emission only, at an intensity of 3, which keeps the night field about as dark against lit stone as vanilla's. Their roughness is 1, because at 0.7 the black surface still mirrored the noon sky as a grey sheen. Noon exposure still hides the specks. Both programs now map to OPAQUE: the tile is opaque everywhere, so the cutout alpha test only cost a texture read per hit. EntityTextureSet carries its emissive intensity and fallback roughness, so createMaterial no longer checks for the portal texture ids. EndPortalSurface forgets a finished bake once it hands the tile over, which frees about 8 MiB of heap, and reduces each layer's angle mod 360 in integer math before converting it. beginEntity() zeroes the block origin, so a capture that never sets one projects from the origin as documented. The lattice test pins hand-computed steps instead of recomputing the formula, and the bake test checks blue. RESOURCE_PACKS.md describes the bake for pack authors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Junyu Tao <jimmyfa@jimmyfa.xyz>
At four blocks a screen the specks came out a small fraction of vanilla's size and read as tinted noise. Vanilla's pattern spans one screen whatever the distance, so no world scale matches it everywhere. The tile now repeats every 48 blocks, 24 blocks a screen, which matches vanilla's speck size from about eight blocks away. Vanilla normalises screen x and y separately, so its specks are 16:9 wide; vertical faces repeat every 27 blocks upward to match, and horizontal faces stay square. The tile grows to 2048 texels so that layer 1's single-texel specks still land on texels of their own. Opposite faces projected the same axes the same way round, so adjacent faces of an end gateway were mirror images folded over their shared edge. Every face now runs U right and V up seen from outside, chosen by which side of the block's middle the face sits on. A black dielectric still reflected a few percent of the noon sky, which read as a grey slab. The surface is now a black metal, whose specular colour is its albedo, at full roughness; EntityTextureSet carries the fallback metalness. Emission rises to 4, where the night field is about as dark as vanilla's. After Remix's exposure and tone mapping the dark teal came out markedly more saturated than vanilla's, so the baked texels are pulled 30% toward their own luma. Half matched vanilla's saturation but turned the brightest specks white. RESOURCE_PACKS.md gives the new material. Tests cover the desaturation and one handedness around a cube's sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Junyu Tao <jimmyfa@jimmyfa.xyz>
Thanks for the careful review. Everything is addressed in two commits on top: 947f5f6 for the points you raised, and a second one after comparing against vanilla shots, which changed the scale and the material again. The description has the new three-column table with a vanilla
What changed beyond your points: at the old 8-block tile the specks were a small fraction of vanilla's size and read as noise, so the tile now repeats every 48 blocks (27 upward on vertical faces, for vanilla's 16:9 speck shape), which matches vanilla's speck size from about eight blocks away. Every face now has the same handedness seen from outside, so a gateway's sides no longer mirror each other across their edges. What still differs: everything passes through Remix's exposure and tone mapping, so the brightest specks are a little brighter and more saturated than vanilla's and the specks fade at noon; the pattern is fixed to the world, so it matches vanilla's scale at one distance only and does not drift. I would like to close that in a follow-up PR by drawing the surfaces with vanilla's own shader over the composited image after tone mapping, the way rain and snow are drawn against the interop depth, while keeping a dark surface in the path-traced scene for reflections, together with portal and gateway lights. |
sambvfx
left a comment
There was a problem hiding this comment.
Thanks, Jimmy. This is a really careful second pass. The black metal at full roughness is a neat answer to the grey sheen, and the comparison table with the vanilla column makes the result easy to judge. I checked each point against the new commits:
- The albedo is a black texel with the tile as emission only. Intensity, roughness and metalness come from
EntityTextureSet, socreateMaterialno longer checks the texture id. surfaceMode()returnsOPAQUEfor both programs ahead ofclassifySurfaceMode. It merges cleanly with #66.polldrops the entry, andbeginEntityzeroes the origin.- The angle is reduced mod 360 in integer math, and the class comment gives the real worst cases.
- The literal lattices, including layer 13's negative rounding, and blue are now asserted.
- The
RESOURCE_PACKS.mdparagraph is in.
I merged it with current main locally. The end portal, capturing consumer and render layer tests pass, and so does the Fabric suite. CI is green too: the forge job's earlier failure was a Maven download, and it passed on rerun.
I also checked it in game on top of main, at noon, at night and after an F3+T resource reload. Both surfaces look right and come back after the reload, with nothing in the log.
For the record: a real-size bake now takes about 0.9 s per surface here, against about 0.2 s at 1024, and each tile is 16 MB. It runs once, off the render thread, so I'm fine with it for the extra speck detail.
On drawing the surfaces with vanilla's shader over the tone-mapped image: it's an interesting idea, but it interacts with upscaling, frame generation and the reflections, so please open an issue to talk it through before starting on a PR.
Approving. Thanks again.
Brings in the end portal and gateway surfaces (NVIDIAGameWorks#68) and the lighting profile upgrade (NVIDIAGameWorks#57). NVIDIAGameWorks#68's code uses Mojang names: the end portal and gateway programs are RenderStateShard.RENDERTYPE_END_PORTAL_SHADER and RENDERTYPE_END_GATEWAY_SHADER, compared against CompositeState.shaderState, the bake reads textures through Resource.open and NativeImage.getPixelRGBA, and its tests draw with addVertex and setUv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Alex Melan <59925151+AlexMelanFromRingo@users.noreply.github.com>
Brings in the end portal and gateway surfaces (NVIDIAGameWorks#68) and the lighting profile upgrade (NVIDIAGameWorks#57) through NVIDIAGameWorks#78. On 26.3 the surfaces are recognised by RenderPipelines.END_PORTAL and END_GATEWAY, which need no access widener; the bake reads end_portal.png from its 26.x folder, textures/entity/end_portal/, and takes NativeImage.getPixel's ARGB. The payload history records the 26.3 minecraft-logic README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Alex Melan <59925151+AlexMelanFromRingo@users.noreply.github.com>
End portals and end gateways had no surface: their render layer was dropped before capture, so the player saw straight through the frame to whatever was underneath.
Vanilla draws both with position-only vertices and
rendertype_end_portal.fsh, which sums up to sixteen scaled, rotated and tinted layers ofend_portal.pngoverend_sky.pngin screen space. A path-traced hit has no screen position to project from, so this bakes the same sum into a texture laid over the world.Layer.
RenderLayerUtilsmapsEND_PORTAL_PROGRAMandEND_GATEWAY_PROGRAM(newly access-widened) to texture ids of their own,rtxremix:end_portal_surfaceandrtxremix:end_gateway_surface(15 and 16 layers, as in the two shader JSONs).surfaceMode()returnsOPAQUEfor both beforeclassifySurfaceModeruns, since the tile is opaque everywhere and an alpha test would only cost a texture read; noSurfaceModeis added.Bake.
EndPortalSurfaceevaluates the shader's sum once per surface, on a worker thread, into a 2048-texel tile (16 MiB per surface) that spans two of vanilla's screen widths. For the tile to repeat without a seam, every layer has to repeat a whole number of times across it, so each layer's scale and rotation is rounded to the nearest integer lattice step. That moves a layer by up to a third of its scale (layer 15) or 14 degrees (layer 14), which is invisible in a field of random specks. The bake reads the textures the resource packs supply and is redone on resource reload; the finished pixels are dropped once uploaded. Vanilla's slow drift with the time of day is not reproduced; the tile is the pattern at time zero.Texture coordinates. Vanilla sends none.
RemixCapturingConsumergives these quads coordinates from their position on the quad's own axis plane, offset by the blockRemixBlockEntityPasspasses to the provider, so the nine blocks of a portal continue one texture rather than each repeating it. The tile repeats every 48 blocks (24 blocks a screen, which matches vanilla's speck size from about eight blocks away). Vanilla normalises screen x and y separately, so its specks are 16:9 wide; vertical faces therefore repeat every 27 blocks upward, and horizontal faces stay square. Every face runs U right and V up seen from outside, so the faces of a gateway read as one pattern turned toward the viewer rather than mirror images folded over their edges. The coordinates are part of the geometry hash, so blocks at different places in the tile get distinct meshes: one per loaded portal block, nine for a 3x3 portal.Material. Vanilla draws the surface unlit, so the albedo is a single black texel and the tile is emission only, at an intensity of 4. The surface is a black metal at full roughness: a black dielectric still reflected a few percent of the noon sky and read as a grey slab, while a metal's specular colour is its albedo.
EntityTextureSetcarries the emissive intensity, fallback roughness and fallback metalness, socreateMaterialhas no portal check. The baked texels are pulled 30% toward their own luma, because after Remix's exposure and tone mapping the dark teal came out markedly more saturated than vanilla's; at 50% the saturation matched vanilla but the brightest specks turned white.docs/RESOURCE_PACKS.mddescribes the bake for pack authors.Verification
.\gradlew.bat test: 808 tests pass, including newEndPortalSurfaceTest(hand-computed lattices for layers 1, 2, 13 and 15, every layer within the bounds above, a seamless repeat, the summed colour and opacity, the desaturation) andRemixCapturingConsumerTestcases for projected coordinates (plane choice, continuity across blocks in both directions, one handedness around a cube's sides, reset onreinit).lowpreset, 1600x900, flat test platform with a 3x3 end portal in its frame and an end gateway on obsidian). The images are the mod's own still-shot captures, so every column has the same poses; the vanilla column is the same build with path tracing off.mainthe portal shows the platform underneath and the gateway is empty.main/rtx off)Known differences
lowpreset and bloom at night; not isolated.Not in this change. Vanilla's end portal and end gateway are light level 15; neither emits a Remix light here. A follow-up PR plans to draw the surfaces with vanilla's own shader over the composited image after tone mapping, the way
RemixWeatherRendererdraws rain and snow against Remix's interop depth, while keeping a dark surface in the path-traced scene for reflections, and to add portal and gateway lights. That would match vanilla exactly in the primary view.🤖 Generated with Claude Code