1717import zmaster587 .advancedRocketry .dimension .DimensionManager ;
1818import zmaster587 .advancedRocketry .dimension .DimensionProperties ;
1919import zmaster587 .advancedRocketry .network .PacketSystemBodiesSync ;
20+ import zmaster587 .advancedRocketry .space .HyperspaceWorld ;
2021import zmaster587 .advancedRocketry .universe .SystemBodyKind ;
2122
2223import java .util .List ;
2324
2425/**
25- * Slot-world sky renderer for a settled tier-2 ship: draws the descent boundary ring plus a
26- * billboard for every nearby system body the server has synced for THIS slot dimension.
26+ * Slot-world sky renderer for a settled tier-2 ship: draws a billboard for every nearby system body
27+ * the server has synced for THIS slot dimension.
28+ *
29+ * <p><b>There is no fixed horizon band here, and there must not be one.</b> This renderer used to
30+ * draw a "descent boundary ring" at a constant radius in the CAMERA's frame — a band the viewpoint
31+ * could never leave, identical at ten blocks from a planet and at ten million, with no coupling to
32+ * the descent radius it was named after. It asserted "a descent boundary is here" in every cell,
33+ * including cells with nothing to descend to. A boundary belongs to a BODY: it is drawn around the
34+ * body's own bearing, at the angle that body's shell actually subtends.</p>
2735 *
2836 * <p>The body data comes from {@link PacketSystemBodiesSync#bodiesForDim(int)} (the shared
2937 * server->client render channel), keyed on {@code world.provider.getDimension()}. Bodies flagged
3038 * {@link PacketSystemBodiesSync.RenderBody#descendTarget} are highlighted so the pilot can see which
3139 * body the ship will descend into once inside its proximity radius.</p>
3240 *
3341 * <p>This provider replaces the ENTIRE sky rather than adding to one, so whatever is not drawn here is
34- * not drawn at all -- hence the starfield alongside the ring and the billboards.</p>
42+ * not drawn at all -- hence the starfield alongside the billboards.</p>
3543 *
3644 * <p>Everything emitted here is wound to face the camera and drawn with vanilla's back-face culling
3745 * left on, matching {@link RenderPlanetarySky}. That is a hard requirement, not a style choice: the sky
4654public class BoundarySky extends IRenderHandler {
4755
4856 // Render tunables (appearance-only; never pinned by a test).
49- private static final float BOUNDARY_RADIUS = 100.0F ;
50- private static final float BOUNDARY_HEIGHT = 6.0F ;
51- private static final int BOUNDARY_SEGMENTS = 48 ;
5257 private static final float BODY_DISTANCE = 90.0F ;
58+ /** Sky-frame radius the boundary circle is emitted on. Inside the starfield, around the body. */
59+ private static final float BOUNDARY_SKY_RADIUS = 95.0F ;
60+ /** Samples around one boundary circle. Enough that a great circle does not read as a polygon. */
61+ private static final int BOUNDARY_SEGMENTS = 64 ;
5362 /** Sky-frame scale the label text is drawn at, so it reads at the billboard's distance. */
5463 private static final float LABEL_SCALE = 0.28F ;
5564
@@ -64,52 +73,32 @@ public class BoundarySky extends IRenderHandler {
6473 public static volatile int labelsDrawnLastFrame ;
6574
6675 /**
67- * Frames on which the descent-boundary ring has been drawn. It exists so "the ring is suppressed
68- * in hyperspace" is a statement a test can falsify: a counter that only ever goes up cannot tell
69- * a suppressed ring from a sky renderer that stopped running altogether, so the corridor's own
70- * counter is read in the same breath and the pair has to move in opposite directions.
76+ * How many atmosphere boundaries the client's LAST FRAME drew. Same shape as the label counter
77+ * and for the same reason: "a boundary is drawn for a descend target and for nothing else" is
78+ * a claim a test can only take without looking at pixels if the renderer counts what it drew.
79+ *
80+ * <p>Read it beside {@link #skyFramesDrawn}, never alone — a zero here means "no boundary was
81+ * drawn" only if the renderer ran at all, and the two are separate questions.</p>
7182 */
72- public static volatile long ringFramesDrawn = 0L ;
83+ public static volatile int boundariesDrawnLastFrame ;
7384
7485 /**
7586 * Frames on which this sky renderer ran AT ALL, counted before any branch inside it.
7687 *
77- * <p>Without it the ring counter answers two different questions with the same zero: "the ring
78- * was suppressed" and "nothing rendered here". The first control leg written against that pair
79- * could not tell them apart, and said so by failing on its own arrangement.</p>
88+ * <p>It is what makes "X was not drawn" falsifiable: a per-feature counter that stays at zero
89+ * cannot tell "the feature was suppressed" from "nothing rendered here", and the first control
90+ * leg written without this pair could not tell them apart and said so by failing on its own
91+ * arrangement.</p>
8092 */
8193 public static volatile long skyFramesDrawn = 0L ;
8294
8395 private final Minecraft mc = Minecraft .getMinecraft ();
8496
85- // Cached static geometry: the descent boundary ring (position-only; colour set at call time).
86- private final int glBoundaryList ;
8797 // Cached static geometry: the shared starfield, so empty space is not a black void.
8898 private final int glStarList ;
8999
90100 public BoundarySky () {
91- int lists = GLAllocation .generateDisplayLists (2 );
92- this .glBoundaryList = lists ;
93- this .glStarList = lists + 1 ;
94-
95- BufferBuilder buffer = Tessellator .getInstance ().getBuffer ();
96- GL11 .glNewList (this .glBoundaryList , GL11 .GL_COMPILE );
97- buffer .begin (GL11 .GL_QUAD_STRIP , DefaultVertexFormats .POSITION );
98- for (int i = 0 ; i <= BOUNDARY_SEGMENTS ; i ++) {
99- double ang = (Math .PI * 2.0D * i ) / BOUNDARY_SEGMENTS ;
100- float x = (float ) (Math .cos (ang ) * BOUNDARY_RADIUS );
101- float z = (float ) (Math .sin (ang ) * BOUNDARY_RADIUS );
102- // TOP vertex before BOTTOM. The strip advances anticlockwise around +Y, so this pairing is
103- // what makes each quad wind anticlockwise -- i.e. front-facing -- as seen from INSIDE the
104- // cylinder. The camera is always inside it: the ring is drawn in the camera-centred sky
105- // frame, at a radius no viewpoint can leave. Emitting bottom-first faces the ring outwards
106- // and vanilla's GL_CULL_FACE/GL_BACK (enabled in EntityRenderer.renderWorldPass just before
107- // the sky pass, and never turned off here) discards every quad of it.
108- buffer .pos (x , BOUNDARY_HEIGHT , z ).endVertex ();
109- buffer .pos (x , -BOUNDARY_HEIGHT , z ).endVertex ();
110- }
111- Tessellator .getInstance ().draw ();
112- GL11 .glEndList ();
101+ this .glStarList = GLAllocation .generateDisplayLists (1 );
113102
114103 // The starfield is the mod's existing one, compiled into a list of our own rather than
115104 // duplicated: same seed, same 2000 quads, same radius as every other AR sky. Without it a slot
@@ -135,7 +124,7 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) {
135124
136125 GlStateManager .disableTexture2D ();
137126
138- // Stars first: the ring and the billboards are meant to sit in front of them.
127+ // Stars first: the billboards are meant to sit in front of them.
139128 GlStateManager .color (1.0F , 1.0F , 1.0F , STAR_ALPHA );
140129 GL11 .glCallList (this .glStarList );
141130
@@ -155,16 +144,24 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) {
155144 return ;
156145 }
157146
158- // Descent boundary ring (untextured colour band).
159- GlStateManager .color (0.35F , 0.65F , 1.0F , 0.35F );
160- GL11 .glCallList (this .glBoundaryList );
161- ringFramesDrawn ++;
147+ // The atmosphere boundaries FIRST, untextured, while the texture unit is still off: they
148+ // belong behind the bodies they surround, and drawing them here saves toggling the texture
149+ // unit twice per frame.
150+ int boundaries = 0 ;
151+ BufferBuilder buffer = Tessellator .getInstance ().getBuffer ();
152+ if (bodies != null ) {
153+ GlStateManager .color (0.35F , 0.65F , 1.0F , 0.55F );
154+ for (PacketSystemBodiesSync .RenderBody body : bodies ) {
155+ boundaries += drawBoundary (buffer , body ) ? 1 : 0 ;
156+ }
157+ }
158+ boundariesDrawnLastFrame = boundaries ;
159+
162160 GlStateManager .enableTexture2D ();
163161
164162 // One billboard per synced body.
165163 int labelled = 0 ;
166164 if (bodies != null && !bodies .isEmpty ()) {
167- BufferBuilder buffer = Tessellator .getInstance ().getBuffer ();
168165 boolean labels = SkyLabels .enabled ();
169166 for (PacketSystemBodiesSync .RenderBody body : bodies ) {
170167 labelled += drawBody (buffer , body , labels ) ? 1 : 0 ;
@@ -175,6 +172,78 @@ public void render(float partialTicks, WorldClient world, Minecraft mc) {
175172 restoreState ();
176173 }
177174
175+ /**
176+ * Draw {@code body}'s atmosphere boundary: the circle on the sky where its shell meets the
177+ * viewer's line of sight. Returns whether anything was emitted.
178+ *
179+ * <p><b>Why a circle around the body and not a band around the camera.</b> The boundary is a
180+ * sphere of radius R about the body, so from a distance d it subtends a half-angle
181+ * {@code asin(R/d)} about the body's own bearing. That makes it a thing in the world: it OPENS
182+ * as the ship closes, and at the crossing it is a great circle — the boundary is all around
183+ * you, because you are on it. A fixed band at the camera's horizon can express none of that; it
184+ * is identical at every distance, which is why the one this replaced said nothing.</p>
185+ *
186+ * <p><b>No singularity, deliberately.</b> Points are sampled ON the sky sphere as
187+ * {@code cosθ·n + sinθ·(cosφ·u + sinφ·v)} rather than projected onto the billboard plane, where
188+ * the radius would go as {@code tan θ} and blow up exactly at the crossing. The ratio is
189+ * clamped at 1, so at or inside the shell θ is a right angle and the great circle is the honest
190+ * limit rather than a NaN.</p>
191+ *
192+ * <p>Emitted as a LINE LOOP: the sky pass runs with {@code GL_CULL_FACE}/{@code GL_BACK} on
193+ * (see the class note), and a filled band would have to be wound correctly for a viewpoint that
194+ * moves through it — the failure mode being silent invisibility. A line has no facing.</p>
195+ */
196+ private boolean drawBoundary (BufferBuilder buffer , PacketSystemBodiesSync .RenderBody body ) {
197+ // A body with no shell has no boundary to draw. Read off the number the SERVER sent rather
198+ // than off the kind, so the renderer never has to know which kinds have one.
199+ if (!body .descendTarget || body .boundaryRadius <= 0L )
200+ return false ;
201+
202+ double dx = body .localX ;
203+ double dy = body .localY ;
204+ double dz = body .localZ ;
205+ double len = Math .sqrt (dx * dx + dy * dy + dz * dz );
206+ if (len < 1.0E-6D )
207+ return false ;
208+
209+ double nx = dx / len , ny = dy / len , nz = dz / len ;
210+ // The angle comes from the same place the range beside the body does, so the circle and the
211+ // number can never describe two different surfaces.
212+ double theta = zmaster587 .advancedRocketry .space .DescentShell
213+ .boundaryHalfAngle (len , body .boundaryRadius );
214+ double ct = Math .cos (theta ), st = Math .sin (theta );
215+
216+ // Any axis not parallel to n spans the perpendicular plane with it. Take the world axis n is
217+ // LEAST aligned with: a fixed choice degenerates to a zero-length cross product for a body
218+ // that happens to lie along it, and that body is precisely the one dead ahead.
219+ double hx = 0.0D , hy = 0.0D , hz = 0.0D ;
220+ double ax = Math .abs (nx ), ay = Math .abs (ny ), az = Math .abs (nz );
221+ if (ax <= ay && ax <= az ) {
222+ hx = 1.0D ;
223+ } else if (ay <= az ) {
224+ hy = 1.0D ;
225+ } else {
226+ hz = 1.0D ;
227+ }
228+ double ux = ny * hz - nz * hy , uy = nz * hx - nx * hz , uz = nx * hy - ny * hx ;
229+ double ul = Math .sqrt (ux * ux + uy * uy + uz * uz );
230+ if (ul < 1.0E-9D )
231+ return false ;
232+ ux /= ul ; uy /= ul ; uz /= ul ;
233+ double vx = ny * uz - nz * uy , vy = nz * ux - nx * uz , vz = nx * uy - ny * ux ;
234+
235+ buffer .begin (GL11 .GL_LINE_LOOP , DefaultVertexFormats .POSITION );
236+ for (int i = 0 ; i < BOUNDARY_SEGMENTS ; i ++) {
237+ double phi = (Math .PI * 2.0D * i ) / BOUNDARY_SEGMENTS ;
238+ double cp = Math .cos (phi ), sp = Math .sin (phi );
239+ buffer .pos ((ct * nx + st * (cp * ux + sp * vx )) * BOUNDARY_SKY_RADIUS ,
240+ (ct * ny + st * (cp * uy + sp * vy )) * BOUNDARY_SKY_RADIUS ,
241+ (ct * nz + st * (cp * uz + sp * vz )) * BOUNDARY_SKY_RADIUS ).endVertex ();
242+ }
243+ Tessellator .getInstance ().draw ();
244+ return true ;
245+ }
246+
178247 /**
179248 * Restore a sane GL state for the rest of the world render.
180249 *
@@ -256,13 +325,30 @@ private boolean drawBody(BufferBuilder buffer, PacketSystemBodiesSync.RenderBody
256325 }
257326
258327 /**
259- * Write the body's name and its current distance under the billboard, inside the already-rotated
260- * body frame so the text faces the camera exactly as the quad does.
328+ * Write the body's name and the range still to fly under the billboard, inside the
329+ * already-rotated body frame so the text faces the camera exactly as the quad does.
330+ *
331+ * <p><b>The number is the distance to the body's ATMOSPHERE, not to the body.</b> Crossing that
332+ * surface is what puts the ship on the planet, so it is the only range a pilot on approach can
333+ * act on: a body-centre range tells him to cover a distance he does not have to cover, and
334+ * still reads a whole shell's worth of blocks at the very instant he crosses. A body with no
335+ * shell to cross — a star, anything not a descend target — carries a zero radius and is
336+ * therefore labelled with its plain distance by the same arithmetic, with no special case
337+ * here.</p>
338+ *
339+ * <p>The billboard's SIZE keeps using the distance to the body itself
340+ * ({@link ApparentSize#halfSizeFor}, at the call site): how big a thing looks is a property of
341+ * the thing, how far there is left to fly is a property of the approach. They are easy to
342+ * conflate because both are "distance to that body", and conflating them would shrink a planet
343+ * to nothing exactly as you arrive.</p>
261344 */
262345 private boolean drawLabel (PacketSystemBodiesSync .RenderBody body , float half , double distance ) {
263346 if (mc .fontRenderer == null )
264347 return false ;
265- String text = nameOf (body ) + " " + ApparentSize .formatDistance (distance );
348+ String text = nameOf (body ) + " "
349+ + ApparentSize .formatDistance (
350+ zmaster587 .advancedRocketry .space .DescentShell .distanceToShell (
351+ distance , body .boundaryRadius ));
266352 GlStateManager .pushMatrix ();
267353 // The sky frame's +Y is up while the font renders DOWN its own +Y, so both axes are negated
268354 // here; without it every label reads upside down and mirrored.
0 commit comments