diff --git a/src/main/java/org/osm2world/core/world/modules/TreeModule.java b/src/main/java/org/osm2world/core/world/modules/TreeModule.java index 63b84b43f..c3fc3942a 100644 --- a/src/main/java/org/osm2world/core/world/modules/TreeModule.java +++ b/src/main/java/org/osm2world/core/world/modules/TreeModule.java @@ -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.*; @@ -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; @@ -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 @@ -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; @@ -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)); @@ -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; @@ -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); } @@ -234,6 +272,8 @@ private interface TreeModel extends Model { LeafCycle leafCycle(); @Nullable TreeSpecies species(); boolean mirrored(); + double defaultHeightToWidth(); + @Nullable TreeDimensions dimensions(); } @@ -241,21 +281,43 @@ private record TreeBillboardModel( LeafType leafType, LeafCycle leafCycle, @Nullable TreeSpecies species, - boolean mirrored + boolean mirrored, + @Nullable TreeDimensions dimensions ) implements TreeModel { @Override public List 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 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; + } } } @@ -263,7 +325,8 @@ public List buildMeshes(InstanceParameters params) { private record TreeGeometryModel( LeafType leafType, LeafCycle leafCycle, - @Nullable TreeSpecies species + @Nullable TreeSpecies species, + @Nullable TreeDimensions dimensions ) implements TreeModel { @Override @@ -280,15 +343,18 @@ public List 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), @@ -297,36 +363,43 @@ public List buildMeshes(InstanceParameters params) { } + @Override + public double defaultHeightToWidth() { + return 2.5; + } } private final List 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 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 @@ -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))); } } @@ -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))); } } @@ -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))); } }