/* ** voxels.cpp ** **--------------------------------------------------------------------------- ** Copyright 2010-2011 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** ** */ #include #include #include #include "templates.h" #include "doomdef.h" #include "m_swap.h" #include "m_argv.h" #include "i_system.h" #include "w_wad.h" #include "c_console.h" #include "c_cvars.h" #include "c_dispatch.h" #include "doomstat.h" #include "v_video.h" #include "sc_man.h" #include "s_sound.h" #include "sbar.h" #include "gi.h" #include "r_sky.h" #include "cmdlib.h" #include "g_level.h" #include "d_net.h" #include "colormatcher.h" #include "d_netinf.h" #include "v_palette.h" #include "r_data/r_translate.h" #include "r_data/colormaps.h" #include "r_data/sprites.h" #include "voxels.h" void VOX_AddVoxel(int sprnum, int frame, FVoxelDef *def); TDeletingArray Voxels; // used only to auto-delete voxels on exit. TDeletingArray VoxelDefs; struct VoxelOptions { VoxelOptions() : DroppedSpin(0), PlacedSpin(0), Scale(1.), AngleOffset(90.), OverridePalette(false) {} int DroppedSpin; int PlacedSpin; double Scale; DAngle AngleOffset; bool OverridePalette; }; //========================================================================== // // GetVoxelRemap // // Calculates a remap table for the voxel's palette. Results are cached so // passing the same palette repeatedly will not require repeated // recalculations. // //========================================================================== static BYTE *GetVoxelRemap(const BYTE *pal) { static BYTE remap[256]; static BYTE oldpal[768]; static bool firsttime = true; if (firsttime || memcmp(oldpal, pal, 768) != 0) { // Not the same palette as last time, so recalculate. firsttime = false; memcpy(oldpal, pal, 768); for (int i = 0; i < 256; ++i) { // The voxel palette uses VGA colors, so we have to expand it // from 6 to 8 bits per component. remap[i] = BestColor((uint32 *)GPalette.BaseColors, (oldpal[i*3 + 0] << 2) | (oldpal[i*3 + 0] >> 4), (oldpal[i*3 + 1] << 2) | (oldpal[i*3 + 1] >> 4), (oldpal[i*3 + 2] << 2) | (oldpal[i*3 + 2] >> 4)); } } return remap; } //========================================================================== // // CopyVoxelSlabs // // Copy all the slabs in a block of slabs. // //========================================================================== static bool CopyVoxelSlabs(kvxslab_t *dest, const kvxslab_t *src, int size) { while (size >= 3) { int slabzleng = src->zleng; if (3 + slabzleng > size) { // slab is too tall return false; } dest->ztop = src->ztop; dest->zleng = src->zleng; dest->backfacecull = src->backfacecull; for (int j = 0; j < slabzleng; ++j) { dest->col[j] = src->col[j]; } slabzleng += 3; src = (kvxslab_t *)((BYTE *)src + slabzleng); dest = (kvxslab_t *)((BYTE *)dest + slabzleng); size -= slabzleng; } return true; } //========================================================================== // // RemapVoxelSlabs // // Remaps all the slabs in a block of slabs. // //========================================================================== static void RemapVoxelSlabs(kvxslab_t *dest, int size, const BYTE *remap) { while (size >= 3) { int slabzleng = dest->zleng; for (int j = 0; j < slabzleng; ++j) { dest->col[j] = remap[dest->col[j]]; } slabzleng += 3; dest = (kvxslab_t *)((BYTE *)dest + slabzleng); size -= slabzleng; } } //========================================================================== // // R_LoadKVX // //========================================================================== FVoxel *R_LoadKVX(int lumpnum) { const kvxslab_t *slabs[MAXVOXMIPS]; FVoxel *voxel = new FVoxel; const BYTE *rawmip; int mip, maxmipsize; int i, j, n; FMemLump lump = Wads.ReadLump(lumpnum); // FMemLump adds an extra 0 byte to the end. BYTE *rawvoxel = (BYTE *)lump.GetMem(); int voxelsize = (int)(lump.GetSize()-1); // Oh, KVX, why couldn't you have a proper header? We'll just go through // and collect each MIP level, doing lots of range checking, and if the // last one doesn't end exactly 768 bytes before the end of the file, // we'll reject it. for (mip = 0, rawmip = rawvoxel, maxmipsize = voxelsize - 768 - 4; mip < MAXVOXMIPS; mip++) { int numbytes = GetInt(rawmip); if (numbytes > maxmipsize || numbytes < 24) { break; } rawmip += 4; FVoxelMipLevel *mipl = &voxel->Mips[mip]; // Load header data. mipl->SizeX = GetInt(rawmip + 0); mipl->SizeY = GetInt(rawmip + 4); mipl->SizeZ = GetInt(rawmip + 8); mipl->Pivot.X = GetInt(rawmip + 12) / 256.; mipl->Pivot.Y = GetInt(rawmip + 16) / 256.; mipl->Pivot.Z = GetInt(rawmip + 20) / 256.; // How much space do we have for voxdata? int offsetsize = (mipl->SizeX + 1) * 4 + mipl->SizeX * (mipl->SizeY + 1) * 2; int voxdatasize = numbytes - 24 - offsetsize; if (voxdatasize < 0) { // Clearly, not enough. break; } if (voxdatasize != 0) { // This mip level is not empty. // Allocate slab data space. mipl->OffsetX = new int[(numbytes - 24 + 3) / 4]; mipl->OffsetXY = (short *)(mipl->OffsetX + mipl->SizeX + 1); mipl->SlabData = (BYTE *)(mipl->OffsetXY + mipl->SizeX * (mipl->SizeY + 1)); // Load x offsets. for (i = 0, n = mipl->SizeX; i <= n; ++i) { // The X offsets stored in the KVX file are relative to the start of the // X offsets array. Make them relative to voxdata instead. mipl->OffsetX[i] = GetInt(rawmip + 24 + i * 4) - offsetsize; } // The first X offset must be 0 (since we subtracted offsetsize), according to the spec: // NOTE: xoffset[0] = (xsiz+1)*4 + xsiz*(ysiz+1)*2 (ALWAYS) if (mipl->OffsetX[0] != 0) { break; } // And the final X offset must point just past the end of the voxdata. if (mipl->OffsetX[mipl->SizeX] != voxdatasize) { break; } // Load xy offsets. i = 24 + i * 4; for (j = 0, n *= mipl->SizeY + 1; j < n; ++j) { mipl->OffsetXY[j] = GetShort(rawmip + i + j * 2); } // Ensure all offsets are within bounds. for (i = 0; i < mipl->SizeX; ++i) { int xoff = mipl->OffsetX[i]; for (j = 0; j < mipl->SizeY; ++j) { int yoff = mipl->OffsetXY[(mipl->SizeY + 1) * i + j]; if (unsigned(xoff + yoff) > unsigned(voxdatasize)) { delete voxel; return NULL; } } } // Record slab location for the end. slabs[mip] = (kvxslab_t *)(rawmip + 24 + offsetsize); } // Time for the next mip Level. rawmip += numbytes; maxmipsize -= numbytes + 4; } // Did we get any mip levels, and if so, does the last one leave just // enough room for the palette after it? if (mip == 0 || rawmip != rawvoxel + voxelsize - 768) { delete voxel; return NULL; } // Do not count empty mips at the end. for (; mip > 0; --mip) { if (voxel->Mips[mip - 1].SlabData != NULL) break; } voxel->NumMips = mip; // Fix pivot data for submips, since some tools seem to like to just center these. for (i = 1; i < mip; ++i) { voxel->Mips[i].Pivot = voxel->Mips[i - 1].Pivot / 2; } for (i = 0; i < mip; ++i) { if (!CopyVoxelSlabs((kvxslab_t *)voxel->Mips[i].SlabData, slabs[i], voxel->Mips[i].OffsetX[voxel->Mips[i].SizeX])) { // Invalid slabs encountered. Reject this voxel. delete voxel; return NULL; } } voxel->LumpNum = lumpnum; voxel->Palette = new BYTE[768]; memcpy(voxel->Palette, rawvoxel + voxelsize - 768, 768); return voxel; } //========================================================================== // // // //========================================================================== FVoxelDef *R_LoadVoxelDef(int lumpnum, int spin) { FVoxel *vox = R_LoadKVX(lumpnum); if (vox == NULL) { Printf("%s is not a valid voxel file\n", Wads.GetLumpFullName(lumpnum)); return NULL; } else { FVoxelDef *voxdef = new FVoxelDef; voxdef->Voxel = vox; voxdef->Scale = 1.; voxdef->DroppedSpin = voxdef->PlacedSpin = spin; voxdef->AngleOffset = 90.; Voxels.Push(vox); VoxelDefs.Push(voxdef); return voxdef; } } //========================================================================== // // FVoxelMipLevel Constructor // //========================================================================== FVoxelMipLevel::FVoxelMipLevel() { SizeZ = SizeY = SizeX = 0; Pivot.Zero(); OffsetX = NULL; OffsetXY = NULL; SlabData = NULL; } //========================================================================== // // FVoxelMipLevel Destructor // //========================================================================== FVoxelMipLevel::~FVoxelMipLevel() { if (OffsetX != NULL) { delete[] OffsetX; } } //========================================================================== // // FVoxel Constructor // //========================================================================== FVoxel::FVoxel() { Palette = NULL; } FVoxel::~FVoxel() { if (Palette != NULL) delete [] Palette; } //========================================================================== // // Remap the voxel to the game palette // //========================================================================== void FVoxel::Remap() { if (Palette != NULL) { BYTE *remap = GetVoxelRemap(Palette); for (int i = 0; i < NumMips; ++i) { RemapVoxelSlabs((kvxslab_t *)Mips[i].SlabData, Mips[i].OffsetX[Mips[i].SizeX], remap); } RemovePalette(); } } //========================================================================== // // Delete the voxel's built-in palette // //========================================================================== void FVoxel::RemovePalette() { if (Palette != NULL) { delete [] Palette; Palette = NULL; } } //========================================================================== // // VOX_ReadSpriteNames // // Reads a list of sprite names from a VOXELDEF lump. // //========================================================================== static bool VOX_ReadSpriteNames(FScanner &sc, TArray &vsprites) { vsprites.Clear(); while (sc.GetString()) { // A sprite name list is terminated by an '=' character. if (sc.String[0] == '=') { if (vsprites.Size() == 0) { sc.ScriptMessage("No sprites specified for voxel.\n"); } return true; } if (sc.StringLen != 4 && sc.StringLen != 5) { sc.ScriptMessage("Sprite name \"%s\" is wrong size.\n", sc.String); } else if (sc.StringLen == 5 && (sc.String[4] = toupper(sc.String[4]), sc.String[4] < 'A' || sc.String[4] >= 'A' + MAX_SPRITE_FRAMES)) { sc.ScriptMessage("Sprite frame %c is invalid.\n", sc.String[4]); } else { int frame = (sc.StringLen == 4) ? 255 : sc.String[4] - 'A'; int i = GetSpriteIndex(sc.String, false); if (i != -1) { vsprites.Push((frame << 24) | i); } } } if (vsprites.Size() != 0) { sc.ScriptMessage("Unexpected end of file\n"); } return false; } //========================================================================== // // VOX_ReadOptions // // Reads a list of options from a VOXELDEF lump, terminated with a '}' // character. The leading '{' must already be consumed // //========================================================================== static void VOX_ReadOptions(FScanner &sc, VoxelOptions &opts) { while (sc.GetToken()) { if (sc.TokenType == '}') { return; } sc.TokenMustBe(TK_Identifier); if (sc.Compare("scale")) { sc.MustGetToken('='); sc.MustGetToken(TK_FloatConst); opts.Scale = sc.Float; } else if (sc.Compare("spin")) { int mul = 1; sc.MustGetToken('='); if (sc.CheckToken('-')) mul = -1; sc.MustGetToken(TK_IntConst); opts.DroppedSpin = opts.PlacedSpin = sc.Number*mul; } else if (sc.Compare("placedspin")) { int mul = 1; sc.MustGetToken('='); if (sc.CheckToken('-')) mul = -1; sc.MustGetToken(TK_IntConst); opts.PlacedSpin = sc.Number*mul; } else if (sc.Compare("droppedspin")) { int mul = 1; sc.MustGetToken('='); if (sc.CheckToken('-')) mul = -1; sc.MustGetToken(TK_IntConst); opts.DroppedSpin = sc.Number*mul; } else if (sc.Compare("angleoffset")) { int mul = 1; sc.MustGetToken('='); if (sc.CheckToken('-')) mul = -1; sc.MustGetAnyToken(); if (sc.TokenType == TK_IntConst) { sc.Float = sc.Number; } else { sc.TokenMustBe(TK_FloatConst); } opts.AngleOffset = mul * sc.Float + 90.; } else if (sc.Compare("overridepalette")) { opts.OverridePalette = true; } else { sc.ScriptMessage("Unknown voxel option '%s'\n", sc.String); if (sc.CheckToken('=')) { sc.MustGetAnyToken(); } } } sc.ScriptMessage("Unterminated voxel option block\n"); } //========================================================================== // // VOX_GetVoxel // // Returns a voxel object for the given lump or NULL if it is not a valid // voxel. If the voxel has already been loaded, it will be reused. // //========================================================================== static FVoxel *VOX_GetVoxel(int lumpnum) { // Is this voxel already loaded? If so, return it. for (unsigned i = 0; i < Voxels.Size(); ++i) { if (Voxels[i]->LumpNum == lumpnum) { return Voxels[i]; } } FVoxel *vox = R_LoadKVX(lumpnum); if (vox != NULL) { Voxels.Push(vox); } return vox; } //========================================================================== // // R_InitVoxels // // Process VOXELDEF lumps for defining voxel options that cannot be // condensed neatly into a sprite name format. // //========================================================================== void R_InitVoxels() { int lump, lastlump = 0; while ((lump = Wads.FindLump("VOXELDEF", &lastlump)) != -1) { FScanner sc(lump); TArray vsprites; while (VOX_ReadSpriteNames(sc, vsprites)) { FVoxel *voxeldata = NULL; int voxelfile; VoxelOptions opts; sc.SetCMode(true); sc.MustGetToken(TK_StringConst); voxelfile = Wads.CheckNumForFullName(sc.String, true, ns_voxels); if (voxelfile < 0) { sc.ScriptMessage("Voxel \"%s\" not found.\n", sc.String); } else { voxeldata = VOX_GetVoxel(voxelfile); if (voxeldata == NULL) { sc.ScriptMessage("\"%s\" is not a valid voxel file.\n", sc.String); } } if (sc.CheckToken('{')) { VOX_ReadOptions(sc, opts); } sc.SetCMode(false); if (voxeldata != NULL && vsprites.Size() != 0) { if (opts.OverridePalette) { voxeldata->RemovePalette(); } FVoxelDef *def = new FVoxelDef; def->Voxel = voxeldata; def->Scale = opts.Scale; def->DroppedSpin = opts.DroppedSpin; def->PlacedSpin = opts.PlacedSpin; def->AngleOffset = opts.AngleOffset; VoxelDefs.Push(def); for (unsigned i = 0; i < vsprites.Size(); ++i) { int sprnum = int(vsprites[i] & 0xFFFFFF); int frame = int(vsprites[i] >> 24); if (frame == 255) { // Apply voxel to all frames. for (int j = MAX_SPRITE_FRAMES - 1; j >= 0; --j) { VOX_AddVoxel(sprnum, j, def); } } else { // Apply voxel to only one frame. VOX_AddVoxel(sprnum, frame, def); } } } } } }