Replace tree primitives with procedural mesh-based 3D model#201
Replace tree primitives with procedural mesh-based 3D model#201
Conversation
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request replaces the Unity primitive-based tree rendering (Cylinder + Cube) with procedurally generated mesh-based 3D models. The change introduces a new TreeModel class that generates cylindrical trunk and spherical canopy meshes, while simplifying the AppleTree class implementation and updating tests to validate the new mesh-based approach.
Changes:
- Introduced
TreeModel.cswith procedural mesh generation for trunk (cylinder) and canopy (sphere) - Simplified
AppleTree.csby removing primitive GameObject creation and usingTreeModel.CreateTree() - Updated
TestAppleTree.csto verify MeshFilter/MeshRenderer components instead of primitive properties
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/c#/main/entity/TreeModel.cs | New class providing static methods to create procedural tree meshes with cylindrical trunk and spherical canopy |
| src/c#/main/entity/TreeModel.cs.meta | Unity meta file for TreeModel.cs with GUID and import settings |
| src/c#/main/entity/entities/AppleTree.cs | Removed trunk/leaves fields and simplified createGameObject() to use TreeModel.CreateTree() |
| src/c#/tests/entity/TestAppleTree.cs | Updated test assertions to check for MeshFilter/MeshRenderer components instead of primitive scales and colors |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { | ||
| GameObject treeRoot = new GameObject(name); | ||
| treeRoot.transform.position = position; | ||
|
|
||
| // Create trunk | ||
| GameObject trunk = CreateTrunk(height); | ||
| trunk.transform.parent = treeRoot.transform; | ||
| trunk.transform.localPosition = Vector3.zero; | ||
|
|
||
| // Create leaves/canopy | ||
| GameObject leaves = CreateLeaves(height); | ||
| leaves.transform.parent = treeRoot.transform; | ||
| leaves.transform.localPosition = new Vector3(0, height - 1, 0); | ||
|
|
||
| return treeRoot; |
There was a problem hiding this comment.
The procedurally generated meshes are not explicitly destroyed when the tree GameObject is destroyed. While Unity's garbage collector will eventually clean them up, it's best practice to explicitly destroy mesh resources in the destroyGameObject method to prevent memory leaks, especially if many trees are created and destroyed frequently. Consider storing references to the meshes and calling Object.Destroy(mesh) in AppleTree.destroyGameObject().
There was a problem hiding this comment.
Added a DestroyTree() method that explicitly destroys mesh resources before destroying the GameObject. Updated AppleTree.destroyGameObject() to use this method. (commit 40efa9d)
| vertices[i] = new Vector3(x, 0, z); | ||
| normals[i] = new Vector3(x, 0, z).normalized; | ||
| uvs[i] = new Vector2((float)i / segments, 0); | ||
| } | ||
|
|
||
| // Top circle | ||
| for (int i = 0; i < segments; i++) { | ||
| float angle = i * angleStep; | ||
| float x = Mathf.Cos(angle) * radius * 0.8f; // Slightly tapered | ||
| float z = Mathf.Sin(angle) * radius * 0.8f; | ||
| vertices[segments + i] = new Vector3(x, height, z); |
There was a problem hiding this comment.
The trunk mesh extends from y=0 to y=height, but the original primitive cylinder was centered and extended from y=-height to y=+height (due to the default cylinder having height 2 and being scaled by height in the Y axis). This changes the trunk's vertical positioning - it's now bottom-aligned instead of center-aligned at the tree's root position. The leaves will also be positioned differently relative to the trunk.
There was a problem hiding this comment.
Fixed the trunk positioning to match Unity's primitive cylinder behavior. The mesh now extends from -height/2 to +height/2, and the trunk is positioned at localPosition (0, height/2, 0) to center it at the tree's root position, matching the original implementation. (commit 40efa9d)
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent<MeshFilter>() != null); | ||
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent<MeshRenderer>() != null); | ||
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent<MeshFilter>() != null); | ||
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent<MeshRenderer>() != null); | ||
|
|
There was a problem hiding this comment.
The test now only verifies that MeshFilter and MeshRenderer components exist, but doesn't verify that the meshes are actually assigned (meshFilter.mesh != null) or that the renderers have materials with the correct colors. This reduces test coverage compared to the original tests which verified colors. Consider adding assertions to check that meshes are non-null and materials have the expected colors (brown for trunk, green for leaves).
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent<MeshFilter>() != null); | |
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent<MeshRenderer>() != null); | |
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent<MeshFilter>() != null); | |
| UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent<MeshRenderer>() != null); | |
| Transform trunkTransform = tree.getGameObject().transform.GetChild(0); | |
| Transform leavesTransform = tree.getGameObject().transform.GetChild(1); | |
| MeshFilter trunkMeshFilter = trunkTransform.GetComponent<MeshFilter>(); | |
| MeshRenderer trunkMeshRenderer = trunkTransform.GetComponent<MeshRenderer>(); | |
| MeshFilter leavesMeshFilter = leavesTransform.GetComponent<MeshFilter>(); | |
| MeshRenderer leavesMeshRenderer = leavesTransform.GetComponent<MeshRenderer>(); | |
| // Components should exist | |
| UnityEngine.Debug.Assert(trunkMeshFilter != null); | |
| UnityEngine.Debug.Assert(trunkMeshRenderer != null); | |
| UnityEngine.Debug.Assert(leavesMeshFilter != null); | |
| UnityEngine.Debug.Assert(leavesMeshRenderer != null); | |
| // Meshes should be assigned | |
| UnityEngine.Debug.Assert(trunkMeshFilter.mesh != null); | |
| UnityEngine.Debug.Assert(leavesMeshFilter.mesh != null); | |
| // Materials should be assigned and have expected colors | |
| UnityEngine.Debug.Assert(trunkMeshRenderer.material != null); | |
| UnityEngine.Debug.Assert(leavesMeshRenderer.material != null); | |
| // Brown trunk | |
| UnityEngine.Debug.Assert(trunkMeshRenderer.material.color == new Color(0.545f, 0.271f, 0.075f)); | |
| // Green leaves | |
| UnityEngine.Debug.Assert(leavesMeshRenderer.material.color == Color.green); |
There was a problem hiding this comment.
Enhanced test coverage to verify:
- Mesh components exist (MeshFilter, MeshRenderer)
- Meshes are assigned (not null)
- Materials are assigned with correct colors (brown trunk: 0.5f, 0.25f, 0 and green leaves)
(commit 40efa9d)
| meshFilter.mesh = trunkMesh; | ||
|
|
||
| // Set brown bark material (matching original color) | ||
| Material trunkMaterial = new Material(Shader.Find("Standard")); |
There was a problem hiding this comment.
Using Shader.Find("Standard") may return null if the Standard shader is not available in the project (e.g., if using a different render pipeline like URP or HDRP). Consider adding a null check and fallback, or using a more universally available shader like "Unlit/Color" or handling the null case gracefully.
There was a problem hiding this comment.
Added shader fallback logic with multiple options: Standard → Unlit/Color → Diffuse. Extracted to GetDefaultShader() helper method to avoid duplication. (commit 40efa9d)
| meshFilter.mesh = leavesMesh; | ||
|
|
||
| // Set green foliage material (matching original color) | ||
| Material leavesMaterial = new Material(Shader.Find("Standard")); |
There was a problem hiding this comment.
Using Shader.Find("Standard") may return null if the Standard shader is not available in the project (e.g., if using a different render pipeline like URP or HDRP). Consider adding a null check and fallback, or using a more universally available shader like "Unlit/Color" or handling the null case gracefully.
There was a problem hiding this comment.
Added shader fallback logic with multiple options: Standard → Unlit/Color → Diffuse. Extracted to GetDefaultShader() helper method to avoid duplication. (commit 40efa9d)
| /// </summary> | ||
| public class TreeModel { | ||
|
|
||
| public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { |
There was a problem hiding this comment.
The CreateTree method doesn't validate the height parameter. If height is 0 or negative, it will create malformed meshes (e.g., cylinder with height <= 0, leaves positioned at negative or zero Y). Consider adding validation to ensure height is positive (e.g., height > 0) and throwing an ArgumentException or clamping to a minimum value like 1.
| public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { | |
| public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { | |
| if (height <= 0) { | |
| height = 1; | |
| } | |
There was a problem hiding this comment.
Added height validation that clamps to minimum of 1 and logs a debug warning when invalid input is provided. (commit 40efa9d)
| // Top circle | ||
| for (int i = 0; i < segments; i++) { | ||
| float angle = i * angleStep; | ||
| float x = Mathf.Cos(angle) * radius * 0.8f; // Slightly tapered |
There was a problem hiding this comment.
The trunk is now tapered (top radius is 0.8x the bottom radius), which differs from the original uniform cylinder primitive. While this may be a visual improvement, it represents a change in appearance from the existing implementation. Consider documenting this change or making the taper configurable.
There was a problem hiding this comment.
Added documentation in the class summary noting that "the trunk has a slight taper (top is 0.8x the bottom radius) for more realistic appearance." (commit 40efa9d)
|
@copilot address comments |
…date height, add shader fallback, improve tests Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
All code review comments have been addressed in commit 40efa9d:
|
Summary
This PR successfully replaces the current loose collection of Unity primitives (Cylinder + Cube) used for trees with a proper 3D model implementation, addressing issue #140.
Changes Made
Files Added
Files Modified
AppleTree.cs: Simplified from 42 to 29 lines
TestAppleTree.cs: Enhanced tests for mesh-based model
Benefits
All Code Review Feedback Addressed
Security & Quality
Statistics
Testing Instructions
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.