/*
** 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.*;
import info.ata4.bsplib.vector.Vector3f;
import info.ata4.bspsrc.BspSourceConfig;
import info.ata4.bspsrc.VmfWriter;
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.TextureAxis;
import info.ata4.bspsrc.modules.texture.TextureBuilder;
import info.ata4.bspsrc.modules.texture.TextureSource;
import info.ata4.bspsrc.modules.texture.ToolTexture;
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.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Decompiling module to write brushes from the LUMP_FACES lump.
*
* Based on several face building methods from Vmex
*
* @author Nico Bergemann <barracuda415 at yahoo.de>
*/
public class FaceSource extends ModuleDecompile {
// logger
private static final Logger L = LogUtils.getLogger();
//BSP=VMF 6=9 2=1 0=0
private static final byte[] TRICONV = {0, 0, 1, 0, 0, 0, 9};
// epsilon for area comparison slop, in mu^2
private static final float AREA_EPS = 1.0f;
// sub-modules
private final BspSourceConfig config;
private final TextureSource texsrc;
private final VmfMeta vmfmeta;
// mapped original faces
public Map<Integer, Set<Integer>> origFaceToSplitFace = new HashMap<>();
// set of face indices that are undersized
private Set<Integer> undersizedFaces = new HashSet<>();
// current offset in multiblend lump
private int multiblendOffset;
public FaceSource(BspFileReader reader, VmfWriter writer, BspSourceConfig config,
TextureSource texsrc, VmfMeta vmfmeta) {
super(reader, writer);
this.config = config;
this.texsrc = texsrc;
this.vmfmeta = vmfmeta;
if (bsp.origFaces.isEmpty()) {
// fix invalid origFace indices when no original faces are available
for (DFace face : bsp.faces) {
face.origFace = -1;
}
} else {
// sync texinfo index, this seems to be required since BSP v20
for (DFace face : bsp.faces) {
if (face.origFace != -1) {
bsp.origFaces.get(face.origFace).texinfo = face.texinfo;
}
}
}
}
/**
* Writes all split faces
*/
public void writeFaces() {
L.info("Writing split faces");
DModel model = bsp.models.get(0); // Model 0 = world brushes
// loop through all faces in M0
for (int i = 0; i < model.numface; i++) {
writeFace(model.fstface + i, false);
}
}
/**
* Writes all original faces. When an original face doesn't exist, it will
* leave gaps in the brush structure.
*/
public void writeOrigFaces() {
L.info("Writing original faces");
// set of face indices that are already written
Set<Integer> writtenFaces = new HashSet<>();
DModel model = bsp.models.get(0); // Model 0 = world brushes
// loop through all faces in M0
for (int i = 0; i < model.numface; i++) {
int iface = model.fstface + i;
DFace face = bsp.faces.get(iface);
// skip if the original face is missing
if (face.origFace < 0) {
continue;
}
// the original face corresponding to this
int iorigface = face.origFace;
// don't write a face more than once
if (writtenFaces.contains(iorigface)) {
continue;
}
writeFace(iorigface, true);
writtenFaces.add(iorigface);
}
}
/**
* Writes all original faces, unless one is undersized, in which case write
* its split faces.
*/
public void writeOrigFacesPlus() {
createFaceMapping();
// set of face indices that are already written
Set<Integer> writtenFaces = new HashSet<>();
L.info("Writing original faces where possible");
DModel model = bsp.models.get(0); // Model 0 = world brushes
int sfaces = 0; // number of written split faces
for (int i = 0; i < model.numface; i++) {
int iface = model.fstface + i;
DFace face = bsp.faces.get(iface);
if (face.origFace >= 0) {
int iorigface = face.origFace;
// don't write a face more than once
if (writtenFaces.contains(iorigface)) {
continue;
}
if (undersizedFaces.contains(iorigface)) {
// oface is undersized! write it as split faces
sfaces++;
Set<Integer> faces = origFaceToSplitFace.get(face.origFace);
// iterate through the corresponding faces
for (Integer iface2 : faces) {
writeFace(iface2, false);
}
if (L.isLoggable(Level.FINEST)) {
StringBuilder sb = new StringBuilder();
sb.append("OF ").append(face.origFace).append(": ");
for (Integer findex : faces) {
sb.append(findex).append(' ');
}
L.finest(sb.toString());
}
} else {
// write this oface as flat
writeFace(face.origFace, true);
}
writtenFaces.add(iorigface);
} else {
// write the face directly
writeFace(iface, false);
}
}
L.log(Level.INFO, "{0} original faces were written as split faces", sfaces);
}
/**
* Writes faces with displacement data only
*/
public void writeDispFaces() {
L.info("Writing displacements");
if (bsp.dispinfos == null || bsp.dispinfos.isEmpty()) {
// no displacements, don't bother searching for matching faces
return;
}
for (int i = 0; i < bsp.faces.size(); i++) {
if (bsp.faces.get(i).dispInfo != -1) {
writeFace(i, false);
}
}
}
public void writeModel(int imodel, Vector3f origin, Vector3f angles) {
DModel model;
try {
model = bsp.models.get(imodel);
} catch (ArrayIndexOutOfBoundsException ex) {
L.log(Level.WARNING, "Invalid model index {0}", imodel);
return;
}
for (int i = 0; i < model.numface; i++) {
writeFace(model.fstface + i, false, origin, angles);
}
}
public void writeModel(int imodel) {
writeModel(imodel, null, null);
}
/**
* Writes a flat face as a brush.
*/
public void writeFace(int iface, boolean orig, Vector3f origin, Vector3f angles) {
DFace face = orig ? bsp.origFaces.get(iface) : bsp.faces.get(iface);
if (face.numedge < 2) {
// 0 or 1 edges? Something must be wrong
return;
}
Winding wind = WindingFactory.fromFace(bsp, face);
// translate to origin
if (origin != null) {
wind = wind.translate(origin);
}
// rotate
if (angles != null) {
wind = wind.rotate(angles);
}
// calculate plane vectors
Vector3f[] plane = wind.buildPlane();
Vector3f e1 = plane[0];
Vector3f e2 = plane[1];
Vector3f e3 = plane[2];
if (!e1.isValid() || !e2.isValid() || !e3.isValid()) {
L.log(Level.WARNING, "Face with wind {0} is invalid", wind);
return;
}
// calculate plane normal
Vector3f ev12 = e2.sub(e1);
Vector3f ev13 = e3.sub(e1);
Vector3f normal = ev12.cross(ev13).normalize();
if (normal.isNaN() || normal.isInfinite()) {
// TODO: is there a way to fix/avoid this?
L.log(Level.FINE, "Bad normal: {0} x {1}", new Object[]{ev12, ev13});
return;
}
writer.start("solid");
writer.put("id", vmfmeta.getUID());
// write metadata for debugging
if (config.isDebug()) {
writer.start("bspsrc_debug");
writer.put("face_index", iface);
writer.put("normal", normal);
writer.put("winding", wind.toString());
if (face.texinfo != -1) {
writer.put("texinfo_index", face.texinfo);
writer.put("texinfo_flags", bsp.texinfos.get(face.texinfo).flags.toString());
}
writer.end("bspsrc_debug");
}
int sideID = vmfmeta.getUID();
// map face index to brush side ID
if (orig) {
vmfmeta.setOrigFaceUID(iface, sideID);
} else {
vmfmeta.setFaceUID(iface, sideID);
}
// build texture
TextureBuilder tb = texsrc.getTextureBuilder();
tb.setOrigin(origin);
tb.setAngles(angles);
tb.setNormal(normal);
tb.setTexinfoIndex(face.texinfo);
Texture texture = tb.build();
// set face texture string
if (!config.faceTexture.isEmpty()) {
texture.setOverrideTexture(config.faceTexture);
}
// add side id to cubemap side list
if (texture.getData() != null) {
texsrc.addBrushSideID(texture.getData().texname, sideID);
}
writer.start("side");
writer.put("id", sideID);
writer.put("plane", e1, e2, e3);
writer.put("smoothing_groups", face.smoothingGroups);
writer.put(texture);
boolean disp = face.dispInfo != -1;
// write displacement?
if (disp && config.writeDisp) {
// map face index to brush side ID
vmfmeta.setDispInfoUID(face.dispInfo, sideID);
// write dispinfo section
writeDisplacement(face.dispInfo);
}
writer.end("side");
// set back face texture string
if (!config.backfaceTexture.isEmpty()) {
texture.setOverrideTexture(config.backfaceTexture);
}
// write prismatic back faces for displacements, pyramidal otherwise
if (disp) {
writePrismBack(wind, texture);
} else {
writePyramBack(wind, texture);
}
writer.end("solid");
}
public void writeFace(int iface, boolean orig) {
writeFace(iface, orig, null, null);
}
/**
* Writes prismatic back brush sides for a face
*/
private void writePrismBack(Winding wind, Texture texture, float depth) {
Vector3f[] plane = wind.buildPlane();
Vector3f e1 = plane[0];
Vector3f e2 = plane[1];
Vector3f e3 = plane[2];
// calculate plane normal
Vector3f ev12 = e2.sub(e1);
Vector3f ev13 = e3.sub(e1);
Vector3f normal = ev12.cross(ev13).normalize();
// displace vertices from face in normal direction by depth
Vector3f bedge = normal.scalar(depth);
e1 = e1.add(bedge);
e2 = e2.add(bedge);
e3 = e3.add(bedge);
writeBackSide(texture, e1, e2, e3);
Vector3f tv2 = bedge.normalize();
// write surrounding sides
int size = wind.size();
for (int i = 0; i < size; i++) {
e1 = wind.get(i);
e2 = wind.get((i + 1) % size);
e3 = e1.add(bedge);
Vector3f tv1 = e2.sub(e1).normalize();
// use null vector if the result is invalid
if (!tv1.isValid()) {
tv1 = Vector3f.NULL;
}
texture.setUAxis(new TextureAxis(tv1));
texture.setVAxis(new TextureAxis(tv2));
writeBackSide(texture, e1, e2, e3);
}
}
private void writePrismBack(Winding wind, Texture texture) {
writePrismBack(wind, texture, config.backfaceDepth);
}
/**
* Writes pyramidal back brush sides for a face
*/
private void writePyramBack(Winding wind, Texture texture, float depth) {
Vector3f[] plane = wind.buildPlane();
Vector3f e1 = plane[0];
Vector3f e2 = plane[1];
Vector3f e3 = plane[2];
// calculate plane normal
Vector3f ev12 = e2.sub(e1);
Vector3f ev13 = e3.sub(e1);
Vector3f normal = ev12.cross(ev13).normalize();
// the coords of the barycenter
e3 = wind.getCenter();
// displace barycenter in normal direction by depth
// results in the apex point for the pyramid
e3 = e3.add(normal.scalar(depth));
// write pyramid sides
int size = wind.size();
for (int i = 0; i < size; i++) {
e1 = wind.get(i);
e2 = wind.get((i + 1) % size);
writeBackSide(texture, e1, e2, e3);
}
}
private void writePyramBack(Winding wind, Texture texture) {
writePyramBack(wind, texture, config.backfaceDepth);
}
public void writeAreaportal(int portalKey) {
for (DAreaportal ap : bsp.areaportals) {
if (ap.portalKey == portalKey) {
writeAreaportal(ap);
// write only once, even though there are two DAreaportal's with
// that key, their geometries are identical
return;
}
}
}
public void writeAreaportal(DAreaportal ap) {
Winding wind = WindingFactory.fromAreaportal(bsp, ap);
// TODO: extrude polygon in the correct direction, currently it seems to be random?
writePolygon(wind, ToolTexture.AREAPORTAL, true);
}
public void writeOccluder(int occluderKey) {
try {
writeOccluder(bsp.occluderDatas.get(occluderKey));
} catch (IndexOutOfBoundsException ex) {
L.log(Level.WARNING, "Invalid occluder key {0}", occluderKey);
}
}
public void writeOccluder(DOccluderData od) {
for (int i = 0; i < od.polycount; i++) {
DOccluderPolyData opd = bsp.occluderPolyDatas.get(od.firstpoly + i);
Winding wind = WindingFactory.fromOccluder(bsp, opd);
// extrude by 8 units instead of one, the skip sides are ignored anyway
writePolygon(wind, ToolTexture.OCCLUDER, ToolTexture.SKIP, true, 8);
}
}
/**
* Writes a brush from raw polygon data.
*
* @param wind winding of the polygon
* @param frontMaterial texture string to use on front brush side
* @param backMaterial texture string to use on back brush sides
* @param prism use prismatic back sides if true, pyramidal otherwise
* @param depth extrude polygon by this depth
*/
public void writePolygon(Winding wind, String frontMaterial, String backMaterial, boolean prism, float depth) {
if (wind.isEmpty() || wind.size() < 3) {
return;
}
Vector3f[] plane = wind.buildPlane();
Vector3f e1 = plane[0];
Vector3f e2 = plane[1];
Vector3f e3 = plane[2];
if (!e1.isValid() || !e2.isValid() || !e3.isValid()) {
L.log(Level.WARNING, "Areaportal with wind {0} is invalid", wind);
return;
}
// calculate plane normal
Vector3f ev12 = e2.sub(e1);
Vector3f ev13 = e3.sub(e1);
Vector3f normal = ev12.cross(ev13).normalize();
if (normal.isNaN() || normal.isInfinite()) {
// TODO: is there a way to fix/avoid this?
L.log(Level.FINE, "Bad normal: {0} x {1}", new Object[]{ev12, ev13});
return;
}
writer.start("solid");
writer.put("id", vmfmeta.getUID());
int sideID = vmfmeta.getUID();
// build texture
TextureBuilder tb = texsrc.getTextureBuilder();
tb.setNormal(normal);
Texture texture = tb.build();
texture.setOriginalTexture(frontMaterial);
writer.start("side");
writer.put("id", sideID);
writer.put("plane", e1, e2, e3);
writer.put(texture);
writer.end("side");
texture.setOriginalTexture(backMaterial);
if (prism) {
writePrismBack(wind, texture, depth);
} else {
writePyramBack(wind, texture, depth);
}
writer.end("solid");
}
public void writePolygon(Winding wind, String frontMaterial, String backMaterial, boolean prism) {
writePolygon(wind, frontMaterial, backMaterial, prism, config.backfaceDepth);
}
public void writePolygon(Winding wind, String material, boolean prism, float depth) {
writePolygon(wind, material, material, prism, depth);
}
public void writePolygon(Winding wind, String material, boolean prism) {
writePolygon(wind, material, material, prism);
}
/**
* Write a brush side with the given texture and plane
*/
private void writeBackSide(Texture texture, Vector3f e1, Vector3f e2, Vector3f e3) {
writer.start("side");
writer.put("id", vmfmeta.getUID());
writer.put("plane", e1, e3, e2);
writer.put("smoothing_groups", 0);
writer.put(texture);
writer.end("side");
}
/**
* Writes dispinfo data for a brush side
*
* @param idispinfo dispinfo index
*/
public void writeDisplacement(int idispinfo) {
DDispInfo di = bsp.dispinfos.get(idispinfo);
Map<String, String> normalMap = new LinkedHashMap<>();
Map<String, String> distanceMap = new LinkedHashMap<>();
Map<String, String> alphaMap = new LinkedHashMap<>();
Map<String, String> triangleTagMap = new LinkedHashMap<>();
Map<String, String> multiBlendMap = new LinkedHashMap<>();
Map<String, String> alphaBlendMap = new LinkedHashMap<>();
List<Map<String, String>> multiBlendColorMaps = new ArrayList<>(DDispMultiBlend.MAX_MULTIBLEND_CHANNELS);
for (int i = 0; i < DDispMultiBlend.MAX_MULTIBLEND_CHANNELS; i++) {
multiBlendColorMaps.add(new LinkedHashMap<String, String>());
}
StringBuilder normalSb = new StringBuilder();
StringBuilder distanceSb = new StringBuilder();
StringBuilder alphaSb = new StringBuilder();
StringBuilder multiblendSb = new StringBuilder();
StringBuilder alphablendSb = new StringBuilder();
List<StringBuilder> multiblendColorSbs = new ArrayList<>(DDispMultiBlend.MAX_MULTIBLEND_CHANNELS);
for (int i = 0; i < DDispMultiBlend.MAX_MULTIBLEND_CHANNELS; i++) {
multiblendColorSbs.add(new StringBuilder());
}
StringBuilder triangleTagSb = new StringBuilder();
StringBuilder allowedVertSb = new StringBuilder();
final int vertcount = di.getVertexCount();
final int psize = di.getPowerSize();
final boolean hasMultiBlend = !bsp.dispmultiblend.isEmpty() && di.hasMultiBlend();
// build vertex related strings
for (int i = 0; i < vertcount; i++) {
DDispVert dv = bsp.dispverts.get(di.dispVertStart + i);
DDispMultiBlend dmb = null;
if (hasMultiBlend) {
dmb = bsp.dispmultiblend.get(multiblendOffset + i);
}
// normals
normalSb.append(dv.vector.x);
normalSb.append(" ");
normalSb.append(dv.vector.y);
normalSb.append(" ");
normalSb.append(dv.vector.z);
// distance
distanceSb.append(dv.dist);
// alpha
alphaSb.append(dv.alpha);
if (hasMultiBlend) {
// multiblend
multiblendSb.append(dmb.multiblend.x);
multiblendSb.append(" ");
multiblendSb.append(dmb.multiblend.y);
multiblendSb.append(" ");
multiblendSb.append(dmb.multiblend.z);
multiblendSb.append(" ");
multiblendSb.append(dmb.multiblend.w);
alphablendSb.append(dmb.alphablend.x);
alphablendSb.append(" ");
alphablendSb.append(dmb.alphablend.y);
alphablendSb.append(" ");
alphablendSb.append(dmb.alphablend.z);
alphablendSb.append(" ");
alphablendSb.append(dmb.alphablend.w);
for (int j = 0; j < dmb.multiblendcolors.length; j++) {
StringBuilder mbcsb = multiblendColorSbs.get(j);
mbcsb.append(dmb.multiblendcolors[j].x);
mbcsb.append(" ");
mbcsb.append(dmb.multiblendcolors[j].y);
mbcsb.append(" ");
mbcsb.append(dmb.multiblendcolors[j].z);
}
}
// check for new row
if (i % (psize + 1) == psize) {
normalMap.put("row" + normalMap.size(), normalSb.toString());
distanceMap.put("row" + distanceMap.size(), distanceSb.toString());
alphaMap.put("row" + alphaMap.size(), alphaSb.toString());
normalSb.setLength(0);
distanceSb.setLength(0);
alphaSb.setLength(0);
// multiblend
if (hasMultiBlend) {
multiBlendMap.put("row" + multiBlendMap.size(), multiblendSb.toString());
alphaBlendMap.put("row" + alphaBlendMap.size(), alphablendSb.toString());
for (int j = 0; j < dmb.multiblendcolors.length; j++) {
Map<String, String> multiBlendColorMap = multiBlendColorMaps.get(j);
multiBlendColorMap.put("row" + multiBlendColorMap.size(),
multiblendColorSbs.get(j).toString());
}
multiblendSb.setLength(0);
alphablendSb.setLength(0);
for (int j = 0; j < dmb.multiblendcolors.length; j++) {
multiblendColorSbs.get(j).setLength(0);
}
}
} else {
normalSb.append(" ");
distanceSb.append(" ");
alphaSb.append(" ");
if (hasMultiBlend) {
multiblendSb.append(" ");
alphablendSb.append(" ");
for (int j = 0; j < dmb.multiblendcolors.length; j++) {
multiblendColorSbs.get(j).append(" ");
}
}
}
}
// count up multiblend index
if (hasMultiBlend) {
multiblendOffset += vertcount;
}
// build triangle tags
int tcount = di.getTriangleTagCount();
for (int i = 0; i < tcount; i++) {
int dt = bsp.disptris.get(di.dispTriStart + i).tags;
if (dt < 0 || dt > 6) {
dt = 0;
}
triangleTagSb.append(TRICONV[dt]);
// check for new row
if (i % 2 * psize == 2 * psize - 1) {
triangleTagMap.put("row" + triangleTagMap.size(), triangleTagSb.toString());
triangleTagSb.setLength(0);
} else {
triangleTagSb.append(" ");
}
}
// build allowed vertices
for (int i = 0; i < di.allowedVerts.length; i++) {
allowedVertSb.append(di.allowedVerts[i]);
if (i < di.allowedVerts.length - 1) {
allowedVertSb.append(" ");
}
}
// write VMF data
writer.start("dispinfo");
if (config.isDebug()) {
writer.put("bspsrc_dispinfo_index", idispinfo);
}
writer.put("power", di.power);
writer.put("startposition", di.startPos, 2);
writer.put("elevation", 0);
writer.put("subdiv", 0);
writer.start("normals");
writer.put(normalMap);
writer.end("normals");
writer.start("distances");
writer.put(distanceMap);
writer.end("distances");
writer.start("alphas");
writer.put(alphaMap);
writer.end("alphas");
writer.start("triangle_tags");
writer.put(triangleTagMap);
writer.end("triangle_tags");
writer.start("allowed_verts");
writer.put("10", allowedVertSb.toString());
writer.end("allowed_verts");
// Multiblend
if (hasMultiBlend) {
writer.start("multiblend");
writer.put(multiBlendMap);
writer.end("multiblend");
writer.start("alphablend");
writer.put(alphaBlendMap);
writer.end("alphablend");
for (int j = 0; j < DDispMultiBlend.MAX_MULTIBLEND_CHANNELS; j++) {
writer.start("multiblend_color_" + j);
writer.put(multiBlendColorMaps.get(j));
writer.end("multiblend_color_" + j);
}
}
writer.end("dispinfo");
}
/**
* Builds a HashSet array of all faces corresponding to i'th orig face.
* Also calculates the area of ofaces.
*/
private void createFaceMapping() {
L.info("Building split face to original face maps");
// look at every face
for (int i = 0; i < bsp.faces.size(); i++) {
int o = bsp.faces.get(i).origFace;
// must check for no face correspondence
if (o == -1) {
continue;
}
Set<Integer> faceSet;
if (origFaceToSplitFace.containsKey(o)) {
faceSet = origFaceToSplitFace.get(o);
} else {
faceSet = new HashSet<>();
origFaceToSplitFace.put(o, faceSet);
}
// add this face to the set
faceSet.add(i);
}
L.info("Building original face areas");
// look at every oface
for (int i = 0; i < bsp.origFaces.size(); i++) {
DFace origFace = bsp.origFaces.get(i);
// recalculate face area when required
if (origFace.area == 0) {
Winding wind = WindingFactory.fromFace(bsp, origFace);
origFace.area = wind.getArea();
}
if (L.isLoggable(Level.FINEST)) {
L.log(Level.FINEST, "OF {0}: area {1}", new Object[]{i, origFace.area});
}
// area of face components
float carea = 0;
Set<Integer> faceSet = origFaceToSplitFace.get(i);
// iterate through the corresponding split faces
for (Integer face : faceSet) {
// add up the areas of all split faces
carea += bsp.faces.get(face).area;
}
// components are bigger, within slop
if (carea > origFace.area + AREA_EPS) {
undersizedFaces.add(i); // mark the oface
if (L.isLoggable(Level.FINEST)) {
L.log(Level.FINEST, "OF {0} is undersized: {1}>{2}",
new Object[]{i, carea, origFace.area});
}
}
}
}
}