code: /*
   * Copyright (c) 2021 Callum Newlands
   *
   *     This program is free software: you can redistribute it and/or modify
   *     it under the terms of the GNU General Public License as published by
   *     the Free Software Foundation, either version 3 of the License, or
   *     (at your option) any later version.
   *
   *     The additional term of 7.b applies: Requiring preservation of specified
   *     reasonable legal notices or author attributions in that material or in the
   *     Appropriate Legal Notices displayed by works containing it
   *
   *     This program is distributed in the hope that it will be useful,
   *     but WITHOUT ANY WARRANTY; without even the implied warranty of
   *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   *     GNU General Public License for more details.
   *
   *     You should have received a copy of the GNU General Public License
   *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
   */
  
  package sceneobjects;
  
  import java.util.Comparator;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Random;
  import java.util.stream.Collectors;
  
  import static rendering.ShaderPrograms.billboardShaderProgram;
  import static rendering.ShaderPrograms.leafShaderProgram;
  import static rendering.ShaderPrograms.textureShader;
  
  import generation.TreeLSystems;
  import generation.TreePool;
  import generation.TurtleInterpreter;
  import lombok.AllArgsConstructor;
  import lombok.Getter;
  import lombok.Setter;
  import lsystems.modules.Module;
  import lsystems.modules.ParametricValueModule;
  import modeldata.LODModel;
  import modeldata.LODModelBuilder;
  import modeldata.SingleModel;
  import modeldata.meshdata.Mesh;
  import modeldata.meshdata.Vertex;
  import modeldata.meshdata.VertexAttribute;
  import org.apache.commons.lang3.NotImplementedException;
  import org.joml.Matrix4f;
  import org.joml.Vector2f;
  import org.joml.Vector3f;
  import params.ParameterLoader;
  import params.Parameters;
  import params.TreeTypes;
  import rendering.LevelOfDetail;
  import rendering.Textures;
  import utils.MathsUtils;
  import utils.MeshUtils;
  import utils.VectorUtils;
  
  public class Tree {
  
  	private static final Parameters parameters = ParameterLoader.getParameters();
  	private final static Vector3f up = new Vector3f(0f, 1f, 0f);
  	private final static Vector3f out = new Vector3f(0f, 0f, 1f);
  	public final static Mesh leaf = new Mesh(
  			List.of(
  					new Vertex(new Vector3f(0f, 0f, -0.5f), up, out, new Vector2f(0, 0)),
  					new Vertex(new Vector3f(1f, 0f, -0.5f), up, out, new Vector2f(0, 1)),
  					new Vertex(new Vector3f(1f, 0f, 0.5f), up, out, new Vector2f(1, 1)),
  					new Vertex(new Vector3f(0f, 0f, 0.5f), up, out, new Vector2f(1, 0))
  			),
  			new int[] {0, 1, 3, 1, 2, 3},
  			List.of(
  					VertexAttribute.POSITION,
  					VertexAttribute.NORMAL,
  					VertexAttribute.TANGENT,
  					VertexAttribute.TEXTURE)
  	);
  	private final int typeIndex;
  	private LODModel model;
  	@Getter
  	private Mask mask = new Mask();
  	@Getter
  	private final int numIterations;
  
  	public Tree(int typeIndex) {
  		this(typeIndex, Optional.empty());
  	}
  
  	public Tree(int typeIndex, int numIterations) {
  		this(typeIndex, Optional.of(numIterations));
  	}
  
  	private Tree(int typeIndex, Optional<Integer> numIterations) {
  		this.typeIndex = typeIndex;
  		if (numIterations.isEmpty()) {
  			Parameters.SceneObjects.Tree params = parameters.sceneObjects.trees.get(typeIndex);
  			Random r = ParameterLoader.getParameters().random.generator;
  			int minI = params.minIterations;
  			int maxI = params.maxIterations;
  			this.numIterations = r.nextInt(maxI - minI) + minI;
  		} else {
  			this.numIterations = numIterations.get();
  		}
  
  		Map<LevelOfDetail, List<Mesh>> lodMeshes = getMeshes();
  
  		LODModelBuilder modelBuilder = new LODModelBuilder();
  		// For each LOD, construct the mesh representation
  		for (LevelOfDetail lod : lodMeshes.keySet()) {
  			List<Mesh> meshes = lodMeshes.get(lod);
  			modelBuilder.withLODModel(lod, new SingleModel(meshes));
  		}
  		model = modelBuilder.build();
  	}
  
  	public void setModelMatrix(Matrix4f modelMat) {
  		this.model.setModelMatrix(modelMat);
  	}
  
  	public void render(LevelOfDetail levelOfDetail, boolean renderForShadows) {
  		model.render(levelOfDetail, renderForShadows);
  	}
  
  	Map<LevelOfDetail, List<Mesh>> getMeshes() {
  
  		Parameters.SceneObjects.Tree params = parameters.sceneObjects.trees.get(typeIndex);
  		int lowLODEdges = params.lowLODEdges;
  		int lowLODLeafMerges = params.lowLODLeafMerges;
  
  		int numEdges = params.numSides;
  		TurtleInterpreter turtleInterpreter = new TurtleInterpreter(numEdges);
  		turtleInterpreter.setSubModels(List.of(MeshUtils.transform(leaf,
  				new Matrix4f().scale(params.leafYScale / params.scale, 1, params.leafXScale / params.scale))));
  		TurtleInterpreter lowLODInterpreter = new TurtleInterpreter(lowLODEdges);
  		lowLODInterpreter.setSubModels(List.of(MeshUtils.transform(leaf,
  				new Matrix4f().scale(
  						params.leafYScale * (1 + 0.1f * lowLODLeafMerges) / params.scale,
  						1,
  						params.leafXScale * (2 * lowLODLeafMerges) / params.scale))));
  		List<Module> instructions;
  		if (params instanceof TreeTypes.BranchingTree) {
  			turtleInterpreter.setIgnored(List.of('A'));
  			lowLODInterpreter.setIgnored(List.of('A'));
  			instructions = TreeLSystems.branching(typeIndex)
  					.performDerivations(numIterations)
  					.stream()
  					.map(m -> m.getName() == 'A' ? new ParametricValueModule('~', 0f) : m)
  					.collect(Collectors.toList());
  		} else if (params instanceof TreeTypes.MonopodialTree) {
  			turtleInterpreter.setIgnored(List.of('A', 'B'));
  			lowLODInterpreter.setIgnored(List.of('A', 'B'));
  			instructions = TreeLSystems.monopodial(typeIndex)
  					.performDerivations(numIterations);
  		} else {
  			throw new NotImplementedException();
  		}
  		turtleInterpreter.interpretInstructions(instructions);
  		lowLODInterpreter.interpretInstructions(instructions);
  
  		Textures.TreeTextures treeTextures = Textures.treeTextures.get(typeIndex);
  		Mesh branches = turtleInterpreter.getMesh();
  		branches.addTexture("diffuseTexture", treeTextures.bark);
  		branches.addTexture("normalTexture", treeTextures.barkNormal);
  		branches.addTexture("specularTexture", treeTextures.barkGlossiness);
  		branches.setShaderProgram(textureShader);
  
  		Mesh canopy = turtleInterpreter.getCombinedSubModelMeshes().get(0);
  		canopy.addTexture("leafFront", treeTextures.leafFront);
  		canopy.addTexture("leafFrontTranslucency", treeTextures.leafFrontT);
  		canopy.addTexture("leafFrontNorm", treeTextures.leafFrontNorm);
  		canopy.addTexture("leafFrontHalfLife", treeTextures.leafFrontHL);
  		canopy.addTexture("leafBack", treeTextures.leafBack);
  		canopy.addTexture("leafBackTranslucency", treeTextures.leafBackT);
  		canopy.addTexture("leafBackNorm", treeTextures.leafBackNorm);
  		canopy.addTexture("leafBackHalfLife", treeTextures.leafBackHL);
  		canopy.setShaderProgram(leafShaderProgram);
  		canopy.setColourFilter(params.leafColourFilter);
  		canopy.setLeaf(true);
  
  		findCanopyMask(branches, canopy);
  
  		// Uses leaf geometry to construct billboard
  //		Mesh board = MeshUtils.transform(Tree.leaf, new Matrix4f()
  //				.scale(1f, 10f / params.scale, 1f / params.scale)
  //				.rotate((float) Math.PI / 2, out));
  		Mesh board = lowLODInterpreter.getMesh();
  		board.addTexture("diffuseTexture", treeTextures.bark);
  		board.addTexture("normalTexture", treeTextures.barkNormal);
  		branches.addTexture("specularTexture", treeTextures.barkGlossiness);
  		board.setShaderProgram(billboardShaderProgram);
  
  		for (int i = 0; i < lowLODLeafMerges; i++) {
  			lowLODInterpreter.reduceSubModelCount();
  		}
  		Mesh LODCanopy = lowLODInterpreter.getCombinedSubModelMeshes().get(0);
  		LODCanopy.addTexture("leafFront", treeTextures.leafFront);
  		LODCanopy.addTexture("leafFrontTranslucency", treeTextures.leafFrontT);
  		LODCanopy.addTexture("leafFrontNorm", treeTextures.leafFrontNorm);
  		LODCanopy.addTexture("leafFrontHalfLife", treeTextures.leafFrontHL);
  		LODCanopy.addTexture("leafBack", treeTextures.leafBack);
  		LODCanopy.addTexture("leafBackTranslucency", treeTextures.leafBackT);
  		LODCanopy.addTexture("leafBackNorm", treeTextures.leafBackNorm);
  		LODCanopy.addTexture("leafBackHalfLife", treeTextures.leafBackHL);
  		LODCanopy.setShaderProgram(leafShaderProgram);
  		LODCanopy.setColourFilter(params.leafColourFilter);
  		LODCanopy.setLeaf(true);
  //		List<Mesh> billboard = List.of(
  //				new Mesh(board),
  //				MeshUtils.transform(board, new Matrix4f().rotate((float) Math.PI / 2, up)),
  //				LODCanopy
  //		);
  
  		return Map.of(
  				LevelOfDetail.HIGH, List.of(new Mesh(branches), new Mesh(canopy)),
  				LevelOfDetail.LOW, List.of(new Mesh(board), new Mesh(LODCanopy)));
  //		return Map.of(
  //				LevelOfDetail.HIGH, List.of(new Mesh(branches)),
  //				LevelOfDetail.LOW, List.of(new Mesh(board)));
  	}
  
  	private void findMaxRadii(Mesh branches) {
  		List<Vector3f> positions = branches.getVertices().stream().map(Vertex::getPosition).collect(Collectors.toList());
  		Parameters.SceneObjects.Tree params = parameters.sceneObjects.trees.get(typeIndex);
  		for (int i = 0; i < 4 * params.numSides; i++) {
  			Vector3f position = positions.get(i);
  			float len = (new Vector2f(position.x, position.z)).length();
  			if (len > mask.trunkRadius) {
  				mask.trunkRadius = len;
  			}
  		}
  		for (Vector3f position : positions) {
  			float len = (new Vector2f(position.x, position.z)).length();
  			if (len > mask.canopyXZRadius) {
  				mask.canopyXZRadius = len;
  			}
  		}
  	}
  
  	private void findCanopyMask(Mesh branches, Mesh canopy) {
  		findMaxRadii(branches);
  		List<Vertex> vertices = canopy.getVertices();
  		List<Vector3f> positions = vertices.stream().map(Vertex::getPosition).collect(Collectors.toList());
  		mask.canopyCentre = positions.stream()
  				.reduce(VectorUtils::add)
  				.map(p -> p.div(vertices.size()))
  				.orElse(new Vector3f());
  		Vector3f topPoint = positions.stream().max(Comparator.comparingDouble(p -> p.y)).orElse(new Vector3f());
  		Vector3f bottomPoint = positions.stream().min(Comparator.comparingDouble(p -> p.y)).orElse(new Vector3f());
  		float canopyHeight = Math.abs(topPoint.y - bottomPoint.y);
  		mask.canopyYRadius = canopyHeight / 2;
  		mask.canopyCentre.y = (topPoint.y + bottomPoint.y) / 2;
  	}
  
  	@Getter
  	@Setter
  	@AllArgsConstructor
  	public static class Reference {
  		private int typeIndex;
  		private int treePoolIndex;
  		private Vector3f position;
  		private Matrix4f model;
  		private float scale;
  
  		public void render(LevelOfDetail levelOfDetail, boolean renderForShadows) {
  			TreePool treePool = TreePool.getTreePool();
  			treePool.renderTreeWithModel(typeIndex, treePoolIndex, model, levelOfDetail, renderForShadows);
  		}
  
  		public void checkCollisions(Vector3f userPosition) {
  			float userRadius = parameters.output.collisions.userRadius;
  			TreePool treePool = TreePool.getTreePool();
  			Mask mask = treePool.getInstanceMask(typeIndex, treePoolIndex);
  			float treeHeight = position.y + (mask.getCanopyCentre().y + mask.getCanopyYRadius()) * scale;
  			if (userPosition.y + userRadius < position.y || userPosition.y - userRadius > position.y + treeHeight) {
  				return;
  			}
  			float trunkRadius = mask.getTrunkRadius() * scale;
  			Vector2f trunkPosition = new Vector2f(position.x, position.z);
  			if (MathsUtils.circlesColliding(trunkPosition, trunkRadius,
  					new Vector2f(userPosition.x, userPosition.z), userRadius)) {
  				System.out.println("COLLISION: TREE: type=" + typeIndex);
  			}
  		}
  	}
  
  	@Getter
  	@Setter
  	public static class Mask {
  		private float canopyXZRadius = 0;
  		private float canopyYRadius = 0;
  		private float trunkRadius = 0;
  		private Vector3f canopyCentre;
  	}
  }
  