bluehorizons

QuadTrees :D

May 4th, 2026
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 6.42 KB | None | 0 0
  1. package william.starsight.graphics.texture;
  2.  
  3. import org.jetbrains.annotations.Contract;
  4. import org.jetbrains.annotations.NotNull;
  5.  
  6. import java.awt.image.BufferedImage;
  7. import java.util.ArrayList;
  8. import java.util.HashMap;
  9. import java.util.List;
  10.  
  11. /**
  12.  * A quad-tree based texture atlas
  13.  *
  14.  * @author William
  15.  */
  16. public class TextureAtlas {
  17.  
  18.     private final QuadTreeNode root = new QuadTreeNode();
  19.  
  20.     {
  21.         root.sideLength = 8 * 256;
  22.     }
  23.  
  24.     public final HashMap<String, QuadTreeNode> textures = new HashMap<>();
  25.  
  26.     public void placeTexture(@NotNull String name, @NotNull BufferedImage contents) {
  27.         if (contents.getWidth() != contents.getHeight()) {
  28.             throw new IllegalArgumentException("The BufferedImage is not square.");
  29.         }
  30.         int sideLength = contents.getWidth();
  31.         if (!((sideLength != 0) && ((sideLength & (sideLength - 1)) == 0))) { // If side length is not a power of 2
  32.             throw new IllegalArgumentException("Side length must be a power of 2!");
  33.         }
  34.  
  35. //        QuadTreeNode node = searchForSmallestSpace(root, sideLength);
  36. //      Also preserving this.
  37. //        if (node.sideLength < sideLength) throw new IllegalStateException("What da");
  38.  
  39.         List<QuadTreeNode> leaves = getRecursiveLeaves(root).stream()
  40.                 .filter(n -> n.contents == null) // Make sure that the node is empty
  41.                 .filter(n -> n.sideLength >= sideLength)
  42.                 .sorted() // Sort by size
  43.                 .toList();
  44.  
  45.         var node = leaves.getFirst(); // The smallest is the first element
  46.  
  47.         /* Split up nodes and dive into first one */
  48.         while (node.sideLength > sideLength) {
  49.             node.isOccupied = true;
  50.             node.subNode1 = new QuadTreeNode(); // Split it up :D
  51.             node.subNode2 = new QuadTreeNode();
  52.             node.subNode3 = new QuadTreeNode();
  53.             node.subNode4 = new QuadTreeNode();
  54.  
  55.             int halfLength = node.sideLength / 2;
  56.  
  57.             node.subNode1.sideLength = halfLength; // Set side lengths
  58.             node.subNode2.sideLength = halfLength;
  59.             node.subNode3.sideLength = halfLength;
  60.             node.subNode4.sideLength = halfLength;
  61.  
  62.             node.subNode2.leftX += halfLength; // Set origin
  63.             node.subNode3.leftX += halfLength;
  64.             node.subNode3.topY += halfLength;
  65.             node.subNode4.topY += halfLength;
  66.  
  67.             node = node.subNode1;
  68.         }
  69.         node.contents = contents;
  70.         node.isOccupied = true;
  71.         node.name = name;
  72.  
  73.         textures.put(name, node);
  74.     }
  75.  
  76.     /*
  77.     I'm preserving this because this was my first attempt at this, and I'm feeling nostalgic
  78.  
  79.     /// Search for smallest node in the thing that is still big enough to fit target
  80.     private QuadTreeNode searchForSmallestSpace(QuadTreeNode node, int target) { // NOTE: This may be too complex. I may just be able to find all leaf nodes and get smallest. This one is bugprone.
  81.         if (node.contents != null) { // If already filled
  82.             return null;
  83.         }
  84.         if (!node.isOccupied) { // If unoccupied by content or subnodes, then check to see if it fits in.
  85.             if (node.sideLength >= target) {
  86.                 return node;
  87.             } else return null; // Too small; discard. This shouldn't happen
  88.         }
  89.         var sub1 = searchForSmallestSpace(node.subNode1, target); // Check all subnodes recursively to get to leaves
  90.         var sub2 = searchForSmallestSpace(node.subNode2, target);
  91.         var sub3 = searchForSmallestSpace(node.subNode3, target);
  92.         var sub4 = searchForSmallestSpace(node.subNode4, target);
  93.  
  94.         // Get smallest of those leaves that fit the target and aren't null.
  95.         List<QuadTreeNode> nodes = List.of(sub1, sub2, sub3, sub4);
  96.         nodes = nodes.stream().filter(Objects::nonNull).sorted().toList(); // .filter may be unnecessary here.
  97.         return nodes.getLast(); // whichever is smallest.
  98.     }
  99. */
  100.     @NotNull
  101.     @Contract(pure = true)
  102.     private List<QuadTreeNode> getRecursiveLeaves(@NotNull QuadTreeNode node) {
  103.         if (!node.isOccupied || node.contents != null) { // Is a leaf node, either empty or content filled
  104.             return List.of(node);
  105.         }
  106.  
  107.         ArrayList<QuadTreeNode> pile = new ArrayList<>();
  108.         pile.addAll(getRecursiveLeaves(node.subNode1));
  109.         pile.addAll(getRecursiveLeaves(node.subNode2));
  110.         pile.addAll(getRecursiveLeaves(node.subNode3));
  111.         pile.addAll(getRecursiveLeaves(node.subNode4));
  112.  
  113.         return pile;
  114.     }
  115.  
  116.     public static final class QuadTreeNode implements Comparable<QuadTreeNode> {
  117.         private QuadTreeNode subNode1 = null;
  118.         private QuadTreeNode subNode2 = null;
  119.         private QuadTreeNode subNode3 = null;
  120.         private QuadTreeNode subNode4 = null;
  121.         private boolean isOccupied = false; // If either any subnodes are non-null or contents is non-null
  122.         private BufferedImage contents = null; // Will be null if any of the above are not null
  123.         private String name = null;
  124.  
  125.         public int getLeftX() {
  126.             return leftX;
  127.         }
  128.  
  129.         public int getTopY() {
  130.             return topY;
  131.         }
  132.  
  133.         public int getSideLength() {
  134.             return sideLength;
  135.         }
  136.  
  137.         private int leftX = 0;
  138.         private int topY = 0;
  139.         private int sideLength = 0;
  140.  
  141.         @Override
  142.         public int compareTo(@NotNull QuadTreeNode o) { // Sorts out the sizes
  143.             return Integer.compare(sideLength, o.sideLength);
  144.         }
  145.     }
  146.  
  147.     /**
  148.      * Flushes atlas to texture
  149.      *
  150.      * @return The finished UNINITIALIZED texture
  151.      */
  152.     public Texture flushTexture() {
  153.         BufferedImage img = new BufferedImage(root.sideLength, root.sideLength, BufferedImage.TYPE_INT_ARGB); // then I can put this in :D
  154.  
  155.         List<QuadTreeNode> placements = new ArrayList<>(textures.values());
  156.  
  157.         var g = img.createGraphics(); // NOTE Could be a VERY temporary solution
  158.  
  159.         for (QuadTreeNode q : placements) {
  160.             int x = q.leftX;
  161.             int y = q.topY;
  162.             int w = q.sideLength;
  163.             int h = q.sideLength;
  164.  
  165.             g.drawImage(q.contents, x, y, w, h, null); // Hopefully this smooths over the ARGB and ABGR issues; I'd hate having to deal with something that dumb.
  166.         }
  167.  
  168.         g.dispose();
  169.  
  170.         return new BufferedImageBasedTexture(img, true);
  171.     }
  172. }
Tags: programming
Add Comment
Please, Sign In to add comment