void draw(SphereComponent hemisphere, Sky const &sky, int firstActiveLayer, LayerData const *layerData) const { DENG2_ASSERT(layerData); if(verts.isEmpty()) return; if(firstActiveLayer < 0) return; bool const yflip = (hemisphere == LowerHemisphere); if(yflip) { // The lower hemisphere must be flipped. glMatrixMode(GL_MODELVIEW); glPushMatrix(); glScalef(1.0f, -1.0f, 1.0f); } // First draw the cap and the background for fadeouts, if needed. bool drawFadeOut = true; drawCap(chooseCapColor(hemisphere, sky.layer(firstActiveLayer), &drawFadeOut), drawFadeOut); for(int i = firstActiveLayer; i < MAX_LAYERS; ++i) { SkyLayer const *skyLayer = sky.layer(i); LayerData const &ldata = layerData[i]; if(!ldata.active) continue; TextureVariant *layerTex = nullptr; if(Material *mat = chooseMaterialForSkyLayer(skyLayer)) { MaterialSnapshot const &ms = mat->prepare(SkyDrawable::layerMaterialSpec(skyLayer->isMasked())); layerTex = &ms.texture(MTU_PRIMARY); GL_BindTexture(layerTex); glEnable(GL_TEXTURE_2D); glMatrixMode(GL_TEXTURE); glPushMatrix(); glLoadIdentity(); Vector2i const &texSize = layerTex->generalCase().dimensions(); if(texSize.x > 0) { glTranslatef(ldata.offset / texSize.x, 0, 0); glScalef(1024.f / texSize.x, 1, 1); } if(yflip) { glScalef(1, -1, 1); glTranslatef(0, -1, 0); } } else { GL_SetNoTexture(); } #define WRITESKYVERTEX(r_, c_) { \ svtx = &vertex(r_, c_); \ if(layerTex) \ { \ glTexCoord2f((c_) / float(columns), (r_) / float(rows)); \ } \ if(drawFadeOut) \ { \ if((r_) == 0) glColor4f(1, 1, 1, 0); \ else glColor3f(1, 1, 1); \ } \ else \ { \ if((r_) == 0) glColor3f(0, 0, 0); \ else glColor3f(1, 1, 1); \ } \ glVertex3f(svtx->x, svtx->y, svtx->z); \ } Vector3f const *svtx; for(int r = 0; r < rows; ++r) { glBegin(GL_TRIANGLE_STRIP); WRITESKYVERTEX(r, 0); WRITESKYVERTEX(r + 1, 0); for(int c = 1; c <= columns; ++c) { WRITESKYVERTEX(r, c); WRITESKYVERTEX(r + 1, c); } glEnd(); } if(layerTex) { glMatrixMode(GL_TEXTURE); glPopMatrix(); glDisable(GL_TEXTURE_2D); } #undef WRITESKYVERTEX } if(yflip) { glMatrixMode(GL_MODELVIEW); glPopMatrix(); } }
void R_ProjectSprite(mobj_t &mob) { /// @todo Lots of stuff here! This needs to be broken down into multiple functions /// and/or classes that handle preprocessing of visible entities. Keep in mind that /// data/state can persist across frames in the mobjs' private data. -jk // Not all objects can/will be visualized. Skip this object if: // ...hidden? if((mob.ddFlags & DDMF_DONTDRAW)) return; // ...not linked into the map? if(!Mobj_HasSubspace(mob)) return; // ...in an invalid state? if(!mob.state || !runtimeDefs.states.indexOf(mob.state)) return; // ...no sprite frame is defined? Record *spriteRec = Mobj_SpritePtr(mob); if(!spriteRec) return; // ...fully transparent? dfloat const alpha = Mobj_Alpha(mob); if(alpha <= 0) return; // ...origin lies in a sector with no volume? ConvexSubspace &subspace = Mobj_BspLeafAtOrigin(mob).subspace(); SectorCluster &cluster = subspace.cluster(); if(!cluster.hasWorldVolume()) return; ClientMobjThinkerData const *mobjData = THINKER_DATA_MAYBE(mob.thinker, ClientMobjThinkerData); // Determine distance to object. Vector3d const moPos = mobjOriginSmoothed(&mob); coord_t const distFromEye = Rend_PointDist2D(moPos); // Should we use a 3D model? ModelDef *mf = nullptr, *nextmf = nullptr; dfloat interp = 0; MobjAnimator const *animator = nullptr; // GL2 model present? if(useModels) { mf = Mobj_ModelDef(mob, &nextmf, &interp); if(mf) { // Use a sprite if the object is beyond the maximum model distance. if(maxModelDistance && !(mf->flags & MFF_NO_DISTANCE_CHECK) && distFromEye > maxModelDistance) { mf = nextmf = nullptr; interp = -1; } } if(mobjData) { animator = mobjData->animator(); } } bool const hasModel = (mf || animator); // Decide which material to use according to the sprite's angle and position // relative to that of the viewer. Material *mat = nullptr; bool matFlipS = false; bool matFlipT = false; defn::Sprite sprite(*spriteRec); try { Record const &spriteView = sprite.nearestView(mob.angle, R_ViewPointToAngle(mob.origin), !!mf); mat = resSys().materialPtr(de::Uri(spriteView.gets("material"), RC_NULL)); matFlipS = spriteView.getb("mirrorX"); } catch(defn::Sprite::MissingViewError const &er) { // Log but otherwise ignore this error. LOG_GL_WARNING("Projecting sprite '%i' frame '%i': %s") << mob.sprite << mob.frame << er.asText(); } if(!mat) return; MaterialAnimator &matAnimator = mat->getAnimator(Rend_SpriteMaterialSpec(mob.tclass, mob.tmap)); // Ensure we've up to date info about the material. matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; // A valid sprite texture in the "Sprites" scheme is required. if(!tex || tex->base().manifest().schemeName().compareWithoutCase("Sprites")) { return; } bool const fullbright = ((mob.state->flags & STF_FULLBRIGHT) != 0 || levelFullBright); // Align to the view plane? (Means scaling down Z with models) bool const viewAlign = (!mf && ((mob.ddFlags & DDMF_VIEWALIGN) || alwaysAlign == 1)) || alwaysAlign == 3; // Perform visibility checking by projecting a view-aligned line segment // relative to the viewer and determining if the whole of the segment has // been clipped away according to the 360 degree angle clipper. coord_t const visWidth = Mobj_VisualRadius(mob) * 2; /// @todo ignorant of rotation... Vector2d v1, v2; R_ProjectViewRelativeLine2D(moPos, mf || viewAlign, visWidth, (mf? 0 : coord_t(-tex->base().origin().x) - (visWidth / 2.0f)), v1, v2); // Not visible? if(!rendSys().angleClipper().checkRangeFromViewRelPoints(v1, v2)) { coord_t const MAX_OBJECT_RADIUS = 128; // Sprite visibility is absolute. if(!hasModel) return; // If the model is close to the viewpoint we should still to draw it, // otherwise large models are likely to disappear too early. viewdata_t const *viewData = &viewPlayer->viewport(); Vector2d delta(distFromEye, moPos.z + (mob.height / 2) - viewData->current.origin.z); if(M_ApproxDistance(delta.x, delta.y) > MAX_OBJECT_RADIUS) return; } // Store information in a vissprite. vissprite_t *vis = R_NewVisSprite(animator? VSPR_MODEL_GL2 : mf? VSPR_MODEL : VSPR_SPRITE); vis->pose.origin = moPos; vis->pose.distance = distFromEye; // The Z origin of the visual should match that of the mobj. When smoothing // is enabled this requires examining all touched sector planes in the vicinity. Plane &floor = cluster.visFloor(); Plane &ceiling = cluster.visCeiling(); bool floorAdjust = false; if(!Mobj_OriginBehindVisPlane(&mob)) { floorAdjust = de::abs(floor.heightSmoothed() - floor.height()) < 8; findMobjZOrigin(mob, floorAdjust, *vis); } coord_t topZ = vis->pose.origin.z + -tex->base().origin().y; // global z top // Determine floor clipping. coord_t floorClip = mob.floorClip; if(mob.ddFlags & DDMF_BOB) { // Bobbing is applied using floorclip. floorClip += Mobj_BobOffset(mob); } // Determine angles. /// @todo Surely this can be done in a subclass/function. -jk dfloat yaw = 0, pitch = 0; if(animator) { // TODO: More angle options with GL2 models. yaw = Mobj_AngleSmoothed(&mob) / dfloat( ANGLE_MAX ) * -360; } else if(mf) { // Determine the rotation angles (in degrees). if(mf->testSubFlag(0, MFF_ALIGN_YAW)) { // Transform the origin point. viewdata_t const *viewData = &viewPlayer->viewport(); Vector2d delta(moPos.y - viewData->current.origin.y, moPos.x - viewData->current.origin.x); yaw = 90 - (BANG2RAD(bamsAtan2(delta.x * 10, delta.y * 10)) - PI / 2) / PI * 180; } else if(mf->testSubFlag(0, MFF_SPIN)) { yaw = modelSpinSpeed * 70 * App_WorldSystem().time() + MOBJ_TO_ID(&mob) % 360; } else if(mf->testSubFlag(0, MFF_MOVEMENT_YAW)) { yaw = R_MovementXYYaw(mob.mom[0], mob.mom[1]); } else { yaw = Mobj_AngleSmoothed(&mob) / dfloat( ANGLE_MAX ) * -360; } // How about a unique offset? if(mf->testSubFlag(0, MFF_IDANGLE)) { yaw += MOBJ_TO_ID(&mob) % 360; // arbitrary } if(mf->testSubFlag(0, MFF_ALIGN_PITCH)) { viewdata_t const *viewData = &viewPlayer->viewport(); Vector2d delta(vis->pose.midZ() - viewData->current.origin.z, distFromEye); pitch = -BANG2DEG(bamsAtan2(delta.x * 10, delta.y * 10)); } else if(mf->testSubFlag(0, MFF_MOVEMENT_PITCH)) { pitch = R_MovementXYZPitch(mob.mom[0], mob.mom[1], mob.mom[2]); } else { pitch = 0; } } // Determine possible short-range visual offset. Vector3d visOff; if((hasModel && useSRVO > 0) || (!hasModel && useSRVO > 1)) { if(mob.tics >= 0) { visOff = Vector3d(mob.srvo) * (mob.tics - frameTimePos) / (float) mob.state->tics; } if(!INRANGE_OF(mob.mom[0], 0, NOMOMENTUM_THRESHOLD) || !INRANGE_OF(mob.mom[1], 0, NOMOMENTUM_THRESHOLD) || !INRANGE_OF(mob.mom[2], 0, NOMOMENTUM_THRESHOLD)) { // Use the object's speed to calculate a short-range offset. visOff += Vector3d(mob.mom) * frameTimePos; } } // Will it be drawn as a 2D sprite? if(!hasModel) { bool const brightShadow = (mob.ddFlags & DDMF_BRIGHTSHADOW) != 0; bool const fitTop = (mob.ddFlags & DDMF_FITTOP) != 0; bool const fitBottom = (mob.ddFlags & DDMF_NOFITBOTTOM) == 0; // Additive blending? blendmode_t blendMode; if(brightShadow) { blendMode = BM_ADD; } // Use the "no translucency" blending mode? else if(noSpriteTrans && alpha >= .98f) { blendMode = BM_ZEROALPHA; } else { blendMode = BM_NORMAL; } // We must find the correct positioning using the sector floor // and ceiling heights as an aid. if(matDimensions.y < ceiling.heightSmoothed() - floor.heightSmoothed()) { // Sprite fits in, adjustment possible? if(fitTop && topZ > ceiling.heightSmoothed()) topZ = ceiling.heightSmoothed(); if(floorAdjust && fitBottom && topZ - matDimensions.y < floor.heightSmoothed()) topZ = floor.heightSmoothed() + matDimensions.y; } // Adjust by the floor clip. topZ -= floorClip; Vector3d const origin(vis->pose.origin.x, vis->pose.origin.y, topZ - matDimensions.y / 2.0f); Vector4f ambientColor; duint vLightListIdx = 0; evaluateLighting(origin, subspace, vis->pose.distance, fullbright, ambientColor, &vLightListIdx); // Apply uniform alpha (overwritting intensity factor). ambientColor.w = alpha; VisSprite_SetupSprite(vis, VisEntityPose(origin, visOff, viewAlign), VisEntityLighting(ambientColor, vLightListIdx), floor.heightSmoothed(), ceiling.heightSmoothed(), floorClip, topZ, *mat, matFlipS, matFlipT, blendMode, mob.tclass, mob.tmap, &Mobj_BspLeafAtOrigin(mob), floorAdjust, fitTop, fitBottom); } else // It will be drawn as a 3D model. { Vector4f ambientColor; duint vLightListIdx = 0; evaluateLighting(vis->pose.origin, subspace, vis->pose.distance, fullbright, ambientColor, &vLightListIdx); // Apply uniform alpha (overwritting intensity factor). ambientColor.w = alpha; if(animator) { // Set up a GL2 model for drawing. vis->pose = VisEntityPose(vis->pose.origin, Vector3d(visOff.x, visOff.y, visOff.z - floorClip), viewAlign, topZ, yaw, 0, pitch, 0); vis->light = VisEntityLighting(ambientColor, vLightListIdx); vis->data.model2.object = &mob; vis->data.model2.animator = animator; vis->data.model2.model = &animator->model(); vis->data.model2.auxData = &mobjData->auxiliaryModelData(); } else { DENG2_ASSERT(mf); VisSprite_SetupModel(vis, VisEntityPose(vis->pose.origin, Vector3d(visOff.x, visOff.y, visOff.z - floorClip), viewAlign, topZ, yaw, 0, pitch, 0), VisEntityLighting(ambientColor, vLightListIdx), mf, nextmf, interp, mob.thinker.id, mob.selector, &Mobj_BspLeafAtOrigin(mob), mob.ddFlags, mob.tmap, fullbright && !(mf && mf->testSubFlag(0, MFF_DIM)), false); } } // Do we need to project a flare source too? if(mob.lumIdx != Lumobj::NoIndex && haloMode > 0) { /// @todo mark this light source visible for LensFx try { Record const &spriteView = sprite.nearestView(mob.angle, R_ViewPointToAngle(mob.origin)); // Lookup the Material for this Sprite and prepare the animator. MaterialAnimator &matAnimator = resSys().material(de::Uri(spriteView.gets("material"), RC_NULL)) .getAnimator(Rend_SpriteMaterialSpec(mob.tclass, mob.tmap)); matAnimator.prepare(); Vector2i const &matDimensions = matAnimator.dimensions(); TextureVariant *tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; // A valid sprite texture in the "Sprites" scheme is required. if(!tex || tex->base().manifest().schemeName().compareWithoutCase("Sprites")) { return; } auto const *pl = (pointlight_analysis_t const *) tex->base().analysisDataPointer(Texture::BrightPointAnalysis); DENG2_ASSERT(pl); Lumobj const &lob = cluster.sector().map().lumobj(mob.lumIdx); vissprite_t *vis = R_NewVisSprite(VSPR_FLARE); vis->pose.distance = distFromEye; // Determine the exact center of the flare. vis->pose.origin = moPos + visOff; vis->pose.origin.z += lob.zOffset(); dfloat flareSize = pl->brightMul; // X offset to the flare position. dfloat xOffset = matDimensions.x * pl->originX - -tex->base().origin().x; // Does the mobj have an active light definition? ded_light_t const *def = (mob.state? runtimeDefs.stateInfo[runtimeDefs.states.indexOf(mob.state)].light : 0); if(def) { if(def->size) flareSize = def->size; if(def->haloRadius) flareSize = def->haloRadius; if(def->offset[0]) xOffset = def->offset[0]; vis->data.flare.flags = def->flags; } vis->data.flare.size = flareSize * 60 * (50 + haloSize) / 100.0f; if(vis->data.flare.size < 8) vis->data.flare.size = 8; // Color is taken from the associated lumobj. V3f_Set(vis->data.flare.color, lob.color().x, lob.color().y, lob.color().z); vis->data.flare.factor = mob.haloFactors[DoomsdayApp::players().indexOf(viewPlayer)]; vis->data.flare.xOff = xOffset; vis->data.flare.mul = 1; vis->data.flare.tex = 0; if(def && def->flare) { de::Uri const &flaremapResourceUri = *def->flare; if(flaremapResourceUri.path().toStringRef().compareWithoutCase("-")) { vis->data.flare.tex = GL_PrepareFlaremap(flaremapResourceUri); } } } catch(defn::Sprite::MissingViewError const &er) { // Log but otherwise ignore this error. LOG_GL_WARNING("Projecting flare source for sprite '%i' frame '%i': %s") << mob.sprite << mob.frame << er.asText(); } catch(res::System::MissingResourceManifestError const &er) { // Log but otherwise ignore this error. LOG_GL_WARNING("Projecting flare source for sprite '%i' frame '%i': %s") << mob.sprite << mob.frame << er.asText(); } } }
void Rend_DrawSprite(vissprite_t const &spr) { drawspriteparams_t const &parm = *VS_SPRITE(&spr); DENG_ASSERT_IN_MAIN_THREAD(); DENG_ASSERT_GL_CONTEXT_ACTIVE(); TextureVariant *tex = nullptr; Vector2f size; dfloat viewOffsetX = 0; ///< View-aligned offset to center point. dfloat s = 1, t = 1; ///< Bottom right coords. // Many sprite properties are inherited from the material. if(MaterialAnimator *matAnimator = parm.matAnimator) { // Ensure we have up to date info about the material. matAnimator->prepare(); tex = matAnimator->texUnit(MaterialAnimator::TU_LAYER0).texture; dint const texBorder = tex->spec().variant.border; size = matAnimator->dimensions() + Vector2i(texBorder * 2, texBorder * 2); viewOffsetX = -size.x / 2 + -tex->base().origin().x; tex->glCoords(&s, &t); } // We may want to draw using another material variant instead. if(renderTextures == 2) { // For lighting debug, render all solid surfaces using the gray texture. Material &debugMaterial = resSys().material(de::Uri("System", Path("gray"))); MaterialAnimator &matAnimator = debugMaterial.getAnimator(Rend_SpriteMaterialSpec()); // Ensure we have up to date info about the material. matAnimator.prepare(); tex = matAnimator.texUnit(MaterialAnimator::TU_LAYER0).texture; } if(renderTextures) { GL_BindTexture(tex); glEnable(GL_TEXTURE_2D); } else { GL_SetNoTexture(); } // Coordinates to the center of the sprite (game coords). coord_t spriteCenter[3] = { spr.pose.origin[0] + spr.pose.srvo[0], spr.pose.origin[1] + spr.pose.srvo[1], spr.pose.origin[2] + spr.pose.srvo[2] }; coord_t v1[3], v2[3], v3[3], v4[3]; R_ProjectViewRelativeLine2D(spriteCenter, spr.pose.viewAligned, size.x, viewOffsetX, v1, v4); v2[0] = v1[0]; v2[1] = v1[1]; v3[0] = v4[0]; v3[1] = v4[1]; v1[2] = v4[2] = spriteCenter[2] - size.y / 2; v2[2] = v3[2] = spriteCenter[2] + size.y / 2; // Calculate the surface normal. coord_t surfaceNormal[3]; V3d_PointCrossProduct(surfaceNormal, v2, v1, v3); V3d_Normalize(surfaceNormal); /*#if _DEBUG // Draw the surface normal. glBegin(GL_LINES); glColor4f(1, 0, 0, 1); glVertex3f(spriteCenter[0], spriteCenter[2], spriteCenter[1]); glColor4f(1, 0, 0, 0); glVertex3f(spriteCenter[0] + surfaceNormal[0] * 10, spriteCenter[2] + surfaceNormal[2] * 10, spriteCenter[1] + surfaceNormal[1] * 10); glEnd(); #endif*/ // All sprite vertices are co-plannar, so just copy the surface normal. // @todo: Can we do something better here? dgl_color_t quadColors[4]; dgl_vertex_t quadNormals[4]; for(dint i = 0; i < 4; ++i) { V3f_Copyd(quadNormals[i].xyz, surfaceNormal); } if(!spr.light.vLightListIdx) { // Lit uniformly. applyUniformColor(4, quadColors, &spr.light.ambientColor[0]); } else { // Lit normally. Spr_VertexColors(4, quadColors, quadNormals, spr.light.vLightListIdx, spriteLight + 1, &spr.light.ambientColor[0]); } // Do we need to do some aligning? bool restoreMatrix = false; bool restoreZ = false; if(spr.pose.viewAligned || alwaysAlign >= 2) { // We must set up a modelview transformation matrix. restoreMatrix = true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); // Rotate around the center of the sprite. glTranslatef(spriteCenter[0], spriteCenter[2], spriteCenter[1]); if(!spr.pose.viewAligned) { dfloat s_dx = v1[0] - v2[0]; dfloat s_dy = v1[1] - v2[1]; if(alwaysAlign == 2) { // Restricted camera alignment. dfloat dx = spriteCenter[0] - vOrigin.x; dfloat dy = spriteCenter[1] - vOrigin.z; dfloat spriteAngle = BANG2DEG( bamsAtan2(spriteCenter[2] - vOrigin.y, std::sqrt(dx * dx + dy * dy))); if(spriteAngle > 180) spriteAngle -= 360; if(fabs(spriteAngle) > maxSpriteAngle) { dfloat turnAngle = (spriteAngle > 0? spriteAngle - maxSpriteAngle : spriteAngle + maxSpriteAngle); // Rotate along the sprite edge. glRotatef(turnAngle, s_dx, 0, s_dy); } } else { // Restricted view plane alignment. // This'll do, for now... Really it should notice both the // sprite angle and vpitch. glRotatef(vpitch * .5f, s_dx, 0, s_dy); } } else { // Normal rotation perpendicular to the view plane. glRotatef(vpitch, viewsidex, 0, viewsidey); } glTranslatef(-spriteCenter[0], -spriteCenter[2], -spriteCenter[1]); } // Need to change blending modes? if(parm.blendMode != BM_NORMAL) { GL_BlendMode(parm.blendMode); } // Transparent sprites shouldn't be written to the Z buffer. if(parm.noZWrite || spr.light.ambientColor[3] < .98f || !(parm.blendMode == BM_NORMAL || parm.blendMode == BM_ZEROALPHA)) { restoreZ = true; glDepthMask(GL_FALSE); } dgl_vertex_t vs[4], *v = vs; dgl_texcoord_t tcs[4], *tc = tcs; // 1---2 // | | Vertex layout. // 0---3 v[0].xyz[0] = v1[0]; v[0].xyz[1] = v1[2]; v[0].xyz[2] = v1[1]; v[1].xyz[0] = v2[0]; v[1].xyz[1] = v2[2]; v[1].xyz[2] = v2[1]; v[2].xyz[0] = v3[0]; v[2].xyz[1] = v3[2]; v[2].xyz[2] = v3[1]; v[3].xyz[0] = v4[0]; v[3].xyz[1] = v4[2]; v[3].xyz[2] = v4[1]; tc[0].st[0] = s * (parm.matFlip[0]? 1:0); tc[0].st[1] = t * (!parm.matFlip[1]? 1:0); tc[1].st[0] = s * (parm.matFlip[0]? 1:0); tc[1].st[1] = t * (parm.matFlip[1]? 1:0); tc[2].st[0] = s * (!parm.matFlip[0]? 1:0); tc[2].st[1] = t * (parm.matFlip[1]? 1:0); tc[3].st[0] = s * (!parm.matFlip[0]? 1:0); tc[3].st[1] = t * (!parm.matFlip[1]? 1:0); drawQuad(v, quadColors, tc); if(renderTextures) { glDisable(GL_TEXTURE_2D); } if(devMobjVLights && spr.light.vLightListIdx) { // Draw the vlight vectors, for debug. glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glTranslatef(spr.pose.origin[0], spr.pose.origin[2], spr.pose.origin[1]); coord_t const distFromViewer = de::abs(spr.pose.distance); rendSys().forAllVectorLights(spr.light.vLightListIdx, [&distFromViewer] (VectorLightData const &vlight) { if(distFromViewer < 1600 - 8) { Rend_DrawVectorLight(vlight, 1 - distFromViewer / 1600); } return LoopContinue; }); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); } // Need to restore the original modelview matrix? if(restoreMatrix) { glPopMatrix(); } // Change back to normal blending? if(parm.blendMode != BM_NORMAL) { GL_BlendMode(BM_NORMAL); } // Enable Z-writing again? if(restoreZ) { glDepthMask(GL_TRUE); } }