Skip to content

Add Sky Rendering Events#5328

Open
lowercasebtw wants to merge 20 commits into
FabricMC:26.1.2from
lowercasebtw:26.1.2
Open

Add Sky Rendering Events#5328
lowercasebtw wants to merge 20 commits into
FabricMC:26.1.2from
lowercasebtw:26.1.2

Conversation

@lowercasebtw

@lowercasebtw lowercasebtw commented Apr 15, 2026

Copy link
Copy Markdown

This will be my first contribution to Fabric API. Had to rework some events from my original to work w/ fabric-api's ecosystem and how it handles events.

Events for each portion of the sky rendering, with corresponding important data accessible during those phases. Allowing cancelling of rendering such as the stars/sun/moon.

Also adds a custom element even in which although unused by FAPI, is intended for mod developers to invoke when adding custom elements to the sky when rendering allowing other mods to intercept them and do as please.

Progress:
Everything is implemented, only needs testing & javadoc work now.

@sylv256 sylv256 added enhancement New feature or request area: rendering labels Apr 15, 2026
@lowercasebtw lowercasebtw marked this pull request as ready for review April 15, 2026 19:39
@lowercasebtw lowercasebtw changed the title DRAFT: Add Sky Rendering Events Add Sky Rendering Events Apr 15, 2026
@CallMeEchoCodes

Copy link
Copy Markdown
Contributor

Seems good to me, but I think it might be useful to add an extraction callback like LevelRenderEvents has. I'd also probably pass CameraRenderState to the SkyRenderContext.

@lowercasebtw

Copy link
Copy Markdown
Author

Seems good to me, but I think it might be useful to add an extraction callback like LevelRenderEvents has. I'd also probably pass CameraRenderState to the SkyRenderContext.

Done

@CallMeEchoCodes CallMeEchoCodes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, Although unsure about impl details with extraction

private void setupContext(ClientLevel level, float partialTicks, Camera camera, SkyRenderState skyRenderState, CallbackInfo ci) {
celestialContext.prepare((SkyRenderer) (Object) this, skyRenderState);
final CameraRenderState cameraRenderState = new CameraRenderState();
camera.extractRenderState(cameraRenderState, partialTicks);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iffy on extracting the camera a second time (although I don't think it will cause any issues)

Is there some way to pass this down properly?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance it should be possible to put a ScopedValue or ThreadLocal in LevelRenderer::addSkyPass, I'd probably wrap that whole method and add a ScopedValue with the CameraRenderState

@CallMeEchoCodes CallMeEchoCodes Apr 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still approved as this can be changed later without breaking api

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is in SkyRenderer, how would you want me to pass it there?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be here. This would be easier LevelRendererMixin. Also, SkyRenderContext could extend AbstractLevelRenderContext, and then you only need to provide skyRenderer. This also means that we don't need a SkyRenderContextImpl and instead just add it to the existing LevelRenderContextImpl.

So basically, add skyRenderer to LevelRenderContextImpl, make it implement SkyRenderContext extends AbstractLevelRenderContext, and set levelRenderContext.skyRenderer in beforeRender.

@modmuss50 modmuss50 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a testmod as well. This may be a good canidate for auto client tests.

Im not the best at saying if these events are what we need or not, hopefully someone more familar with rendering can chime in.


@Inject(method = "extractRenderState", at = @At("TAIL"))
private void afterExtractSky(ClientLevel level, float partialTicks, Camera camera, SkyRenderState state, CallbackInfo ci) {
SkyRenderEvents.END_EXTRACTION.invoker().execute(new SkyExtractionContextImpl(level, camera, state, partialTicks));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should they be "render" events if they happen during extraction?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LevelRenderEvents has END_EXTRACTION so I matched that

Comment on lines +178 to +179
final boolean cancelled = SkyRenderEvents.PRE_SKY.invoker().execute(skyRenderContext);
if (!cancelled) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesnt build, wont pass checkstyle (and many other places)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkstyle?

Comment on lines +21 to +22
@ApiStatus.NonExtendable
public interface CelestialRenderContext extends SkyRenderContext {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs docs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do sometime soon

@modmuss50

Copy link
Copy Markdown
Member
 java.lang.NullPointerException: Cannot invoke "net.fabricmc.fabric.impl.client.rendering.level.LevelExtractionContextImpl.prepare(net.minecraft.client.renderer.GameRenderer, net.minecraft.client.renderer.LevelRenderer, net.minecraft.client.renderer.state.level.LevelRenderState, net.minecraft.client.multiplayer.ClientLevel, net.minecraft.client.DeltaTracker, net.minecraft.client.Camera)" because "this.extractionContext" is null

Client crashes as well.

@lowercasebtw

Copy link
Copy Markdown
Author
 java.lang.NullPointerException: Cannot invoke "net.fabricmc.fabric.impl.client.rendering.level.LevelExtractionContextImpl.prepare(net.minecraft.client.renderer.GameRenderer, net.minecraft.client.renderer.LevelRenderer, net.minecraft.client.renderer.state.level.LevelRenderState, net.minecraft.client.multiplayer.ClientLevel, net.minecraft.client.DeltaTracker, net.minecraft.client.Camera)" because "this.extractionContext" is null

Client crashes as well.

Hmm will check out

@kevinthegreat1 kevinthegreat1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I don't think many of the events are needed, we are still discussing that. Meanwhile, I have commented on some implementation things that I think should be done regardless of which approach we take in the end.

private void setupContext(ClientLevel level, float partialTicks, Camera camera, SkyRenderState skyRenderState, CallbackInfo ci) {
celestialContext.prepare((SkyRenderer) (Object) this, skyRenderState);
final CameraRenderState cameraRenderState = new CameraRenderState();
camera.extractRenderState(cameraRenderState, partialTicks);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be here. This would be easier LevelRendererMixin. Also, SkyRenderContext could extend AbstractLevelRenderContext, and then you only need to provide skyRenderer. This also means that we don't need a SkyRenderContextImpl and instead just add it to the existing LevelRenderContextImpl.

So basically, add skyRenderer to LevelRenderContextImpl, make it implement SkyRenderContext extends AbstractLevelRenderContext, and set levelRenderContext.skyRenderer in beforeRender.

* <p>To attach modded data to vanilla render states, see {@link net.fabricmc.fabric.api.client.rendering.v1.FabricRenderState FabricRenderState}.
* Only attach the minimum data needed for rendering. Do not attach objects that are not thread-safe such as {@link net.minecraft.client.multiplayer.ClientLevel}.
*/
public static final Event<EndExtraction> END_EXTRACTION = EventFactory.createArrayBacked(EndExtraction.class, callbacks -> context -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, use LevelRenderEvents.END_EXTRACTION instead. We intentionally only added one extraction event.

@FlashyReese FlashyReese left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good direction. Proper sky render events would be useful for mods that modify vanilla sky rendering and mods that fully replace or layer custom skies.

My main concern is that the API currently feels split between vanilla sky element interception and a broader custom sky render hook. For full custom sky renderers, the important part is having one clear sky render phase with documented render-state guarantees, otherwise mods may still need their own LevelRenderer/SkyRenderer mixins.

The things I think should be clarified before this becomes API:

  • should cancellable PRE events still call every listener before returning the final cancel result?
  • should all sky events share one context source/lifecycle?
  • what render state is guaranteed during PRE_SKY?
  • should PRE_CUSTOM_ELEMENT / POST_CUSTOM_ELEMENT be typed, or left out for now?

I am mostly looking at this from the perspective of custom sky rendering where mods need to stack or replace skies, not only cancel vanilla sun/moon/stars.

*/
public static final Event<PreSky> PRE_SKY = EventFactory.createArrayBacked(PreSky.class, callbacks -> context -> {
for (final PreSky callback : callbacks) {
if (callback.execute(context)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this call every callback and OR the cancellation result instead of returning immediately?

If one mod cancels here, later listeners never see the event at all. That seems rough for compatibility between multiple sky mods.

import net.minecraft.client.renderer.state.level.SkyRenderState;

@ApiStatus.NonExtendable
public interface SkyRenderContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this extend AbstractLevelRenderContext?

Sky rendering is still part of the level render frame, so access to levelRenderer/gameRenderer/levelState seems useful and keeps this consistent with the rest of the rendering API.

/**
* Called at the start of the "addSkyPass" lambda.
*/
public static final Event<PreSky> PRE_SKY = EventFactory.createArrayBacked(PreSky.class, callbacks -> context -> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the event custom sky renderers would depend on the most.

Can the javadocs define what render state is guaranteed here? Mainly fog state, matrix/projection expectations, and whether returning true skips all vanilla sky rendering.


@FunctionalInterface
public interface PreCustomElement {
boolean execute(Identifier key, Object context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about Object context here.

If Fabric does not invoke this itself and the context is untyped, it seems hard for mods to rely on. I think this should either be typed more strongly or left out for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: rendering enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants