/* ** 2011 April 5 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. */ package info.ata4.bspsrc.modules.geom; import info.ata4.bsplib.BspFileReader; import info.ata4.bsplib.struct.DBrush; import info.ata4.bsplib.struct.DBrushSide; import info.ata4.bsplib.struct.DModel; import info.ata4.bsplib.vector.Vector3f; import info.ata4.bspsrc.BspSourceConfig; import info.ata4.bspsrc.VmfWriter; import info.ata4.bspsrc.modules.BspProtection; import info.ata4.bspsrc.modules.ModuleDecompile; import info.ata4.bspsrc.modules.VmfMeta; import info.ata4.bspsrc.modules.texture.Texture; import info.ata4.bspsrc.modules.texture.TextureBuilder; import info.ata4.bspsrc.modules.texture.TextureSource; import info.ata4.bspsrc.util.BspTreeStats; import info.ata4.bspsrc.util.Winding; import info.ata4.bspsrc.util.WindingFactory; import info.ata4.log.LogUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * Decompiling module to rebuild brushes from the LUMP_BRUSHES and LUMP_BRUSHSIDES lumps. * * Based on Vmex.vmfbrushes() and Vmex.writebrush() * * @author Nico Bergemann <barracuda415 at yahoo.de> */ public class BrushSource extends ModuleDecompile { // logger private static final Logger L = LogUtils.getLogger(); // sub-modules private final BspSourceConfig config; private final TextureSource texsrc; private final BspProtection bspprot; private final VmfMeta vmfmeta; // additional model data private List<DBrushModel> models = new ArrayList<>(); // amount of world brushes private int worldbrushes = 0; // brush side ID mappings private Map<Integer, Integer> brushSideToID = new HashMap<>(); private Map<Integer, Integer> brushIndexToID = new HashMap<>(); public BrushSource(BspFileReader reader, VmfWriter writer, BspSourceConfig config, TextureSource texsrc, BspProtection bspprot, VmfMeta vmfmeta) { super(reader, writer); this.config = config; this.texsrc = texsrc; this.bspprot = bspprot; this.vmfmeta = vmfmeta; assignBrushes(); } /** * Returns the brush side VMF ID for the corresponding brush side index. * The brush side must have been previously written via * {@link #writeSide writeSide}. * * @param ibrushside brush side index * @return brush side ID or -1 if the index isn't mapped yet */ public int getBrushSideIDForIndex(int ibrushside) { if (brushSideToID.containsKey(ibrushside)) { return brushSideToID.get(ibrushside); } // not found return -1; } /** * Returns the brush VMF ID for the corresponding brush index. * The brush must have been previously written via * {@link #writeBrush writeBrush}. * * @param ibrush brush index * @return brush ID or -1 if the index isn't mapped yet */ public int getBrushIDForIndex(int ibrush) { if (brushIndexToID.containsKey(ibrush)) { return brushIndexToID.get(ibrush); } // not found return -1; } /** * Walks the map's BSP tree to associate brushes with entities and to find * the index of the last worldbrush. */ private void assignBrushes() { // walk the BSP tree // to from the headnode of each model // to calculate the minimum and maximum brush in the tree // much simpler than the guessing method // plus this recovers null-faced brushes BspTreeStats tl = new BspTreeStats(bsp); // walk model 0 (worldspawn model) tl.walk(0); L.fine("Walked worldspawn tree"); worldbrushes = tl.getMaxBrushLeaf() + 1; for (DModel model : bsp.models) { tl.reset(); tl.walk(model.headnode); DBrushModel bmodel = new DBrushModel(); bmodel.fstbrush = tl.getMinBrushLeaf(); bmodel.numbrush = tl.getMaxBrushLeaf() - tl.getMinBrushLeaf() + 1; models.add(bmodel); } L.log(Level.FINE, "Largest worldbrush: {0}", worldbrushes); } /** * Writes all world brushes. Depending on the settings, some brushes may be * skipped so the entity decompiler can use them. */ public void writeBrushes() { L.info("Writing brushes and planes"); for (int i = 0; i < worldbrushes; i++) { DBrush brush = bsp.brushes.get(i); // skip details if (config.writeDetails && brush.isSolid() && brush.isDetail()) { continue; } // skip areaportals if (config.writeAreaportals && brush.isAreaportal()) { continue; } // skip ladders if (config.writeLadders && brush.isLadder()) { continue; } // NOTE: occluder brushes aren't worldbrushes, so they don't need to // be handled here writeBrush(i); } } public boolean writeBrush(int ibrush, Vector3f origin, Vector3f angles) { DBrush brush = bsp.brushes.get(ibrush); int brushID = vmfmeta.getUID(); // map brush index to ID brushIndexToID.put(ibrush, brushID); Map<Integer, Winding> validBrushSides = new HashMap<>(); // check and preprocess the brush sides before writing the brush for (int i = 0; i < brush.numside; i++) { int ibrushside = brush.fstside + i; DBrushSide brushSide = bsp.brushSides.get(ibrushside); // don't output surplus bevel faces - they lead to bad brushes if (brushSide.bevel) { continue; } try { Winding wind = WindingFactory.fromSide(bsp, brush, brushSide).removeDegenerated(); // skip sides with no vertices if (wind.isEmpty()) { throw new BrushSideException("no vertices"); } // skip sides with too few vertices if (wind.size() < 3) { throw new BrushSideException("less than 3 vertices"); } // skip sides that are way too big if (wind.isHuge()) { throw new BrushSideException("too big"); } Vector3f[] plane = wind.buildPlane(); Vector3f e1 = plane[0]; Vector3f e2 = plane[1]; Vector3f e3 = plane[2]; if (!e1.isValid() || !e2.isValid() || !e3.isValid()) { throw new BrushSideException("invalid plane"); } // Check for duplicate plane points. All three plane points must // be unique or it isn't a valid plane. for (int p1 = 0; p1 < plane.length; p1++) { for (int p2 = 0; p2 < plane.length; p2++) { if (p1 == p2) { continue; } Vector3f v1 = plane[p1]; Vector3f v2 = plane[p2]; if (v1.equals(v2)) { throw new BrushSideException("duplicate plane point " + v1); } } } // rotate if (angles != null) { wind = wind.rotate(angles); } // translate to origin if (origin != null) { wind = wind.translate(origin); } // the brush side should be safe to write validBrushSides.put(ibrushside, wind); } catch (BrushSideException ex) { if (config.isDebug()) { L.log(Level.WARNING, "Skipped side {0} of brush {1}: {2}", new Object[]{i, ibrush, ex.getMessage()}); } } } // all brush sides invalid = invalid brush if (validBrushSides.isEmpty()) { L.log(Level.WARNING, "Skipped empty brush {0}", ibrush); return false; } // skip brushes with less than three sides, they can't be compiled and // may crash older Hammer builds if (validBrushSides.size() < 3) { L.log(Level.WARNING, "Skipped brush {0} with less than 3 sides", ibrush); return false; } // now write the brush writer.start("solid"); writer.put("id", brushID); // write metadata for debugging if (config.isDebug()) { writer.start("bspsrc_debug"); writer.put("brush_index", ibrush); writer.put("brush_contents", brush.contents.toString()); writer.end("bspsrc_debug"); } // write valid sides only for (Map.Entry<Integer, Winding> entry : validBrushSides.entrySet()) { int ibrushside = entry.getKey(); Winding wind = entry.getValue(); writeSide(ibrushside, ibrush, wind, origin, angles); } // add visgroup metadata if this is a protector detail brush if (!brush.isDetail() && bspprot.isProtectedBrush(brush)) { vmfmeta.writeMetaVisgroup("VMEX protector brushes"); } writer.end("solid"); return true; } public boolean writeBrush(int ibrush) { return writeBrush(ibrush, null, null); } private boolean writeSide(int ibrushside, int ibrush, Winding wind, Vector3f origin, Vector3f angles) { DBrushSide brushSide = bsp.brushSides.get(ibrushside); // calculate plane vectors Vector3f[] plane = wind.buildPlane(); Vector3f e1 = plane[0]; Vector3f e2 = plane[1]; Vector3f e3 = plane[2]; // calculate plane normal // NOTE: the plane normal from the BSP could be invalid if the brush was // rotated! better re-calculate it every time. Vector3f ev12 = e2.sub(e1); Vector3f ev13 = e3.sub(e1); Vector3f normal = ev12.cross(ev13).normalize(); // build texture TextureBuilder tb = texsrc.getTextureBuilder(); tb.setOrigin(origin); tb.setAngles(angles); tb.setNormal(normal); tb.setTexinfoIndex(brushSide.texinfo); tb.setBrushIndex(ibrush); tb.setBrushSideIndex(ibrushside); Texture texture = tb.build(); // set custom face texture string if (!config.faceTexture.isEmpty()) { texture.setOverrideTexture(config.faceTexture); } int sideID = vmfmeta.getUID(); // add side id to cubemap side list if (texture.getData() != null) { texsrc.addBrushSideID(texture.getData().texname, sideID); } // map brush side index to brush side ID brushSideToID.put(ibrushside, sideID); writer.start("side"); writer.put("id", sideID); // write metadata for debugging if (config.isDebug()) { writer.start("bspsrc_debug"); writer.put("brushside_index", ibrushside); writer.put("normal", normal); writer.put("winding", wind.toString()); if (texture.getOverrideTexture() != null) { writer.put("original_material", texture.getOriginalTexture()); } if (brushSide.texinfo != -1) { writer.put("texinfo_index", brushSide.texinfo); writer.put("texinfo_flags", bsp.texinfos.get(brushSide.texinfo).flags.toString()); } writer.end("bspsrc_debug"); } writer.put("plane", e1, e2, e3); writer.put("smoothing_groups", 0); writer.put(texture); writer.end("side"); return true; } public boolean writeModel(int imodel, Vector3f origin, Vector3f angles) { DBrushModel bmodel; try { bmodel = models.get(imodel); } catch (IndexOutOfBoundsException ex) { L.log(Level.WARNING, "Invalid model index {0}", imodel); return false; } for (int i = 0; i < bmodel.numbrush; i++) { writeBrush(bmodel.fstbrush + i, origin, angles); } return true; } public boolean writeModel(int imodel) { return writeModel(imodel); } public int getWorldbrushes() { return worldbrushes; } private class DBrushModel { private int fstbrush; private int numbrush; } private class BrushSideException extends Exception { BrushSideException(String message) { super(message); } } }