<> <> <> DIRECTORY Atom, Basics, BasicTime, Convert, G3dMatrix, G3dPlane, G3dRender, G3dScanConvert, G3dShadeClipXfm, G3dShape, G3dSortandDisplay, G3dVector, Real, Rope; G3dShadeClipXfmImpl: CEDAR MONITOR IMPORTS Atom, Basics, BasicTime, Convert, G3dMatrix, G3dPlane, G3dRender, G3dVector, Real, Rope EXPORTS G3dShadeClipXfm ~ BEGIN <> Context: TYPE ~ G3dRender.Context; RGB: TYPE ~ G3dRender.RGB; -- [ r, g, b: REAL]; SixSides: TYPE ~ G3dRender.SixSides; ScaleAndAddXfm: TYPE ~ G3dRender.ScaleAndAddXfm; OutCode: TYPE ~ G3dRender.OutCode; Matrix: TYPE ~ G3dRender.Matrix; Triple: TYPE ~ G3dRender.Triple; TripleSequence: TYPE ~ G3dRender.TripleSequence; NatSequence: TYPE ~ G3dRender.NatSequence; IntegerSequence: TYPE ~ G3dRender.IntegerSequence; RealSequence: TYPE ~ G3dRender.RealSequence; Shape: TYPE ~ G3dRender.Shape; FacingDir: TYPE ~ G3dRender.FacingDir; Patch: TYPE ~ G3dRender.Patch; VertexSequence: TYPE ~ G3dShape.VertexSequence; PatchSequence: TYPE ~ G3dRender.PatchSequence; PatchSequenceRep: TYPE ~ G3dRender.PatchSequenceRep; CtlPoint: TYPE ~ G3dRender.CtlPoint; CtlPointSequence: TYPE ~ G3dRender.CtlPointSequence; CtlPtInfo: TYPE ~ G3dRender.CtlPtInfo; CtlPtInfoSequence: TYPE ~ G3dRender.CtlPtInfoSequence; CtlPtInfoSequenceRep: TYPE ~ G3dRender.CtlPtInfoSequenceRep; CtlPtInfoProc: TYPE ~ G3dRender.CtlPtInfoProc; Shading: TYPE ~ G3dRender.Shading; ShadingSequence: TYPE ~ G3dRender.ShadingSequence; ShapeClass: TYPE ~ G3dRender.ShapeClass; ShadingClass: TYPE ~ G3dRender.ShadingClass; ShapeProc: TYPE ~ G3dRender.ShapeProc; -- PROC[ Context, Shape ] LORA: TYPE ~ LIST OF REF ANY; NoneOut: OutCode ~ G3dRender.NoneOut; AllOut: OutCode ~ G3dRender.AllOut; <> Transform: PROC[p: Triple, mat: Matrix] RETURNS[Triple] ~ G3dMatrix.Transform; TransformVec: PROC[vec: Triple, mat: Matrix] RETURNS[Triple] ~ G3dMatrix.TransformVec; GetProp: PROC [propList: Atom.PropList, prop: REF ANY] RETURNS [REF ANY] ~ Atom.GetPropFromList; PutProp: PROC [propList: Atom.PropList, prop: REF ANY, val: REF ANY] RETURNS [Atom.PropList] ~ Atom.PutPropOnList; <> nullPtr: INT ~ 0; -- handy name for zero in sort sequences nullTriple: Triple _ [0.0, 0.0, 0.0]; <> <> vertexStore: CtlPtInfoSequence _ NEW[ CtlPtInfoSequenceRep[96] ]; -- CtlPoint Cache vertexStoreLength: NAT _ 96; vertexStorePtr: NAT _ 0; -- place to return next free record GetCtlPtInfo: PUBLIC ENTRY PROC[] RETURNS[REF CtlPtInfo] ~ { ENABLE UNWIND => NULL; vtx: REF CtlPtInfo; IF vertexStorePtr = 0 THEN vtx _ NEW[CtlPtInfo] ELSE { vertexStorePtr _ vertexStorePtr - 1; vtx _ vertexStore[vertexStorePtr]; vertexStore[vertexStorePtr] _ NIL; }; RETURN[ vtx ]; }; ReleaseCtlPtInfo: PUBLIC ENTRY PROC[vtx: REF CtlPtInfo] ~ { ENABLE UNWIND => NULL; IF vertexStorePtr = vertexStoreLength THEN { vertexStore _ NEW[ CtlPtInfoSequenceRep[vertexStoreLength + 32] ]; vertexStoreLength _ vertexStoreLength + 32; vertexStorePtr _ 0; }; IF vertexStorePtr > 0 AND vertexStore[vertexStorePtr-1] = vtx THEN SIGNAL G3dRender.Error[$MisMatch, "Double vertexInfo release"] ELSE vertexStore[vertexStorePtr] _ vtx; vertexStorePtr _ vertexStorePtr + 1; }; patchCache: PatchSequence _ NEW[ PatchSequenceRep[16] ]; -- temps for clipping, etc. patchCacheLength: NAT _ 16; patchCachePtr: NAT _ 0; -- place to return next free record GetPatch: PUBLIC ENTRY PROC[size: NAT] RETURNS[REF Patch] ~ { ENABLE UNWIND => NULL; p: REF Patch; IF patchCachePtr = 0 THEN p _ NEW[Patch[size]] ELSE { patchCachePtr _ patchCachePtr - 1; p _ patchCache[patchCachePtr]; patchCache[patchCachePtr] _ NIL; IF p.maxLength < size THEN p _ NEW[Patch[size]]; }; RETURN[ p ]; }; ReleasePatch: PUBLIC ENTRY PROC[p: REF Patch] ~ { ENABLE UNWIND => NULL; IF p = NIL THEN RETURN[]; IF patchCachePtr = patchCacheLength THEN { patchCache _ NEW[ PatchSequenceRep[patchCacheLength + 2] ]; patchCacheLength _ patchCacheLength + 2; patchCachePtr _ 0; }; IF patchCachePtr > 0 AND patchCache[patchCachePtr-1] = p THEN SIGNAL G3dRender.Error[$MisMatch, "Double patch release"] ELSE patchCache[patchCachePtr] _ p; patchCachePtr _ patchCachePtr + 1; }; <> Sgn: PROCEDURE [number: REAL] RETURNS [INT] ~ INLINE { IF number < 0. THEN RETURN[-1] ELSE RETURN[1]; }; Ceiling: PROC[number: REAL] RETURNS[result: INTEGER] ~ { result _ Real.Round[number]; IF result < number THEN result _ result + 1; }; ElapsedTime: PROC[startTime: REAL] RETURNS[Rope.ROPE] ~ { timeX10: REAL _ 10.0 * (BasicTime.PulsesToSeconds[BasicTime.GetClockPulses[]] - startTime); RETURN[ Rope.Cat[ Convert.RopeFromReal[ Real.Fix[timeX10] / 10.0 ], " secs. " ] ]; }; CurrentTime: PROC[] RETURNS[REAL] ~ { RETURN[ BasicTime.PulsesToSeconds[BasicTime.GetClockPulses[]] ]; }; DiffPosns: PROC[vtx1, vtx2: CtlPoint] RETURNS[Triple] ~ { RETURN[[vtx1.x - vtx2.x, vtx1.y - vtx2.y, vtx1.z - vtx2.z]] }; GetNormal: PROC[ vertices: VertexSequence, poly: NatSequence, cVtx: NAT ] RETURNS[ normal: Triple ] ~ { lVtx: NAT _ poly[ (cVtx + poly.length - 1) MOD poly.length ]; nVtx: NAT _ poly[ (cVtx + 1) MOD poly.length ]; cVtx _ poly[cVtx]; normal _ G3dVector.Cross[ -- in object space so do right-handed G3dVector.Sub[ vertices[lVtx].point, vertices[cVtx].point ], G3dVector.Sub[ vertices[nVtx].point, vertices[cVtx].point ] ]; }; HoldEverything: PROCEDURE [] ~ { ERROR ABORTED; }; <> GetPolyNmls: PUBLIC PROC[context: Context, shape: Shape] ~ { <> <> xfm: Matrix _ G3dMatrix.Mul[shape.matrix, context.eyeSpaceXfm]; IF shape.faces.valid.normal THEN RETURN; -- don't mess with pre-existing normals IF shape.type # $ConvexPolygon AND shape.type # $Poly THEN { SIGNAL G3dRender.Error[$MisMatch, "Operation only for polygons"]; RETURN[]; }; FOR i: NAT IN [0..shape.surfaces.length) DO sumNmls: Triple _ [0., 0., 0.]; FOR cVtx: NAT IN [0..shape.surfaces[i].length) DO sumNmls _ G3dVector.Add[ sumNmls, GetNormal[shape.vertices, shape.surfaces[i], cVtx] ]; ENDLOOP; shape.faces[i].normal _ G3dVector.Normalize[sumNmls]; ENDLOOP; shape.faces.valid.normal _ TRUE; }; <<>> GetVtxNmls: PUBLIC PROC[context: Context, shape: Shape] ~ { <> xfm: Matrix _ G3dMatrix.Mul[shape.matrix, context.eyeSpaceXfm]; IF shape.vertices.valid.normal THEN RETURN; -- don't mess with pre-existing normals IF shape.type # $ConvexPolygon AND shape.type # $Poly THEN { SIGNAL G3dRender.Error[$MisMatch, "Operation only for polygons"]; RETURN[]; }; FOR i: NAT IN [0..shape.surfaces.length) DO -- get normals at vertices, add to earlier ones IF shape.surfaces[i] # NIL THEN FOR cVtx: NAT IN [0..shape.surfaces[i].length) DO OPEN shape.vertices[shape.surfaces[i][cVtx]]; cornerNormal: Triple _ GetNormal[ shape.vertices, shape.surfaces[i], cVtx ]; normal _ G3dVector.Add[ cornerNormal, normal]; -- assumes normal initally zero ENDLOOP; ENDLOOP; FOR i: NAT IN [0..shape.vertices.length) DO IF shape.vertices[i] # NIL THEN { OPEN shape.vertices[i]; IF G3dVector.Length[ normal ] > shape.sphereExtent.radius * .0001 THEN normal _ G3dVector.Normalize[ normal ] ELSE normal _ nullTriple; }; ENDLOOP; shape.vertices.valid.normal _ TRUE; }; <> ClipBoundingSphere: PUBLIC PROC[context: Context, shape: Shape, xfm: Matrix] RETURNS[G3dRender.ClipState] ~ { <> clipFlag: BOOLEAN _ FALSE; center: Triple _ shape.sphereExtent.center; eyeCtr: Triple _ G3dMatrix.Transform[ center, xfm]; FOR plane: SixSides IN SixSides DO distance: REAL _ G3dPlane.DistanceToPoint[ eyeCtr, context.clippingPlanes[plane] ]; IF distance < -shape.sphereExtent.radius THEN RETURN[out] ELSE IF distance < shape.sphereExtent.radius THEN clipFlag _ TRUE; ENDLOOP; IF clipFlag THEN RETURN[clipped] ELSE RETURN[in]; }; ClipPoly: PUBLIC PROC[ context: Context, poly: REF Patch] RETURNS [REF Patch] ~ { Clip: PROC[side: SixSides, pIn, pOut: REF Patch] RETURNS [REF Patch, REF Patch] = { Dist: PROC[side: SixSides, vtx: CtlPoint] RETURNS [REAL] = { -- + inside, - outside RETURN[ G3dPlane.DistanceToPoint[ [vtx.ex, vtx.ey, vtx.ez], context.clippingPlanes[side] ] ]; }; lastDist, dist: REAL; outCnt, last: NAT; IF pIn.nVtces < 2 THEN RETURN[ pIn, pOut ]; -- return if degenerate (line needs 2) outCnt _ 0; IF pIn.type # $PolyLine THEN { lastDist _ Dist[side, pIn.ctlPt[pIn.nVtces - 1].coord]; last _ pIn.nVtces - 1; }; IF pOut.maxLength < 2 * pIn.nVtces THEN pOut _ NEW[Patch[2 * pIn.nVtces]]; FOR i: NAT IN [0..pIn.nVtces) DO a, b: REAL; dist _ Dist[side, pIn.ctlPt[i].coord]; IF (i # 0 OR pIn.type # $PolyLine) AND lastDist * dist < 0. THEN { <> b _ dist / (dist - lastDist); a _ 1.0 - b; pOut.ctlPt[outCnt].coord.x _ pIn.ctlPt[i].coord.x * a + pIn.ctlPt[last].coord.x * b; pOut.ctlPt[outCnt].coord.y _ pIn.ctlPt[i].coord.y * a + pIn.ctlPt[last].coord.y * b; pOut.ctlPt[outCnt].coord.z _ pIn.ctlPt[i].coord.z * a + pIn.ctlPt[last].coord.z * b; pOut.ctlPt[outCnt].coord.ex _ pIn.ctlPt[i].coord.ex * a + pIn.ctlPt[last].coord.ex * b; pOut.ctlPt[outCnt].coord.ey _ pIn.ctlPt[i].coord.ey * a + pIn.ctlPt[last].coord.ey * b; pOut.ctlPt[outCnt].coord.ez _ pIn.ctlPt[i].coord.ez * a + pIn.ctlPt[last].coord.ez * b; pOut.ctlPt[outCnt].shade.exn _ pIn.ctlPt[i].shade.exn*a + pIn.ctlPt[last].shade.exn*b; pOut.ctlPt[outCnt].shade.eyn _ pIn.ctlPt[i].shade.eyn*a + pIn.ctlPt[last].shade.eyn*b; pOut.ctlPt[outCnt].shade.ezn _ pIn.ctlPt[i].shade.ezn*a + pIn.ctlPt[last].shade.ezn*b; pOut.ctlPt[outCnt].shade.r _ pIn.ctlPt[i].shade.r * a + pIn.ctlPt[last].shade.r * b; pOut.ctlPt[outCnt].shade.g _ pIn.ctlPt[i].shade.g * a + pIn.ctlPt[last].shade.g * b; pOut.ctlPt[outCnt].shade.b _ pIn.ctlPt[i].shade.b * a + pIn.ctlPt[last].shade.b * b; pOut.ctlPt[outCnt].shade.t _ pIn.ctlPt[i].shade.t * a + pIn.ctlPt[last].shade.t* b; pOut.ctlPt[outCnt].shade.txtrX _ pIn.ctlPt[i].shade.txtrX * a + pIn.ctlPt[last].shade.txtrX* b; pOut.ctlPt[outCnt].shade.txtrY _ pIn.ctlPt[i].shade.txtrY * a + pIn.ctlPt[last].shade.txtrY* b; pOut.ctlPt[outCnt].shade.er _ pIn.ctlPt[i].shade.er * a + pIn.ctlPt[last].shade.er * b; pOut.ctlPt[outCnt].shade.eg _ pIn.ctlPt[i].shade.eg * a + pIn.ctlPt[last].shade.eg * b; pOut.ctlPt[outCnt].shade.eb _ pIn.ctlPt[i].shade.eb * a + pIn.ctlPt[last].shade.eb * b; pOut.ctlPt[outCnt].shade.et _ pIn.ctlPt[i].shade.et * a + pIn.ctlPt[last].shade.et * b; outCnt _ outCnt + 1; }; IF dist >= 0. THEN { -- put out point if inside pOut.ctlPt[outCnt].coord.x _ pIn.ctlPt[i].coord.x; pOut.ctlPt[outCnt].coord.y _ pIn.ctlPt[i].coord.y; pOut.ctlPt[outCnt].coord.z _ pIn.ctlPt[i].coord.z; pOut.ctlPt[outCnt].coord.ex _ pIn.ctlPt[i].coord.ex; pOut.ctlPt[outCnt].coord.ey _ pIn.ctlPt[i].coord.ey; pOut.ctlPt[outCnt].coord.ez _ pIn.ctlPt[i].coord.ez; pOut.ctlPt[outCnt].shade.exn _ pIn.ctlPt[i].shade.exn; pOut.ctlPt[outCnt].shade.eyn _ pIn.ctlPt[i].shade.eyn; pOut.ctlPt[outCnt].shade.ezn _ pIn.ctlPt[i].shade.ezn; pOut.ctlPt[outCnt].shade.r _ pIn.ctlPt[i].shade.r; pOut.ctlPt[outCnt].shade.g _ pIn.ctlPt[i].shade.g; pOut.ctlPt[outCnt].shade.b _ pIn.ctlPt[i].shade.b; pOut.ctlPt[outCnt].shade.t _ pIn.ctlPt[i].shade.t; pOut.ctlPt[outCnt].shade.txtrX _ pIn.ctlPt[i].shade.txtrX; pOut.ctlPt[outCnt].shade.txtrY _ pIn.ctlPt[i].shade.txtrY; pOut.ctlPt[outCnt].shade.er _ pIn.ctlPt[i].shade.er; pOut.ctlPt[outCnt].shade.eg _ pIn.ctlPt[i].shade.eg; pOut.ctlPt[outCnt].shade.eb _ pIn.ctlPt[i].shade.eb; pOut.ctlPt[outCnt].shade.et _ pIn.ctlPt[i].shade.et; outCnt _ outCnt + 1; }; lastDist _ dist; last _ i; ENDLOOP; pOut.type _ pIn.type; pOut.oneSided _ pIn.oneSided; pOut.dir _ pIn.dir; pOut.nVtces _ outCnt; pOut.renderData _ pIn.renderData; pOut.props _ pIn.props; RETURN [ pOut, pIn ]; }; -- end Clip Proc orOfCodes: OutCode _ NoneOut; poly2: REF Patch _ GetPatch[2 * poly.nVtces]; -- get temp patch, released below IF poly.type # $ConvexPolygon AND poly.type # $Poly AND poly.type # $PolyLine THEN { SIGNAL G3dRender.Error[$Unimplemented, "Not clippable as polygon"]; RETURN[ NIL ]; }; FOR i: NAT IN [0..poly.nVtces) DO orOfCodes _ LOOPHOLE[ Basics.BITOR[LOOPHOLE[orOfCodes], LOOPHOLE[poly.ctlPt[i].coord.clip]], OutCode]; ENDLOOP; IF orOfCodes.near THEN [poly, poly2] _ Clip[ Near, poly, poly2]; IF orOfCodes.far THEN [poly, poly2] _ Clip[ Far, poly, poly2]; IF orOfCodes.left THEN [poly, poly2] _ Clip[ Left, poly, poly2]; IF orOfCodes.right THEN [poly, poly2] _ Clip[ Right, poly, poly2]; IF orOfCodes.bottom THEN [poly, poly2] _ Clip[Bottom, poly, poly2]; IF orOfCodes.top THEN [poly, poly2] _ Clip[ Top, poly, poly2]; ReleasePatch[poly2]; -- done with temp patch RETURN[ poly ]; }; GetPatchClipState: PUBLIC PROC[ patch: REF Patch] ~ { orOfCodes: OutCode _ NoneOut; andOfCodes: OutCode _ AllOut; FOR i: NAT IN [0..patch.nVtces) DO orOfCodes _ LOOPHOLE[ Basics.BITOR[ LOOPHOLE[orOfCodes], LOOPHOLE[patch[i].coord.clip]], OutCode]; andOfCodes _ LOOPHOLE[ Basics.BITAND[LOOPHOLE[andOfCodes], LOOPHOLE[patch[i].coord.clip]], OutCode]; ENDLOOP; IF andOfCodes # NoneOut THEN patch.clipState _ out ELSE IF orOfCodes = NoneOut THEN patch.clipState _ in ELSE patch.clipState _ clipped; }; GetClipCodeForPt: PUBLIC PROC[context: Context, pt: Triple] RETURNS[clip: OutCode] ~ { <> clip.bottom_ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Bottom]] < 0.; clip.top _ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Top] ] < 0.; clip.left _ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Left] ] < 0.; clip.right _ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Right] ] < 0.; clip.near _ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Near] ] < 0.; clip.far _ G3dPlane.DistanceToPoint[ pt, context.clippingPlanes[Far] ] < 0.; }; XfmPtToEyeSpace: PUBLIC PROC[context: Context, pt: Triple, xfm: Matrix _ NIL] RETURNS[Triple, OutCode] ~ { << Transform CtlPoint to Eye Space >> IF xfm = NIL THEN xfm _ context.eyeSpaceXfm; pt _ Transform[ pt, xfm ]; RETURN[ pt, GetClipCodeForPt[context, pt] ]; }; XfmTripleToDisplay: PUBLIC PROC[pt: Triple, xfm, display: ScaleAndAddXfm, offset: REAL] RETURNS[result: Triple] ~ { << Transform vertex from eyespace to display coordinates - local utility>> aLilBit: REAL _ G3dScanConvert.justNoticeable * G3dScanConvert.justNoticeable; IF pt.z <= 0.0 THEN { SIGNAL G3dRender.Error[$MisMatch, "Negative depth"]; pt.z _ .001; -- fudge bad depths }; result.x _ xfm.scaleX*pt.x/pt.z + xfm.addX; -- convert to normalized display coords result.y _ xfm.scaleY*pt.y/pt.z + xfm.addY; result.z _ xfm.scaleZ/pt.z + xfm.addZ; result.x _ display.scaleX * result.x + display.addX; -- convert to screen coordinates result.y _ display.scaleY * result.y + display.addY; result.z _ display.scaleZ * result.z + display.addZ; result.x _ result.x - offset; -- nojaggy tiler offsets by 1/2 pixel and spreads out by 1 result.y _ result.y - offset; RETURN [ result ]; }; XfmPtToDisplay: PUBLIC PROC[context: Context, pt: Triple, shape: Shape _ NIL] RETURNS[Triple] ~ { << Transform vertex from eyespace to display coordinates>> result: Triple _ XfmTripleToDisplay[ pt: pt, xfm: context.eyeToNdc, display: context.ndcToPixels, offset: IF context.antiAliasing THEN .5 ELSE 0.0 -- nojaggy tiler offsets by 1/2 pixel ]; IF shape # NIL THEN { xLo: INTEGER _ Real.Fix[ MAX[0.0, result.x - 2.0] ]; xHi: INTEGER _ Ceiling[ MIN[context.viewPort.w, result.x + 2.0] ]; yLo: INTEGER _ Real.Fix[ MAX[0.0, result.y - 2.0] ]; yHi: INTEGER _ Ceiling[ MIN[context.viewPort.h, result.y + 2.0] ]; IF shape.screenExtent.min.x > xLo THEN shape.screenExtent.min.x _ xLo; IF shape.screenExtent.max.x < xHi THEN shape.screenExtent.max.x _ xHi; IF shape.screenExtent.min.y > yLo THEN shape.screenExtent.min.y _ yLo; IF shape.screenExtent.max.y < yHi THEN shape.screenExtent.max.y _ yHi; }; RETURN [ result ]; }; <> BackFacing: PUBLIC PROC[ context: Context, poly: REF Patch, useEyeSpace: BOOLEAN _ FALSE ] RETURNS [FacingDir] ~ { <> SELECT poly.type FROM $ConvexPolygon, $Poly => IF useEyeSpace THEN { <> this: CtlPtInfo _ poly.ctlPt[0]; next: CtlPtInfo _ poly.ctlPt[1]; last: CtlPtInfo _ poly.ctlPt[2]; <> direction: Triple _ G3dVector.Normalize[G3dVector.Cross[ [next.coord.ex - this.coord.ex, next.coord.ey - this.coord.ey, next.coord.ez - this.coord.ez], [last.coord.ex - this.coord.ex, last.coord.ey - this.coord.ey, last.coord.ez - this.coord.ez] ] ]; dotDir: REAL _ G3dVector.Dot[[this.coord.ex, this.coord.ey, this.coord.ez] , direction]; IF dotDir > 0.0 THEN RETURN[back] ELSE IF dotDir < 0.0 THEN RETURN[front]; } ELSE { -- do check in image space, rejecting eensy polygon edges s: REAL _ 1.0 / G3dScanConvert.justNoticeable; -- scales visible feature size to 1 FOR i: NAT IN [0..poly.nVtces) DO this: CtlPtInfo _ poly.ctlPt[i]; next: CtlPtInfo _ poly.ctlPt[(i+1) MOD poly.nVtces]; last: CtlPtInfo _ poly.ctlPt[(i+poly.nVtces-1) MOD poly.nVtces]; zNorm: INT _ -- integer computation of z-coord of normal (left-handed) ( Real.Fix[s*next.coord.sx - s*this.coord.sx] ) * ( Real.Fix[s*last.coord.sy - s*this.coord.sy] ) - ( Real.Fix[s*next.coord.sy - s*this.coord.sy] ) * ( Real.Fix[s*last.coord.sx - s*this.coord.sx] ); zNorm _ zNorm *Sgn[context.ndcToPixels.scaleY] *Sgn[context.ndcToPixels.scaleX]; <> IF zNorm > 0 THEN RETURN[ back ] ELSE IF zNorm < 0 THEN RETURN[ front ]; ENDLOOP; SIGNAL G3dRender.Error[$Condition, "Edges too small for stable arithmetic"]; }; $Bezier => { <