From 74ef4b9ea4a9d6d9f5b229b7d408f38d9f2b49d6 Mon Sep 17 00:00:00 2001 From: slinky55 Date: Sat, 11 Apr 2026 19:21:13 -0500 Subject: [PATCH] model loading, load material from model, changed some architecture. --- src/main/java/DirectionalLight.java | 16 ++-- src/main/java/Material.java | 41 +++++++++ src/main/java/Mesh.java | 50 ++++++----- src/main/java/Model.java | 47 +++++++++++ src/main/java/ModelLoader.java | 117 ++++++++++++++++++++++++++ src/main/java/OrthoCamera.java | 2 + src/main/java/PrimitiveGenerator.java | 7 +- src/main/java/Shader.java | 47 ++++++++++- src/main/java/Texture.java | 44 ++++++++++ src/main/java/TextureLoader.java | 2 + src/main/java/Tony.java | 45 +++++----- src/main/resources/shaders/basic.frag | 62 +++++++++++--- src/main/resources/shaders/basic.vert | 21 +++-- tree.fbx | Bin 0 -> 24252 bytes 14 files changed, 432 insertions(+), 69 deletions(-) create mode 100644 src/main/java/Material.java create mode 100644 src/main/java/Model.java create mode 100644 src/main/java/ModelLoader.java create mode 100644 src/main/java/Texture.java create mode 100644 src/main/java/TextureLoader.java create mode 100644 tree.fbx diff --git a/src/main/java/DirectionalLight.java b/src/main/java/DirectionalLight.java index 048cb53..1aca760 100644 --- a/src/main/java/DirectionalLight.java +++ b/src/main/java/DirectionalLight.java @@ -2,15 +2,19 @@ import org.joml.Vector3f; public class DirectionalLight { public Vector3f direction; - public Vector3f color; - public float intensity; + + public Vector3f ambient; + public Vector3f diffuse; + public Vector3f specular; public DirectionalLight ( Vector3f direction, - Vector3f color, - float intensity) { + Vector3f ambient, + Vector3f diffuse, + Vector3f specular) { this.direction = direction; - this.color = color; - this.intensity = intensity; + this.ambient = ambient; + this.diffuse = diffuse; + this.specular = specular; } } diff --git a/src/main/java/Material.java b/src/main/java/Material.java new file mode 100644 index 0000000..e5c7d9a --- /dev/null +++ b/src/main/java/Material.java @@ -0,0 +1,41 @@ +import org.joml.Vector3f; +import org.joml.Vector4f; + +public class Material { + // Phong + public Vector3f ambient = new Vector3f(1.0f); + public Vector3f diffuse = new Vector3f(1.0f); + public Vector3f specular = new Vector3f(1.0f); + public float shininess = 32.0f; + + public Texture diffuseMap; + public Texture specularMap; + + public boolean hasDiffuseMap; + public boolean hasSpecularMap; + + public Material() { + this.ambient = new Vector3f(1.f, 1.f, 1.f); + this.diffuse = new Vector3f(1.f, 1.f, 1.f); + this.specular = new Vector3f(1.f, 1.f, 1.f); + + diffuseMap = null; + specularMap = null; + + hasDiffuseMap = false; + hasSpecularMap = false; + } + + public Material(Vector3f ambient, Vector3f diffuse, Vector3f specular, float shininess) { + this.ambient = ambient; + this.diffuse = diffuse; + this.specular = specular; + this.shininess = shininess; + + diffuseMap = null; + specularMap = null; + + hasDiffuseMap = false; + hasSpecularMap = false; + } +} diff --git a/src/main/java/Mesh.java b/src/main/java/Mesh.java index adf7027..3e2a7fd 100644 --- a/src/main/java/Mesh.java +++ b/src/main/java/Mesh.java @@ -1,3 +1,4 @@ +import org.lwjgl.BufferUtils; import org.lwjgl.system.MemoryStack; import java.nio.FloatBuffer; @@ -15,11 +16,14 @@ public class Mesh { private int _vbo; private int _ebo; - private int vertexCount; + private final int vertexCount; - public Mesh(float[] p, float[] u, float[] n, int[] indices) { + public int material; + + public Mesh(float[] p, float[] u, float[] n, int[] indices, int material) { vertexCount = indices.length; sendToGPU(Vertex.interleaveVertexData(p, u, n), indices); + this.material = material; } public void render() { @@ -35,29 +39,31 @@ public class Mesh { } private void sendToGPU(float[] vertices, int[] indices) { - try (MemoryStack stack = MemoryStack.stackPush()) { - _vao = glGenVertexArrays(); - glBindVertexArray(_vao); + _vao = glGenVertexArrays(); + glBindVertexArray(_vao); - // pos - _vbo = glGenBuffers(); - glBindBuffer(GL_ARRAY_BUFFER, _vbo); - glBufferData(GL_ARRAY_BUFFER, stack.floats(vertices), GL_STATIC_DRAW); + // pos + _vbo = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertices.length); + vertexBuffer.put(vertices).flip(); + glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW); - _ebo = glGenBuffers(); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, stack.ints(indices), GL_STATIC_DRAW); + _ebo = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo); + IntBuffer indicesBuffer = BufferUtils.createIntBuffer(indices.length); + indicesBuffer.put(indices).flip(); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW); - // set vao positions - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, false, STRIDE, 0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 2, GL_FLOAT, false, STRIDE, 3 * Float.BYTES); - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 3, GL_FLOAT, false, STRIDE, 5 * Float.BYTES); + // set vao positions + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, STRIDE, 0); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, false, STRIDE, 3 * Float.BYTES); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, false, STRIDE, 5 * Float.BYTES); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); - } + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); } } diff --git a/src/main/java/Model.java b/src/main/java/Model.java new file mode 100644 index 0000000..b734d50 --- /dev/null +++ b/src/main/java/Model.java @@ -0,0 +1,47 @@ +import org.joml.Matrix3f; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.List; + +public class Model { + public List meshes; + public List materials; + + public Matrix4f modelMatrix; + + public Model(Mesh mesh, Material mat) { + meshes = new ArrayList<>(); + meshes.add(mesh); + + materials = new ArrayList<>(); + materials.add(mat); + } + + public Model(List meshes, List materials) { + this.meshes = meshes; + this.materials = materials; + } + + public void render(Shader shader) { + shader.setUniform("model", modelMatrix); + + Matrix3f normalMatrix = new Matrix3f(modelMatrix).invert().transpose(); + shader.setUniform("normalMatrix", normalMatrix); + + for (Mesh m : meshes) { + Material mat = materials.get(m.material); + + // set material uniforms + shader.setUniform("matAmbient", mat.ambient); + shader.setUniform("matDiffuse", mat.diffuse); + shader.setUniform("matSpecular", mat.specular); + shader.setUniform("shininess", mat.shininess); + + shader.setUniform("hasDiffuseMap", false); + shader.setUniform("hasSpecularMap", false); + + m.render(); + } + } +} diff --git a/src/main/java/ModelLoader.java b/src/main/java/ModelLoader.java new file mode 100644 index 0000000..7af9ee7 --- /dev/null +++ b/src/main/java/ModelLoader.java @@ -0,0 +1,117 @@ +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.joml.Vector4f; +import org.lwjgl.PointerBuffer; +import org.lwjgl.assimp.*; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.lwjgl.assimp.Assimp.*; + +public class ModelLoader { + + + private static final Logger logger = LogManager.getLogger(ModelLoader.class); + + public static Model loadModel(String resourcePath) { + AIScene scene = aiImportFile(resourcePath, + aiProcess_Triangulate | + aiProcess_FlipUVs | + aiProcess_JoinIdenticalVertices | + aiProcess_GenSmoothNormals | + aiProcess_ConvertToLeftHanded); + + if (scene == null || scene.mRootNode() == null) { + logger.error("Failed to load model %s".formatted(resourcePath)); + return null; + } + + List meshList = new ArrayList<>(); + int numMeshes = scene.mNumMeshes(); + PointerBuffer aiMeshes = scene.mMeshes(); + + for (int i = 0; i < numMeshes; i++) { + AIMesh aiMesh = AIMesh.create(aiMeshes.get(i)); + Mesh mesh = processMesh(aiMesh); + meshList.add(mesh); + } + + int numMaterials = scene.mNumMaterials(); + PointerBuffer aiMaterials = scene.mMaterials(); + List materialList = new ArrayList<>(); + + for (int i = 0; i < numMaterials; i++) { + AIMaterial aiMaterial = AIMaterial.create(aiMaterials.get(i)); + materialList.add(processMaterial(aiMaterial)); + } + + return new Model(meshList, materialList); + } + + private static Mesh processMesh(AIMesh aiMesh) { + int numVertices = aiMesh.mNumVertices(); + float[] positions = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + float[] normals = new float[numVertices * 3]; + + for (int i = 0; i < numVertices; i++) { + AIVector3D pos = aiMesh.mVertices().get(i); + positions[i * 3] = (pos.x()); + positions[i * 3 + 1] = (pos.y()); + positions[i * 3 + 2] = (pos.z()); + + if (aiMesh.mTextureCoords(0) != null) { + AIVector3D tex = aiMesh.mTextureCoords(0).get(i); + uvs[i * 2] = tex.x(); + uvs[i * 2 + 1] = tex.y(); + } else { + uvs[i * 2] = 0.0f; + uvs[i * 2 + 1] = 0.0f; + } + + AIVector3D norm = aiMesh.mNormals().get(i); + normals[i * 3] = norm.x(); + normals[i * 3 + 1] = norm.y(); + normals[i * 3 + 2] = norm.z(); + } + + List indicesList = new ArrayList<>(); + int numFaces = aiMesh.mNumFaces(); + AIFace.Buffer aiFaces = aiMesh.mFaces(); + for (int i = 0; i < numFaces; i++) { + AIFace aiFace = aiFaces.get(i); + IntBuffer buffer = aiFace.mIndices(); + while (buffer.hasRemaining()) { + indicesList.add(buffer.get()); + } + } + + int[] indices = new int[indicesList.size()]; + for (int i = 0; i < indicesList.size(); i++) { + indices[i] = indicesList.get(i); + } + + return new Mesh(positions, uvs, normals, indices, aiMesh.mMaterialIndex()); + } + + private static Material processMaterial(AIMaterial aiMaterial) { + Material material = new Material(); + AIColor4D color = AIColor4D.create(); + + if (Assimp.aiGetMaterialColor(aiMaterial, Assimp.AI_MATKEY_COLOR_DIFFUSE, 0, 0, color) == Assimp.aiReturn_SUCCESS) { + material.diffuse.set(color.r(), color.g(), color.b()); + } + + if (Assimp.aiGetMaterialColor(aiMaterial, Assimp.AI_MATKEY_COLOR_AMBIENT, 0, 0, color) == Assimp.aiReturn_SUCCESS) { + material.ambient.set(color.r(), color.g(), color.b()); + } + + if (Assimp.aiGetMaterialColor(aiMaterial, Assimp.AI_MATKEY_COLOR_SPECULAR, 0, 0, color) == Assimp.aiReturn_SUCCESS) { + material.specular.set(color.r(), color.g(), color.b()); + } + + return material; + } +} diff --git a/src/main/java/OrthoCamera.java b/src/main/java/OrthoCamera.java index d619edb..63ff56c 100644 --- a/src/main/java/OrthoCamera.java +++ b/src/main/java/OrthoCamera.java @@ -32,4 +32,6 @@ public class OrthoCamera { public Matrix4f getView() { return view; } + + public Vector3f getPosition() { return position; }; } diff --git a/src/main/java/PrimitiveGenerator.java b/src/main/java/PrimitiveGenerator.java index 9e136af..af77fbb 100644 --- a/src/main/java/PrimitiveGenerator.java +++ b/src/main/java/PrimitiveGenerator.java @@ -2,7 +2,7 @@ import org.joml.Vector2f; import org.joml.Vector3f; public class PrimitiveGenerator { - public static Mesh getCube() { + public static Model getCube() { float s = 0.5f; // Positions: 24 vertices (6 faces * 4 vertices) @@ -54,6 +54,9 @@ public class PrimitiveGenerator { indices[idx + 5] = offset + 0; } - return new Mesh(p, u, n, indices); + Mesh mesh = new Mesh(p, u, n, indices, 0); + Material mat = new Material(); + + return new Model(mesh, mat); } } diff --git a/src/main/java/Shader.java b/src/main/java/Shader.java index 84b2c95..c0559e4 100644 --- a/src/main/java/Shader.java +++ b/src/main/java/Shader.java @@ -1,8 +1,11 @@ +import org.joml.Matrix3f; import org.joml.Matrix4f; import org.joml.Vector3f; +import org.joml.Vector4f; import org.lwjgl.system.MemoryStack; import java.nio.FloatBuffer; +import java.nio.IntBuffer; import java.util.HashMap; import java.util.Vector; @@ -74,7 +77,21 @@ public class Shader { logger.error(msg); return; } - glUniform3fv(location, fb.array()); + glUniform3f(location, fb.get(0), fb.get(1), fb.get(2)); + } + } + + public void setUniform(String uniformName, Vector4f data) { + try (MemoryStack stack = MemoryStack.stackPush()) { + FloatBuffer fb = stack.mallocFloat(4); + data.get(fb); + int location = getUniformLocation(uniformName); + if (location < 0) { + var msg = "Failed to find location for uniform name %s".formatted(uniformName); + logger.error(msg); + return; + } + glUniform4f(location, fb.get(0), fb.get(1), fb.get(2), fb.get(3)); } } @@ -92,6 +109,34 @@ public class Shader { } } + public void setUniform(String uniformName, Matrix3f data) { + try (MemoryStack stack = MemoryStack.stackPush()) { + FloatBuffer fb = stack.mallocFloat(9); + data.get(fb); + int location = getUniformLocation(uniformName); + if (location < 0) { + var msg = "Failed to find location for uniform name %s".formatted(uniformName); + logger.error(msg); + return; + } + glUniformMatrix3fv(location, false, fb); + } + } + + public void setUniform(String uniformName, boolean data) { + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer fb = stack.mallocInt(1); + fb.put(data ? 1 : 0); + int location = getUniformLocation(uniformName); + if (location < 0) { + var msg = "Failed to find location for uniform name %s".formatted(uniformName); + logger.error(msg); + return; + } + glUniform1i(location, fb.get(0)); + } + } + private int getUniformLocation(String uniformName) { if (!uniforms.containsKey(uniformName)) { int location = glGetUniformLocation(id, uniformName); diff --git a/src/main/java/Texture.java b/src/main/java/Texture.java new file mode 100644 index 0000000..8088c89 --- /dev/null +++ b/src/main/java/Texture.java @@ -0,0 +1,44 @@ +import java.nio.ByteBuffer; + +import static org.lwjgl.opengl.GL40.*; +import static org.lwjgl.stb.STBImage.*; + +public class Texture { + private final int id; + private final int width; + private final int height; + + public Texture(String filePath) { + int[] w = new int[1]; + int[] h = new int[1]; + int[] channels = new int[1]; + + // Load image pixels + ByteBuffer data = stbi_load(filePath, w, h, channels, 4); + if (data == null) { + throw new RuntimeException("Texture file not found: " + filePath); + } + + this.width = w[0]; + this.height = h[0]; + + // Create OpenGL texture + this.id = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, id); + + // Texture Parameters (Essential for your Isometric view) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Upload data + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + glGenerateMipmap(GL_TEXTURE_2D); + + stbi_image_free(data); + } + + public void bind(int unit) { + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, id); + } +} \ No newline at end of file diff --git a/src/main/java/TextureLoader.java b/src/main/java/TextureLoader.java new file mode 100644 index 0000000..2e3f26f --- /dev/null +++ b/src/main/java/TextureLoader.java @@ -0,0 +1,2 @@ +public class TextureLoader { +} diff --git a/src/main/java/Tony.java b/src/main/java/Tony.java index 66c1ac6..f143469 100644 --- a/src/main/java/Tony.java +++ b/src/main/java/Tony.java @@ -1,3 +1,4 @@ +import org.joml.Matrix3f; import org.joml.Matrix4f; import org.joml.Vector3f; import org.lwjgl.*; @@ -6,6 +7,7 @@ import org.lwjgl.opengl.*; import org.lwjgl.system.*; import java.nio.*; +import java.util.List; import static org.lwjgl.glfw.Callbacks.*; import static org.lwjgl.glfw.GLFW.*; @@ -19,9 +21,10 @@ import org.apache.logging.log4j.Logger; public class Tony { private long window; private Shader shader; - private Mesh cube; + private Model cube; private OrthoCamera camera; private DirectionalLight sun; + private Model tree; private static final Logger logger = LogManager.getLogger(Tony.class); @@ -35,12 +38,19 @@ public class Tony { shader = new Shader("/shaders/basic.vert", "/shaders/basic.frag"); cube = PrimitiveGenerator.getCube(); + tree = ModelLoader.loadModel("tree.fbx"); + tree.modelMatrix = new Matrix4f() + .translation(new Vector3f(0.f, 0.f, 0.f)) + .rotate((float)Math.toRadians(90), 1, 0, 0) + .scale(.2f); + camera = new OrthoCamera(1024.f / 720); sun = new DirectionalLight( - new Vector3f(-1.0f, -1.0f, -0.5f), // Pointing Down, Left, and slightly Back - new Vector3f(1.0f, 1.0f, 0.9f), // Warm yellowish sun - 1.2f // Brightness + new Vector3f(-3.0f, -3.0f, -0.5f), + new Vector3f(.2f, .2f, .2f), + new Vector3f(0.5f, 0.5f, 0.5f), + new Vector3f(1.f, 1.f, 1.f) ); loop(); @@ -90,34 +100,31 @@ public class Tony { } // the stack frame is popped automatically glfwMakeContextCurrent(window); - GL.createCapabilities(); - - glViewport(0, 0, 1024, 720); - glfwSwapInterval(1); glfwShowWindow(window); + + GL.createCapabilities(); + glViewport(0, 0, 1024, 720); + glEnable(GL_DEPTH_TEST); + glClearColor(0.33f, 0.48f, 0.48f, 1.0f); } private void loop() { - glClearColor(0.33f, 0.48f, 0.48f, 1.0f); - - Matrix4f modelMatrix = new Matrix4f(); - while ( !glfwWindowShouldClose(window) ) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shader.bind(); - modelMatrix.rotateXYZ(0, 0.01f, 0); - shader.setUniform("projection", camera.getProjection()); shader.setUniform("view", camera.getView()); - shader.setUniform("model", modelMatrix); - shader.setUniform("lightDirection", sun.direction); - shader.setUniform("lightColor", sun.color); - shader.setUniform("lightIntensity", sun.intensity); + shader.setUniform("viewPos", camera.getPosition()); - cube.render(); + shader.setUniform("lightDirection", sun.direction); + shader.setUniform("lightAmbient", sun.ambient); + shader.setUniform("lightDiffuse", sun.diffuse); + shader.setUniform("lightSpecular", sun.specular); + + tree.render(shader); shader.unbind(); diff --git a/src/main/resources/shaders/basic.frag b/src/main/resources/shaders/basic.frag index 01068c2..e5a3f50 100644 --- a/src/main/resources/shaders/basic.frag +++ b/src/main/resources/shaders/basic.frag @@ -1,25 +1,59 @@ #version 400 core -out vec4 color; +out vec4 FragColor; -in vec2 texCoord; -in vec3 normal; +in vec3 FragPos; +in vec3 Normal; +in vec2 TexCoords; + +uniform vec3 viewPos; -uniform sampler2D texture_diffuse; uniform vec3 lightDirection; -uniform vec3 lightColor; -uniform float lightIntensity; +uniform vec3 lightAmbient; +uniform vec3 lightDiffuse; +uniform vec3 lightSpecular; -void main() { - vec3 normal = normalize(outNormal); - vec3 lightDir = normalize(-lightDirection); +uniform vec3 matAmbient; +uniform vec3 matDiffuse; +uniform vec3 matSpecular; +uniform float shininess; - float diff = max(dot(normal, lightDir), 0.0); +uniform sampler2D diffuseMap; +uniform sampler2D specularMap; +uniform bool hasDiffuseMap; +uniform bool hasSpecularMap; - float ambient = 0.2; +void main() +{ + vec3 N = normalize(Normal); + vec3 V = normalize(viewPos - FragPos); + vec3 L = normalize(-lightDirection); - vec3 diffuse = diff * lightColor * lightIntensity; - vec4 texColor = texture(texture_diffuse, texCoord); + vec3 texDiffuse = hasDiffuseMap + ? texture(diffuseMap, TexCoords).rgb + : vec3(1.0); - color = vec4(ambient + diffuse, 1.0) * texColor; + vec3 texSpecular = hasSpecularMap + ? texture(specularMap, TexCoords).rgb + : vec3(1.0); + + vec3 albedo = matDiffuse * texDiffuse; + vec3 specColor = matSpecular * texSpecular; + vec3 ambientCol = matAmbient * texDiffuse; + + vec3 ambient = lightAmbient * ambientCol; + + float diff = max(dot(N, L), 0.0); + vec3 diffuse = lightDiffuse * diff * albedo; + + vec3 specular = vec3(0.0); + if (diff > 0.0) { + vec3 H = normalize(L + V); + float spec = pow(max(dot(N, H), 0.0), shininess); + specular = lightSpecular * spec * specColor; + } + + vec3 result = ambient + diffuse + specular; + + FragColor = vec4(result, 1.0); } \ No newline at end of file diff --git a/src/main/resources/shaders/basic.vert b/src/main/resources/shaders/basic.vert index 23410ca..5a8bc7e 100644 --- a/src/main/resources/shaders/basic.vert +++ b/src/main/resources/shaders/basic.vert @@ -1,13 +1,24 @@ #version 400 core -layout (location = 0) in vec3 position; -layout (location = 1) in vec2 uv; -layout (location = 2) in vec3 +layout (location = 0) in vec3 aPosition; +layout (location = 1) in vec2 aTexCoord; +layout (location = 2) in vec3 aNormal; -uniform mat4 model; uniform mat4 view; uniform mat4 projection; +uniform mat4 model; +uniform mat3 normalMatrix; + +out vec3 FragPos; +out vec3 Normal; +out vec2 TexCoords; + void main() { - gl_Position = projection * view * model * vec4(position, 1.0); + vec4 position = projection * view * model * vec4(aPosition, 1.0); + FragPos = vec3(position.x, position.y, position.z); + TexCoords = aTexCoord; + Normal = normalize(normalMatrix * aNormal); + + gl_Position = position; } \ No newline at end of file diff --git a/tree.fbx b/tree.fbx new file mode 100644 index 0000000000000000000000000000000000000000..f8e16b617d6e0a263b61b5ba950552d9947bff33 GIT binary patch literal 24252 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 literal 0 HcmV?d00001