Skip to content

Commit

Permalink
Support crown and trunk diameter/circumference for trees
Browse files Browse the repository at this point in the history
  • Loading branch information
tordanik committed Nov 14, 2024
1 parent 96cd670 commit 73c61ad
Showing 1 changed file with 136 additions and 62 deletions.
198 changes: 136 additions & 62 deletions src/main/java/org/osm2world/core/world/modules/TreeModule.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.osm2world.core.world.modules;

import static java.lang.Math.PI;
import static java.util.Arrays.asList;
import static org.osm2world.core.target.common.material.Materials.TREE_CROWN;
import static org.osm2world.core.target.common.material.Materials.TREE_TRUNK;
import static org.osm2world.core.util.ValueParseUtil.parseMeasure;
import static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.filterWorldObjectCollisions;
import static org.osm2world.core.world.modules.common.WorldModuleParseUtil.parseHeight;

import java.util.*;

Expand All @@ -17,10 +18,10 @@
import org.osm2world.core.map_elevation.data.EleConnector;
import org.osm2world.core.map_elevation.data.GroundState;
import org.osm2world.core.math.GeometryUtil;
import org.osm2world.core.math.Vector3D;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.target.common.material.Materials;
import org.osm2world.core.target.common.material.*;
import org.osm2world.core.target.common.mesh.ExtrusionGeometry;
import org.osm2world.core.target.common.mesh.Mesh;
import org.osm2world.core.target.common.model.InstanceParameters;
Expand All @@ -29,6 +30,7 @@
import org.osm2world.core.world.data.*;
import org.osm2world.core.world.modules.common.ConfigurableWorldModule;
import org.osm2world.core.world.modules.common.WorldModuleBillboardUtil;
import org.osm2world.core.world.modules.common.WorldModuleParseUtil;

/**
* adds trees, tree rows, tree groups and forests to the world
Expand Down Expand Up @@ -121,6 +123,63 @@ public static TreeSpecies getValue(TagSet tags) {

}

/**
* @param height tree height in meters
* @param crownDiameter diameter of the tree's crown in meters
* @param trunkDiameter diameter of the tree's trunk (at breast height) in meters, or null if unknown
*/
private record TreeDimensions(double height, double crownDiameter, @Nullable Double trunkDiameter) {

/**
* parse height and other dimensions (optionally modified by some random factor for forests)
*
* @param random randomness generator to slightly vary the values, can be set to null if no randomness is desired
* @param model used to get default ratios between dimensions, such as height to width. Optional.
*/
public static TreeDimensions fromTags(TagSet tags, @Nullable Random random, @Nullable TreeModel model,
double defaultHeight) {

double defaultHeightToWidth = model != null ? model.defaultHeightToWidth() : 2;
double defaultCrownToTrunk = 30;

double scaleFactor = random != null ? 0.5 + 0.75 * random.nextDouble() : 1.0;

Double trunkDiameter = parseMeasure(tags.getValue("diameter"));

if (trunkDiameter == null) {
Double trunkCircumference = parseMeasure(tags.getValue("circumference"));
if (trunkCircumference != null) {
trunkDiameter = trunkCircumference / PI;
}
}

Double crownDiameter = parseMeasure(tags.getValue("diameter_crown"));
Double height = parseMeasure(tags.getValue("height"));

if (height == null) {
height = parseMeasure(tags.getValue("est_height"));
if (height == null) {
if (crownDiameter != null) {
height = crownDiameter * defaultHeightToWidth;
} else if (trunkDiameter != null) {
height = trunkDiameter * defaultCrownToTrunk * defaultHeightToWidth;
} else {
height = defaultHeight;
}
}
}

if (crownDiameter == null) {
crownDiameter = height / defaultHeightToWidth;
}

return new TreeDimensions(scaleFactor * height,scaleFactor * crownDiameter,
trunkDiameter != null ? scaleFactor * trunkDiameter : null);

}

}

private boolean useBillboards = false;
private double defaultTreeHeight = 10;
private double defaultTreeHeightForest = 20;
Expand Down Expand Up @@ -164,35 +223,13 @@ public final void applyTo(MapData mapData) {

}

private static final float TREE_RADIUS_PER_HEIGHT = 0.2f;

/**
* parse height (for forests, add some random factor)
*/
private double getTreeHeight(MapElement element,
boolean isConiferousTree, boolean isFruitTree) {

float heightFactor = 1;
if (element instanceof MapArea) {
heightFactor = 0.5f + 0.75f * (float)Math.random();
}

double defaultHeight = defaultTreeHeight;
if (element instanceof MapArea && !isFruitTree) {
defaultHeight = defaultTreeHeightForest;
}

return heightFactor *
parseHeight(element.getTags(), (float)defaultHeight);

}

/**
* retrieves a suitable {@link TreeModel} from {@link #existingModels}, or creates it if necessary
*
* @param seed an object to be used as the seed for random decisions
* @param seed an object to be used as the seed for random decisions
*/
private TreeModel getTreeModel(VectorXYZ seed, LeafType leafType, LeafCycle leafCycle, TreeSpecies species) {
private TreeModel getTreeModel(Vector3D seed, LeafType leafType, LeafCycle leafCycle, TreeSpecies species,
@Nullable TreeDimensions dimensions) {

var r = new Random((long)(seed.getX() * 10) + (long)(seed.getZ() * 10000));

Expand All @@ -211,6 +248,7 @@ private TreeModel getTreeModel(VectorXYZ seed, LeafType leafType, LeafCycle leaf
&& existingModel.leafCycle() == leafCycle
&& existingModel.species() == species
&& existingModel.mirrored() == mirrored
&& existingModel.dimensions() == dimensions
&& (existingModel instanceof TreeBillboardModel) == useBillboards) {
model = existingModel;
break;
Expand All @@ -219,8 +257,8 @@ private TreeModel getTreeModel(VectorXYZ seed, LeafType leafType, LeafCycle leaf

if (model == null) {
model = useBillboards
? new TreeBillboardModel(leafType, leafCycle, species, mirrored)
: new TreeGeometryModel(leafType, leafCycle, species);
? new TreeBillboardModel(leafType, leafCycle, species, mirrored, dimensions)
: new TreeGeometryModel(leafType, leafCycle, species, dimensions);
existingModels.add(model);
}

Expand All @@ -234,36 +272,61 @@ private interface TreeModel extends Model {
LeafCycle leafCycle();
@Nullable TreeSpecies species();
boolean mirrored();
double defaultHeightToWidth();
@Nullable TreeDimensions dimensions();

}

private record TreeBillboardModel(
LeafType leafType,
LeafCycle leafCycle,
@Nullable TreeSpecies species,
boolean mirrored
boolean mirrored,
@Nullable TreeDimensions dimensions
) implements TreeModel {

@Override
public List<Mesh> buildMeshes(InstanceParameters params) {

Material material = species == TreeSpecies.APPLE_TREE
Material material = getMaterial();

return WorldModuleBillboardUtil.buildCrosstree(material, params.position(),
dimensions != null ? dimensions.crownDiameter : defaultHeightToWidth() * params.height(),
params.height(), mirrored);

}

private Material getMaterial() {
return species == TreeSpecies.APPLE_TREE
? Materials.TREE_BILLBOARD_BROAD_LEAVED_FRUIT
: leafType == LeafType.NEEDLELEAVED
? Materials.TREE_BILLBOARD_CONIFEROUS
: Materials.TREE_BILLBOARD_BROAD_LEAVED;
}

return WorldModuleBillboardUtil.buildCrosstree(material, params.position(),
(species != null ? 1.0 : 0.5) * params.height(), params.height(), mirrored);

@Override
public double defaultHeightToWidth() {
List<TextureLayer> textureLayers = getMaterial().getTextureLayers();
if (!textureLayers.isEmpty()) {
TextureData texture = textureLayers.get(0).baseColorTexture;
TextureDataDimensions textureDimensions = texture.dimensions();
if (textureDimensions.widthPerEntity() != null && textureDimensions.heightPerEntity() != null) {
return textureDimensions.heightPerEntity() / textureDimensions.widthPerEntity();
} else {
return 1.0 / texture.getAspectRatio();
}
} else {
return 2;
}
}

}

private record TreeGeometryModel(
LeafType leafType,
LeafCycle leafCycle,
@Nullable TreeSpecies species
@Nullable TreeSpecies species,
@Nullable TreeDimensions dimensions
) implements TreeModel {

@Override
Expand All @@ -280,15 +343,18 @@ public List<Mesh> buildMeshes(InstanceParameters params) {
boolean coniferous = (leafType == LeafType.NEEDLELEAVED);

double stemRatio = coniferous?0.3:0.5;
double radius = height*TREE_RADIUS_PER_HEIGHT;
double width = dimensions != null ? dimensions.crownDiameter : defaultHeightToWidth() * height;
double trunkRadius = dimensions != null && dimensions.trunkDiameter != null ? dimensions.trunkDiameter / 2
: width / 8;

ExtrusionGeometry trunk = ExtrusionGeometry.createColumn(null,
posXYZ, height*stemRatio,radius / 4, radius / 5,
posXYZ, height*stemRatio,trunkRadius, 0.8 * trunkRadius,
false, true, null, TREE_TRUNK.getTextureDimensions());

ExtrusionGeometry crown = ExtrusionGeometry.createColumn(null,
posXYZ.addY(height*stemRatio), height*(1-stemRatio), radius, coniferous ? 0 : radius,
true, true, null, TREE_CROWN.getTextureDimensions());
posXYZ.addY(height*stemRatio), height*(1-stemRatio), width / 2,
coniferous ? 0 : width / 2, true, true, null,
TREE_CROWN.getTextureDimensions());

return List.of(
new Mesh(trunk, TREE_TRUNK),
Expand All @@ -297,36 +363,43 @@ public List<Mesh> buildMeshes(InstanceParameters params) {

}

@Override
public double defaultHeightToWidth() {
return 2.5;
}
}

private final List<TreeModel> existingModels = new ArrayList<>();

public class Tree extends NoOutlineNodeWorldObject implements ProceduralWorldObject {

private final LeafType leafType;
private final LeafCycle leafCycle;
private final TreeSpecies species;
private final TreeDimensions dimensions;
private final TreeModel model;

public Tree(MapNode node) {

super(node);

LeafType leafType = LeafType.getValue(node.getTags());
LeafCycle leafCycle = LeafCycle.getValue(node.getTags());
TreeSpecies species = TreeSpecies.getValue(node.getTags());
TagSet tags = node.getTags();

/* inherit information from the tree row this tree belongs to, if any */

Optional<MapWaySegment> parentTreeRow = node.getConnectedWaySegments().stream()
.filter(s -> s.getTags().contains("natural", "tree_row")).findAny();
if (parentTreeRow.isPresent()) {
// inherit information from the tree row this tree belongs to
if (leafType == null) leafType = LeafType.getValue(parentTreeRow.get().getTags());
if (leafCycle == null) leafCycle = LeafCycle.getValue(parentTreeRow.get().getTags());
if (species == null) species = TreeSpecies.getValue(parentTreeRow.get().getTags());
tags = WorldModuleParseUtil.inheritTags(tags, parentTreeRow.get().getTags());
}

this.leafType = leafType;
this.leafCycle = leafCycle;
this.species = species;
/* interpret the tags */

var leafType = LeafType.getValue(tags);
var leafCycle = LeafCycle.getValue(tags);
var species = TreeSpecies.getValue(tags);

TreeModel dimensionlessModel = getTreeModel(node.getPos(), leafType, leafCycle, species, null);
dimensions = TreeDimensions.fromTags(tags, null, dimensionlessModel, defaultTreeHeight);
model = getTreeModel(node.getPos(), leafType, leafCycle, species, dimensions);

}

@Override
Expand All @@ -336,9 +409,7 @@ public GroundState getGroundState() {

@Override
public void buildMeshesAndModels(Target target) {
TreeModel treeModel = getTreeModel(getBase(), leafType, leafCycle, species);
double height = getTreeHeight(node, leafType == LeafType.NEEDLELEAVED, species != null);
target.addSubModel(new ModelInstance(treeModel, new InstanceParameters(getBase(), 0, height)));
target.addSubModel(new ModelInstance(model, new InstanceParameters(getBase(), 0, dimensions.height)));
}

}
Expand Down Expand Up @@ -417,9 +488,10 @@ public void buildMeshesAndModels(Target target) {

for (EleConnector treeConnector : treeConnectors) {
VectorXYZ pos = treeConnector.getPosXYZ();
TreeModel treeModel = getTreeModel(pos, leafType, leafCycle, species);
double height = getTreeHeight(segment, leafType == LeafType.NEEDLELEAVED, species != null);
target.addSubModel(new ModelInstance(treeModel, new InstanceParameters(pos, 0, height)));
TreeModel treeModel = getTreeModel(pos, leafType, leafCycle, species, null);
TreeDimensions dimensions = TreeDimensions.fromTags(segment.getTags(), null, treeModel, defaultTreeHeight);
treeModel = getTreeModel(pos, leafType, leafCycle, species, dimensions);
target.addSubModel(new ModelInstance(treeModel, new InstanceParameters(pos, 0, dimensions.height)));
}

}
Expand Down Expand Up @@ -509,9 +581,11 @@ public void buildMeshesAndModels(Target target) {

for (EleConnector treeConnector : treeConnectors) {
VectorXYZ pos = treeConnector.getPosXYZ();
TreeModel treeModel = getTreeModel(pos, leafType, leafCycle, species);
double height = getTreeHeight(area, leafType == LeafType.NEEDLELEAVED, species != null);
target.addSubModel(new ModelInstance(treeModel, new InstanceParameters(pos, 0, height)));
TreeModel treeModel = getTreeModel(pos, leafType, leafCycle, species, null);
TreeDimensions dimensions = TreeDimensions.fromTags(area.getTags(), new Random(area.getId()), treeModel,
area.getTags().contains("landuse", "orchard") ? defaultTreeHeight : defaultTreeHeightForest);
treeModel = getTreeModel(pos, leafType, leafCycle, species, dimensions);
target.addSubModel(new ModelInstance(treeModel, new InstanceParameters(pos, 0, dimensions.height)));
}

}
Expand Down

0 comments on commit 73c61ad

Please sign in to comment.