├── CNAME ├── .prettierrc ├── .prettierignore ├── assets └── grass.blend ├── src ├── app │ ├── public │ │ ├── grass.glb │ │ ├── grass.jpg │ │ ├── logo.png │ │ ├── rock1.glb │ │ ├── rock2.glb │ │ ├── rock3.glb │ │ ├── ambience.mp3 │ │ ├── favicon.ico │ │ ├── og-image.png │ │ ├── dirt_color.jpg │ │ ├── dumbeldor.ttf │ │ ├── icon_about.png │ │ ├── icon_muted.png │ │ ├── background.webp │ │ ├── dirt_normal.jpg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── flower_blue.glb │ │ ├── flower_white.glb │ │ ├── flower_yellow.glb │ │ ├── icon_playing.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── styles.css │ ├── shaders │ │ ├── skybox.vert │ │ └── skybox.frag │ ├── environment.js │ ├── rocks.js │ ├── main.js │ ├── clouds.js │ ├── noise.js │ ├── scene.js │ ├── index.html │ ├── skybox.js │ ├── ground.js │ ├── grass.js │ └── ui.js └── lib │ ├── assets │ ├── bark │ │ ├── oak_ao_1k.jpg │ │ ├── pine_ao_1k.jpg │ │ ├── birch_ao_1k.jpg │ │ ├── oak_color_1k.jpg │ │ ├── oak_normal_1k.jpg │ │ ├── pine_color_1k.jpg │ │ ├── willow_ao_1k.jpg │ │ ├── birch_color_1k.jpg │ │ ├── birch_normal_1k.jpg │ │ ├── pine_normal_1k.jpg │ │ ├── willow_color_1k.jpg │ │ ├── birch_roughness_1k.jpg │ │ ├── oak_roughness_1k.jpg │ │ ├── pine_roughness_1k.jpg │ │ ├── willow_normal_1k.jpg │ │ ├── willow_roughness_1k.jpg │ │ └── README.md │ └── leaves │ │ ├── ash_color.png │ │ ├── aspen_color.png │ │ ├── oak_color.png │ │ └── pine_color.png │ ├── index.js │ ├── enums.js │ ├── rng.js │ ├── branch.js │ ├── presets │ ├── index.js │ ├── pine_small.json │ ├── aspen_medium.json │ ├── pine_medium.json │ ├── ash_medium.json │ ├── ash_small.json │ ├── oak_large.json │ ├── oak_medium.json │ ├── oak_small.json │ ├── ash_large.json │ ├── bush_1.json │ ├── aspen_small.json │ ├── aspen_large.json │ ├── pine_large.json │ ├── bush_2.json │ └── bush_3.json │ ├── textures.js │ ├── options.js │ └── tree.js ├── tsconfig.json ├── docker-compose.yml ├── .vscode └── launch.json ├── vite.app.config.js ├── Dockerfile ├── vite.lib.config.js ├── .github └── workflows │ ├── npm-publish.yml │ └── main.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /CNAME: -------------------------------------------------------------------------------- 1 | www.eztree.dev 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | -------------------------------------------------------------------------------- /assets/grass.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/assets/grass.blend -------------------------------------------------------------------------------- /src/app/public/grass.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/grass.glb -------------------------------------------------------------------------------- /src/app/public/grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/grass.jpg -------------------------------------------------------------------------------- /src/app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/logo.png -------------------------------------------------------------------------------- /src/app/public/rock1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/rock1.glb -------------------------------------------------------------------------------- /src/app/public/rock2.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/rock2.glb -------------------------------------------------------------------------------- /src/app/public/rock3.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/rock3.glb -------------------------------------------------------------------------------- /src/app/public/ambience.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/ambience.mp3 -------------------------------------------------------------------------------- /src/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/favicon.ico -------------------------------------------------------------------------------- /src/app/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/og-image.png -------------------------------------------------------------------------------- /src/app/public/dirt_color.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/dirt_color.jpg -------------------------------------------------------------------------------- /src/app/public/dumbeldor.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/dumbeldor.ttf -------------------------------------------------------------------------------- /src/app/public/icon_about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/icon_about.png -------------------------------------------------------------------------------- /src/app/public/icon_muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/icon_muted.png -------------------------------------------------------------------------------- /src/app/public/background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/background.webp -------------------------------------------------------------------------------- /src/app/public/dirt_normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/dirt_normal.jpg -------------------------------------------------------------------------------- /src/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /src/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/app/public/flower_blue.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/flower_blue.glb -------------------------------------------------------------------------------- /src/app/public/flower_white.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/flower_white.glb -------------------------------------------------------------------------------- /src/app/public/flower_yellow.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/flower_yellow.glb -------------------------------------------------------------------------------- /src/app/public/icon_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/icon_playing.png -------------------------------------------------------------------------------- /src/lib/assets/bark/oak_ao_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/oak_ao_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/pine_ao_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/pine_ao_1k.jpg -------------------------------------------------------------------------------- /src/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/lib/assets/bark/birch_ao_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/birch_ao_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/oak_color_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/oak_color_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/oak_normal_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/oak_normal_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/pine_color_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/pine_color_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/willow_ao_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/willow_ao_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/leaves/ash_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/leaves/ash_color.png -------------------------------------------------------------------------------- /src/lib/assets/leaves/aspen_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/leaves/aspen_color.png -------------------------------------------------------------------------------- /src/lib/assets/leaves/oak_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/leaves/oak_color.png -------------------------------------------------------------------------------- /src/lib/assets/leaves/pine_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/leaves/pine_color.png -------------------------------------------------------------------------------- /src/lib/assets/bark/birch_color_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/birch_color_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/birch_normal_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/birch_normal_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/pine_normal_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/pine_normal_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/willow_color_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/willow_color_1k.jpg -------------------------------------------------------------------------------- /src/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/app/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/app/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/lib/assets/bark/birch_roughness_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/birch_roughness_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/oak_roughness_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/oak_roughness_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/pine_roughness_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/pine_roughness_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/willow_normal_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/willow_normal_1k.jpg -------------------------------------------------------------------------------- /src/lib/assets/bark/willow_roughness_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgreenheck/ez-tree/HEAD/src/lib/assets/bark/willow_roughness_1k.jpg -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | export { Tree } from './tree'; 2 | export { TreePreset } from './presets/index'; 3 | export { BarkType, Billboard, LeafType, TreeType } from './enums'; 4 | -------------------------------------------------------------------------------- /src/app/shaders/skybox.vert: -------------------------------------------------------------------------------- 1 | varying vec3 vPosition; 2 | 3 | void main() { 4 | vPosition = position; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 6 | } -------------------------------------------------------------------------------- /src/app/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/lib/assets/bark/README.md: -------------------------------------------------------------------------------- 1 | # Sources 2 | 3 | Higher resolution textures can be found from the original source 4 | 5 | Birch - https://www.texturecan.com/details/221/ 6 | Pine - https://www.texturecan.com/details/588/ 7 | Oak - https://polyhaven.com/a/bark_brown_02 8 | Willow - https://polyhaven.com/a/bark_willow_02 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDeclarationOnly": true, 5 | "outDir": "./build", 6 | "allowJs": true, 7 | "moduleResolution": "Bundler", 8 | "skipLibCheck": true, 9 | "target": "ESNext", 10 | "module": "ESNext" 11 | }, 12 | "include": [ 13 | "src/lib/**/*" 14 | ] 15 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | tree-gen-app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - '5173:5173' 10 | volumes: 11 | - ./src:/usr/src/app/src 12 | - ./public:/usr/src/app/public 13 | environment: 14 | - NODE_ENV=development 15 | command: npm run app -- --host 0.0.0.0 16 | -------------------------------------------------------------------------------- /src/lib/enums.js: -------------------------------------------------------------------------------- 1 | export const BarkType = { 2 | Birch: 'birch', 3 | Oak: 'oak', 4 | Pine: 'pine', 5 | Willow: 'willow' 6 | }; 7 | 8 | export const Billboard = { 9 | Single: 'single', 10 | Double: 'double', 11 | }; 12 | 13 | export const LeafType = { 14 | Ash: 'ash', 15 | Aspen: 'aspen', 16 | Pine: 'pine', 17 | Oak: 'oak', 18 | }; 19 | 20 | export const TreeType = { 21 | Deciduous: 'deciduous', 22 | Evergreen: 'evergreen', 23 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Debug in Browser", 11 | "skipFiles": [ 12 | "${workspaceFolder}/node_modules/**/*.js" 13 | ], 14 | "url": "http://127.0.0.1:5173", 15 | "webRoot": "${workspaceFolder}/src" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /vite.app.config.js: -------------------------------------------------------------------------------- 1 | // Config file for running the demo locally 2 | import path from 'path'; 3 | 4 | /** 5 | * @type {import('vite').UserConfig} 6 | */ 7 | export default { 8 | build: { 9 | emptyOutDir: true, 10 | outDir: '../../dist', 11 | sourcemap: true, 12 | }, 13 | root: './src/app', 14 | resolve: { 15 | alias: { 16 | '@dgreenheck/ez-tree': path.resolve( 17 | __dirname, 18 | 'build/ez-tree.es.js', 19 | ), 20 | }, 21 | }, 22 | server: { 23 | hmr: true, 24 | }, 25 | assetsInclude: ['**/*.frag', '**/*.vert'], 26 | }; 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node runtime as the base image 2 | FROM node:18 3 | 4 | # Set the working directory in the container 5 | WORKDIR /usr/src/app 6 | 7 | # Copy package.json and package-lock.json 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | # Build the Tree.js app 17 | RUN npm run build:app 18 | 19 | # Install a simple HTTP server for serving static content 20 | RUN npm install -g http-server 21 | 22 | # Make port 8080 available to the world outside this container 23 | EXPOSE 8080 24 | 25 | # Run the app when the container launches 26 | CMD ["http-server", "dist"] -------------------------------------------------------------------------------- /src/lib/rng.js: -------------------------------------------------------------------------------- 1 | export default class RNG { 2 | m_w = 123456789; 3 | m_z = 987654321; 4 | mask = 0xffffffff; 5 | 6 | constructor(seed) { 7 | this.m_w = (123456789 + seed) & this.mask; 8 | this.m_z = (987654321 - seed) & this.mask; 9 | } 10 | 11 | /** 12 | * Returns a random number between min and max 13 | */ 14 | random(max = 1, min = 0) { 15 | this.m_z = (36969 * (this.m_z & 65535) + (this.m_z >> 16)) & this.mask; 16 | this.m_w = (18000 * (this.m_w & 65535) + (this.m_w >> 16)) & this.mask; 17 | let result = ((this.m_z << 16) + (this.m_w & 65535)) >>> 0; 18 | result /= 4294967296; 19 | 20 | return (max - min) * result + min; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vite.lib.config.js: -------------------------------------------------------------------------------- 1 | import dts from "vite-plugin-dts"; 2 | 3 | /** 4 | * @type {import('vite').UserConfig} 5 | */ 6 | export default { 7 | build: { 8 | outDir: './build', 9 | lib: { 10 | entry: './src/lib/index.js', 11 | name: '@dgreenheck/ez-tree', 12 | fileName: (format) => `ez-tree.${format}.js`, 13 | }, 14 | rollupOptions: { 15 | external: ['three'], 16 | output: { 17 | globals: { 18 | three: 'THREE', 19 | }, 20 | }, 21 | }, 22 | sourcemap: true, 23 | }, 24 | plugins: [ 25 | dts({ 26 | outDir: './build', 27 | insertTypesEntry: true, 28 | rollupTypes: true, 29 | tsconfigPath: "./tsconfig.json" 30 | }), 31 | ], 32 | }; -------------------------------------------------------------------------------- /src/lib/branch.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export class Branch { 4 | /** 5 | * Generates a new branch 6 | * @param {THREE.Vector3} origin The starting point of the branch 7 | * @param {THREE.Euler} orientation The starting orientation of the branch 8 | * @param {number} length The length of the branch 9 | * @param {number} radius The radius of the branch at its starting point 10 | */ 11 | constructor( 12 | origin = new THREE.Vector3(), 13 | orientation = new THREE.Euler(), 14 | length = 0, 15 | radius = 0, 16 | level = 0, 17 | sectionCount = 0, 18 | segmentCount = 0, 19 | ) { 20 | this.origin = origin.clone(); 21 | this.orientation = orientation.clone(); 22 | this.length = length; 23 | this.radius = radius; 24 | this.level = level; 25 | this.sectionCount = sectionCount; 26 | this.segmentCount = segmentCount; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/environment.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Skybox } from './skybox'; 3 | import { Ground } from './ground'; 4 | import { Grass } from './grass'; 5 | import { Rocks } from './rocks'; 6 | import { Clouds } from './clouds'; 7 | 8 | export class Environment extends THREE.Object3D { 9 | constructor() { 10 | super(); 11 | 12 | this.ground = new Ground(); 13 | this.add(this.ground); 14 | 15 | this.grass = new Grass(); 16 | this.add(this.grass); 17 | 18 | this.skybox = new Skybox(); 19 | this.add(this.skybox); 20 | 21 | this.rocks = new Rocks(); 22 | this.add(this.rocks); 23 | 24 | this.clouds = new Clouds(); 25 | this.clouds.position.set(0, 200, 0); 26 | this.clouds.rotation.x = Math.PI / 2; 27 | this.add(this.clouds); 28 | } 29 | 30 | update(elapsedTime) { 31 | this.grass.update(elapsedTime); 32 | this.clouds.update(elapsedTime); 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 20 20 | registry-url: https://registry.npmjs.org/ 21 | - name: Install dependencies 22 | run: npm install 23 | - name: Build project 24 | run: npm run build:lib 25 | - name: Publish to npm 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: npm publish --access public 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Greenheck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/shaders/skybox.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec3 vPosition; 4 | 5 | uniform float uSunAzimuth; // Sun azimuth angle (in degrees) 6 | uniform float uSunElevation; // Sun elevation angle (in degrees) 7 | uniform vec3 uSunColor; 8 | uniform vec3 uSkyColorLow; 9 | uniform vec3 uSkyColorHigh; 10 | uniform float uSunSize; 11 | 12 | void main() { 13 | // Convert angles from degrees to radians 14 | float azimuth = radians(uSunAzimuth); 15 | float elevation = radians(uSunElevation); 16 | 17 | // Calculate the sun direction vector based on azimuth and elevation 18 | vec3 sunDirection = normalize(vec3( 19 | cos(elevation) * sin(azimuth), 20 | sin(elevation), 21 | cos(elevation) * cos(azimuth) 22 | )); 23 | 24 | // Normalize the fragment position 25 | vec3 direction = normalize(vPosition); 26 | 27 | // Gradient for the sky (simple blue gradient) 28 | float t = direction.y * 0.5 + 0.5; 29 | vec3 skyColor = mix(uSkyColorLow, uSkyColorHigh, t); 30 | 31 | // Compute sun appearance 32 | float sunIntensity = pow(max(dot(direction, sunDirection), 0.0), 1000.0 / uSunSize); 33 | vec3 sunColor = uSunColor * sunIntensity; 34 | 35 | // Combine sun and sky color 36 | vec3 color = skyColor + sunColor; 37 | 38 | gl_FragColor = vec4(color, 1.0); 39 | } -------------------------------------------------------------------------------- /src/lib/presets/index.js: -------------------------------------------------------------------------------- 1 | import ashSmall from './ash_small.json'; 2 | import ashMedium from './ash_medium.json'; 3 | import ashLarge from './ash_large.json'; 4 | import aspenSmall from './aspen_small.json'; 5 | import aspenMedium from './aspen_medium.json'; 6 | import aspenLarge from './aspen_large.json'; 7 | import bush1 from './bush_1.json'; 8 | import bush2 from './bush_2.json'; 9 | import bush3 from './bush_3.json'; 10 | import oakSmall from './oak_small.json'; 11 | import oakMedium from './oak_medium.json'; 12 | import oakLarge from './oak_large.json'; 13 | import pineSmall from './pine_small.json'; 14 | import pineMedium from './pine_medium.json'; 15 | import pineLarge from './pine_large.json'; 16 | import TreeOptions from '../options'; 17 | 18 | export const TreePreset = { 19 | 'Ash Small': ashSmall, 20 | 'Ash Medium': ashMedium, 21 | 'Ash Large': ashLarge, 22 | 'Aspen Small': aspenSmall, 23 | 'Aspen Medium': aspenMedium, 24 | 'Aspen Large': aspenLarge, 25 | 'Bush 1': bush1, 26 | 'Bush 2': bush2, 27 | 'Bush 3': bush3, 28 | 'Oak Small': oakSmall, 29 | 'Oak Medium': oakMedium, 30 | 'Oak Large': oakLarge, 31 | 'Pine Small': pineSmall, 32 | 'Pine Medium': pineMedium, 33 | 'Pine Large': pineLarge, 34 | }; 35 | 36 | /** 37 | * @param {string} name The name of the preset to load 38 | * @returns {TreeOptions} 39 | */ 40 | export function loadPreset(name) { 41 | const preset = TreePreset[name]; 42 | return preset ? structuredClone(preset) : new TreeOptions(); 43 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 20 37 | cache: 'npm' 38 | - name: Install dependencies 39 | run: npm install 40 | - name: Build Tree.js Library 41 | run: npm run build:lib 42 | - name: Build Tree.js App 43 | run: npm run build:app 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v3 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v2 48 | with: 49 | # Upload dist repository 50 | path: './dist' 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v2 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dgreenheck/ez-tree", 3 | "author": "Daniel Greenheck", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/dgreenheck/ez-tree.git" 9 | }, 10 | "homepage": "https://dgreenheck.github.io/ez-tree/", 11 | "bugs": { 12 | "url": "https://github.com/dgreenheck/ez-tree/issues" 13 | }, 14 | "type": "module", 15 | "main": "build/ez-tree.umd.js", 16 | "module": "build/ez-tree.es.js", 17 | "exports": { 18 | ".": { 19 | "import": "./build/ez-tree.es.js", 20 | "require": "./build/ez-tree.umd.js" 21 | } 22 | }, 23 | "files": [ 24 | "build", 25 | "src", 26 | "LICENSE", 27 | "package.json", 28 | "README.md" 29 | ], 30 | "types": "./build/ez-tree.es.d.ts", 31 | "scripts": { 32 | "build:app": "vite build --config vite.app.config.js", 33 | "build:lib": "vite build --config vite.lib.config.js", 34 | "build:watch": "vite build --config vite.lib.config.js --watch", 35 | "app": "npm run build:lib && vite --config vite.app.config.js", 36 | "preview": "vite preview" 37 | }, 38 | "devDependencies": { 39 | "@types/three": "^0.169.0", 40 | "prettier": "3.3.3", 41 | "three": "^0.167.x", 42 | "vite": "^5.0.0", 43 | "vite-plugin-dts": "^3.9.1", 44 | "vite-plugin-static-copy": "^1.0.5" 45 | }, 46 | "peerDependencies": { 47 | "three": ">=0.167" 48 | }, 49 | "keywords": [ 50 | "threejs", 51 | "three-js", 52 | "procedural", 53 | "generation", 54 | "tree" 55 | ], 56 | "dependencies": { 57 | "tweakpane": "^4.0.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/presets/pine_small.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 11744, 3 | "type": "evergreen", 4 | "bark": { 5 | "type": "pine", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 1, 16 | "angle": { 17 | "1": 117, 18 | "2": 60, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 91, 23 | "1": 7, 24 | "2": 5 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": 0 33 | }, 34 | "gnarliness": { 35 | "0": 0.05, 36 | "1": 0.08, 37 | "2": 0, 38 | "3": 0 39 | }, 40 | "length": { 41 | "0": 39.55, 42 | "1": 12.12, 43 | "2": 10, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 0.55, 48 | "1": 0.41, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.16, 66 | "2": 0.3, 67 | "3": 0.3 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "pine", 84 | "billboard": "double", 85 | "angle": 10, 86 | "count": 21, 87 | "start": 0, 88 | "size": 0.965, 89 | "sizeVariance": 0.7, 90 | "tint": 16777215, 91 | "alphaTest": 0.3 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/aspen_medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 18020, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "birch", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 2, 16 | "angle": { 17 | "1": 75, 18 | "2": 32, 19 | "3": 7 20 | }, 21 | "children": { 22 | "0": 10, 23 | "1": 3, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": 0.0148 33 | }, 34 | "gnarliness": { 35 | "0": 0.05, 36 | "1": 0.12, 37 | "2": 0.12, 38 | "3": 0.02 39 | }, 40 | "length": { 41 | "0": 50, 42 | "1": 6.07, 43 | "2": 11.19, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 0.72, 48 | "1": 0.41, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.59, 66 | "2": 0.35, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.37, 71 | "1": 0.13, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "aspen", 84 | "billboard": "double", 85 | "angle": 30, 86 | "count": 11, 87 | "start": 0.124, 88 | "size": 2.5, 89 | "sizeVariance": 0.7, 90 | "tint": 16775778, 91 | "alphaTest": 0.5 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib/presets/pine_medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 13977, 3 | "type": "evergreen", 4 | "bark": { 5 | "type": "pine", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 1, 16 | "angle": { 17 | "1": 110, 18 | "2": 16, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 82, 23 | "1": 3, 24 | "2": 5 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.003 33 | }, 34 | "gnarliness": { 35 | "0": 0.05, 36 | "1": 0.08, 37 | "2": 0, 38 | "3": 0 39 | }, 40 | "length": { 41 | "0": 50, 42 | "1": 23.87, 43 | "2": 14.08, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 1.05, 48 | "1": 0.36, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.27, 66 | "2": 0.14, 67 | "3": 0.3 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "pine", 84 | "billboard": "double", 85 | "angle": 39, 86 | "count": 30, 87 | "start": 0.09, 88 | "size": 1.435, 89 | "sizeVariance": 0.201, 90 | "tint": 16777215, 91 | "alphaTest": 0.3 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/ash_medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 36330, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5.0 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 48, 18 | "2": 75, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 7, 23 | "1": 4, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.06 33 | }, 34 | "gnarliness": { 35 | "0": 0.03, 36 | "1": 0.25, 37 | "2": 0.2, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 43.47, 42 | "1": 27.14, 43 | "2": 9.51, 44 | "3": 4.6 45 | }, 46 | "radius": { 47 | "0": 2, 48 | "1": 0.63, 49 | "2": 0.76, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 8, 55 | "2": 6, 56 | "3": 4 57 | }, 58 | "segments": { 59 | "0": 12, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.23, 66 | "2": 0.33, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0.09, 77 | "1": -0.07, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "ash", 84 | "billboard": "double", 85 | "angle": 55, 86 | "count": 16, 87 | "start": 0, 88 | "size": 2.67, 89 | "sizeVariance": 0.72, 90 | "tint": 16777215, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/ash_small.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 26867, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5 12 | } 13 | }, 14 | "branch": { 15 | "levels": 2, 16 | "angle": { 17 | "1": 48, 18 | "2": 75, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 10, 23 | "1": 3, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.02 33 | }, 34 | "gnarliness": { 35 | "0": 0.11, 36 | "1": 0.09, 37 | "2": 0.05, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 23.87, 42 | "1": 18, 43 | "2": 5.59, 44 | "3": 4.6 45 | }, 46 | "radius": { 47 | "0": 0.81, 48 | "1": 0.56, 49 | "2": 0.76, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 10, 56 | "3": 10 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.53, 66 | "2": 0.33, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0.3, 77 | "1": -0.07, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "ash", 84 | "billboard": "double", 85 | "angle": 55, 86 | "count": 30, 87 | "start": 0, 88 | "size": 2.05, 89 | "sizeVariance": 0.717, 90 | "tint": 16777215, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/oak_large.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 23399, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 16774097, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 10 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 54, 18 | "2": 43, 19 | "3": 32 20 | }, 21 | "children": { 22 | "0": 9, 23 | "1": 5, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.025 33 | }, 34 | "gnarliness": { 35 | "0": -0.04, 36 | "1": 0.16, 37 | "2": -0.06, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 47.7, 42 | "1": 29.39, 43 | "2": 17.62, 44 | "3": 7.16 45 | }, 46 | "radius": { 47 | "0": 3, 48 | "1": 0.69, 49 | "2": 0.69, 50 | "3": 1.19 51 | }, 52 | "sections": { 53 | "0": 16, 54 | "1": 9, 55 | "2": 8, 56 | "3": 3 57 | }, 58 | "segments": { 59 | "0": 12, 60 | "1": 5, 61 | "2": 3, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.35, 66 | "2": 0.1, 67 | "3": 0.0 68 | }, 69 | "taper": { 70 | "0": 0.73, 71 | "1": 0.42, 72 | "2": 0.69, 73 | "3": 0.75 74 | }, 75 | "twist": { 76 | "0": -0.23, 77 | "1": 0.42, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "oak", 84 | "billboard": "double", 85 | "angle": 36, 86 | "count": 10, 87 | "start": 0.16, 88 | "size": 4.5, 89 | "sizeVariance": 0.7, 90 | "tint": 14013901, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/oak_medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 35729, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 16774097, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 10 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 54, 18 | "2": 58, 19 | "3": 32 20 | }, 21 | "children": { 22 | "0": 6, 23 | "1": 4, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.01 33 | }, 34 | "gnarliness": { 35 | "0": 0, 36 | "1": -0.1, 37 | "2": -0.15, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 37.24, 42 | "1": 11.08, 43 | "2": 12.39, 44 | "3": 7.16 45 | }, 46 | "radius": { 47 | "0": 1.41, 48 | "1": 0.9, 49 | "2": 0.69, 50 | "3": 1.19 51 | }, 52 | "sections": { 53 | "0": 8, 54 | "1": 6, 55 | "2": 3, 56 | "3": 1 57 | }, 58 | "segments": { 59 | "0": 7, 60 | "1": 5, 61 | "2": 3, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.49, 66 | "2": 0.06, 67 | "3": 0.12 68 | }, 69 | "taper": { 70 | "0": 0.73, 71 | "1": 0.42, 72 | "2": 0.69, 73 | "3": 0.75 74 | }, 75 | "twist": { 76 | "0": -0.23, 77 | "1": 0.42, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "oak", 84 | "billboard": "double", 85 | "angle": 42, 86 | "count": 18, 87 | "start": 0.16, 88 | "size": 2.5, 89 | "sizeVariance": 0.7, 90 | "tint": 14013901, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/oak_small.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 30895, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 16774097, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 10 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 54, 18 | "2": 58, 19 | "3": 32 20 | }, 21 | "children": { 22 | "0": 4, 23 | "1": 2, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.01 33 | }, 34 | "gnarliness": { 35 | "0": 0.07, 36 | "1": -0.08, 37 | "2": 0.11, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 28.08, 42 | "1": 4.55, 43 | "2": 9.78, 44 | "3": 7.16 45 | }, 46 | "radius": { 47 | "0": 1, 48 | "1": 1.02, 49 | "2": 0.69, 50 | "3": 1.19 51 | }, 52 | "sections": { 53 | "0": 16, 54 | "1": 9, 55 | "2": 8, 56 | "3": 1 57 | }, 58 | "segments": { 59 | "0": 7, 60 | "1": 5, 61 | "2": 3, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.49, 66 | "2": 0.06, 67 | "3": 0.12 68 | }, 69 | "taper": { 70 | "0": 0.73, 71 | "1": 0.42, 72 | "2": 0.69, 73 | "3": 0.75 74 | }, 75 | "twist": { 76 | "0": -0.23, 77 | "1": 0.42, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "oak", 84 | "billboard": "double", 85 | "angle": 42, 86 | "count": 14, 87 | "start": 0.16, 88 | "size": 1.38, 89 | "sizeVariance": 0.7, 90 | "tint": 14013901, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/ash_large.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 29919, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 39, 18 | "2": 39, 19 | "3": 51 20 | }, 21 | "children": { 22 | "0": 10, 23 | "1": 4, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.010869565217391311 33 | }, 34 | "gnarliness": { 35 | "0": -0.05, 36 | "1": 0.2, 37 | "2": 0.16, 38 | "3": 0.049999999999999996 39 | }, 40 | "length": { 41 | "0": 45, 42 | "1": 29.42, 43 | "2": 15.3, 44 | "3": 4.6 45 | }, 46 | "radius": { 47 | "0": 3.03, 48 | "1": 0.53, 49 | "2": 0.79, 50 | "3": 1.11 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 8, 55 | "2": 6, 56 | "3": 4 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.32, 66 | "2": 0.34, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.6199999999999999, 72 | "2": 0.7599999999999999, 73 | "3": 0 74 | }, 75 | "twist": { 76 | "0": 0.09, 77 | "1": -0.07, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "ash", 84 | "billboard": "double", 85 | "angle": 30, 86 | "count": 10, 87 | "start": 0.01, 88 | "size": 4.62, 89 | "sizeVariance": 0.72, 90 | "tint": 16777215, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/bush_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 45590, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 21.521739130434785, 18 | "2": 62.608695652173914, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 7, 23 | "1": 3, 24 | "2": 2 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.02 33 | }, 34 | "gnarliness": { 35 | "0": 0.11, 36 | "1": 0.09, 37 | "2": 0.05, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 0.1, 42 | "1": 15.302173913043479, 43 | "2": 5.59, 44 | "3": 4.6 45 | }, 46 | "radius": { 47 | "0": 0.5793478260869566, 48 | "1": 0.9521739130434783, 49 | "2": 0.76, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 6, 54 | "1": 6, 55 | "2": 10, 56 | "3": 10 57 | }, 58 | "segments": { 59 | "0": 4, 60 | "1": 4, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.53, 66 | "2": 0.33, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0.3, 77 | "1": -0.07, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "ash", 84 | "billboard": "double", 85 | "angle": 55, 86 | "count": 12, 87 | "start": 0, 88 | "size": 2.4456521739130435, 89 | "sizeVariance": 0.717, 90 | "tint": 14745557, 91 | "alphaTest": 0.5 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib/presets/aspen_small.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 36330, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "birch", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 2, 16 | "angle": { 17 | "1": 70, 18 | "2": 35, 19 | "3": 7 20 | }, 21 | "children": { 22 | "0": 4, 23 | "1": 3, 24 | "2": 3 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": 0.010869565217391311 33 | }, 34 | "gnarliness": { 35 | "0": 0.04, 36 | "1": -0.010000000000000007, 37 | "2": 0.12, 38 | "3": 0.02 39 | }, 40 | "length": { 41 | "0": 23.99, 42 | "1": 3.36, 43 | "2": 7.699999999999999, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 0.36999999999999994, 48 | "1": 0.41, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.44999999999999996, 66 | "2": 0.32999999999999996, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.37, 71 | "1": 0.13, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "aspen", 84 | "billboard": "double", 85 | "angle": 30, 86 | "count": 13, 87 | "start": 0.2, 88 | "size": 2.5, 89 | "sizeVariance": 0.7, 90 | "tint": 16775778, 91 | "alphaTest": 0.5 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/presets/aspen_large.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 30631, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "birch", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 2, 16 | "angle": { 17 | "1": 47, 18 | "2": 63, 19 | "3": 7 20 | }, 21 | "children": { 22 | "0": 10, 23 | "1": 6, 24 | "2": 0 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": 0.021739130434782622 33 | }, 34 | "gnarliness": { 35 | "0": 0.05, 36 | "1": -0.030000000000000006, 37 | "2": 0.12, 38 | "3": 0.02 39 | }, 40 | "length": { 41 | "0": 69.60000000000001, 42 | "1": 18.56, 43 | "2": 11.19, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 1.11, 48 | "1": 0.5800000000000001, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.62, 66 | "2": 0.049999999999999975, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7000000000000001, 71 | "1": 0.13, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "aspen", 84 | "billboard": "double", 85 | "angle": 36, 86 | "count": 20, 87 | "start": 0.15217391304347827, 88 | "size": 3.4782608695652173, 89 | "sizeVariance": 0.7, 90 | "tint": 16580390, 91 | "alphaTest": 0.5 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib/presets/pine_large.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 44166, 3 | "type": "evergreen", 4 | "bark": { 5 | "type": "pine", 6 | "tint": 16777215, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 1, 11 | "y": 1 12 | } 13 | }, 14 | "branch": { 15 | "levels": 1, 16 | "angle": { 17 | "1": 129.1304347826087, 18 | "2": 16, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 100, 23 | "1": 3, 24 | "2": 0 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": 0.009000000000000001 33 | }, 34 | "gnarliness": { 35 | "0": 0.05, 36 | "1": 0.08, 37 | "2": 0, 38 | "3": 0 39 | }, 40 | "length": { 41 | "0": 65.25217391304348, 42 | "1": 34.84782608695652, 43 | "2": 27.246739130434783, 44 | "3": 1 45 | }, 46 | "radius": { 47 | "0": 1.271739130434783, 48 | "1": 0.366304347826087, 49 | "2": 0.7, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 12, 54 | "1": 10, 55 | "2": 8, 56 | "3": 6 57 | }, 58 | "segments": { 59 | "0": 8, 60 | "1": 6, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.29347826086956524, 66 | "2": 0.14, 67 | "3": 0.3 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0, 77 | "1": 0, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "pine", 84 | "billboard": "double", 85 | "angle": 17, 86 | "count": 18, 87 | "start": 0.07608695652173914, 88 | "size": 2.608695652173913, 89 | "sizeVariance": 0.201, 90 | "tint": 16777215, 91 | "alphaTest": 0.3 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib/presets/bush_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 45590, 3 | "type": "deciduous", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5 12 | } 13 | }, 14 | "branch": { 15 | "levels": 2, 16 | "angle": { 17 | "1": 19.565217391304348, 18 | "2": 27.39130434782609, 19 | "3": 60 20 | }, 21 | "children": { 22 | "0": 10, 23 | "1": 3, 24 | "2": 2 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.02 33 | }, 34 | "gnarliness": { 35 | "0": 0.021739130434782594, 36 | "1": 0.10869565217391308, 37 | "2": 0.05, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 0.1, 42 | "1": 19.645652173913046, 43 | "2": 7.701086956521739, 44 | "3": 4.6 45 | }, 46 | "radius": { 47 | "0": 0.5793478260869566, 48 | "1": 0.9521739130434783, 49 | "2": 0.76, 50 | "3": 0.7 51 | }, 52 | "sections": { 53 | "0": 3, 54 | "1": 4, 55 | "2": 10, 56 | "3": 10 57 | }, 58 | "segments": { 59 | "0": 4, 60 | "1": 4, 61 | "2": 4, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.6413043478260869, 66 | "2": 0.7065217391304348, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0.3586956521739131, 77 | "1": -0.043478260869565244, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "aspen", 84 | "billboard": "double", 85 | "angle": 55, 86 | "count": 7, 87 | "start": 0, 88 | "size": 2.4456521739130435, 89 | "sizeVariance": 0.717, 90 | "tint": 14745557, 91 | "alphaTest": 0.5 92 | } 93 | } -------------------------------------------------------------------------------- /src/lib/presets/bush_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": 31343, 3 | "type": "evergreen", 4 | "bark": { 5 | "type": "oak", 6 | "tint": 13552830, 7 | "flatShading": false, 8 | "textured": true, 9 | "textureScale": { 10 | "x": 0.5, 11 | "y": 5 12 | } 13 | }, 14 | "branch": { 15 | "levels": 3, 16 | "angle": { 17 | "1": 66.52173913043478, 18 | "2": 52.82608695652174, 19 | "3": 0 20 | }, 21 | "children": { 22 | "0": 13, 23 | "1": 4, 24 | "2": 4 25 | }, 26 | "force": { 27 | "direction": { 28 | "x": 0, 29 | "y": 1, 30 | "z": 0 31 | }, 32 | "strength": -0.007 33 | }, 34 | "gnarliness": { 35 | "0": 0.05434782608695654, 36 | "1": 0.06521739130434778, 37 | "2": 0.05, 38 | "3": 0.09 39 | }, 40 | "length": { 41 | "0": 10.958695652173914, 42 | "1": 21.81739130434783, 43 | "2": 13.130434782608695, 44 | "3": 5.529347826086957 45 | }, 46 | "radius": { 47 | "0": 0.5793478260869566, 48 | "1": 0.9521739130434783, 49 | "2": 0.6858695652173914, 50 | "3": 0.7391304347826086 51 | }, 52 | "sections": { 53 | "0": 4, 54 | "1": 3, 55 | "2": 3, 56 | "3": 10 57 | }, 58 | "segments": { 59 | "0": 3, 60 | "1": 3, 61 | "2": 3, 62 | "3": 3 63 | }, 64 | "start": { 65 | "1": 0.14130434782608695, 66 | "2": 0.29347826086956524, 67 | "3": 0 68 | }, 69 | "taper": { 70 | "0": 0.7, 71 | "1": 0.7, 72 | "2": 0.7, 73 | "3": 0.7 74 | }, 75 | "twist": { 76 | "0": 0.3, 77 | "1": -0.03260869565217389, 78 | "2": 0, 79 | "3": 0 80 | } 81 | }, 82 | "leaves": { 83 | "type": "pine", 84 | "billboard": "double", 85 | "angle": 54, 86 | "count": 3, 87 | "start": 0.15217391304347827, 88 | "size": 3.0434782608695654, 89 | "sizeVariance": 0.45652173913043476, 90 | "tint": 10339327, 91 | "alphaTest": 0.5 92 | } 93 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | -------------------------------------------------------------------------------- /src/app/rocks.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GLTFLoader } from 'three/examples/jsm/Addons.js'; 3 | import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; 4 | 5 | let loaded = false; 6 | let _rock1Mesh = null; 7 | let _rock2Mesh = null; 8 | let _rock3Mesh = null; 9 | 10 | /** 11 | * 12 | * @returns {Promise} 13 | */ 14 | async function fetchAssets() { 15 | if (loaded) return; 16 | 17 | const gltfLoader = new GLTFLoader(); 18 | 19 | const dracoLoader = new DRACOLoader(); 20 | dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/'); 21 | gltfLoader.setDRACOLoader(dracoLoader); 22 | 23 | _rock1Mesh = (await gltfLoader.loadAsync('rock1.glb')).scene.children[0]; 24 | _rock2Mesh = (await gltfLoader.loadAsync('rock2.glb')).scene.children[0]; 25 | _rock3Mesh = (await gltfLoader.loadAsync('rock3.glb')).scene.children[0]; 26 | 27 | loaded = true; 28 | } 29 | 30 | export class RockOptions { 31 | /** 32 | * Scale factor 33 | */ 34 | size = { x: 2, y: 2, z: 2 }; 35 | 36 | /** 37 | * Maximum variation in the rock size 38 | */ 39 | sizeVariation = { x: 3, y: 3, z: 3 }; 40 | } 41 | 42 | export class Rocks extends THREE.Group { 43 | constructor(options = new RockOptions()) { 44 | super(); 45 | 46 | /** 47 | * @type {RockOptions} 48 | */ 49 | this.options = options; 50 | 51 | fetchAssets().then(() => { 52 | this.add(this.generateInstances(_rock1Mesh)); 53 | this.add(this.generateInstances(_rock2Mesh)); 54 | this.add(this.generateInstances(_rock3Mesh)); 55 | }); 56 | } 57 | 58 | generateInstances(mesh) { 59 | const instancedMesh = new THREE.InstancedMesh(mesh.geometry, mesh.material, 200); 60 | 61 | const dummy = new THREE.Object3D(); 62 | 63 | let count = 0; 64 | for (let i = 0; i < 50; i++) { 65 | // Set position randomly 66 | const p = new THREE.Vector3( 67 | 2 * (Math.random() - 0.5) * 250, 68 | 0.3, 69 | 2 * (Math.random() - 0.5) * 250 70 | ); 71 | 72 | dummy.position.copy(p); 73 | 74 | // Set rotation randomly 75 | dummy.rotation.set( 76 | 0, 77 | 2 * Math.PI * Math.random(), 78 | 0 79 | ); 80 | 81 | // Set scale randomly 82 | dummy.scale.set( 83 | this.options.sizeVariation.x * Math.random() + this.options.size.x, 84 | this.options.sizeVariation.y * Math.random() + this.options.size.y, 85 | this.options.sizeVariation.z * Math.random() + this.options.size.z 86 | ); 87 | 88 | // Apply the transformation to the instance 89 | dummy.updateMatrix(); 90 | 91 | instancedMesh.setMatrixAt(count, dummy.matrix); 92 | count++; 93 | } 94 | instancedMesh.count = count; 95 | 96 | // Ensure the transformation is updated in the GPU 97 | instancedMesh.instanceMatrix.needsUpdate = true; 98 | 99 | instancedMesh.castShadow = true; 100 | 101 | return instancedMesh; 102 | } 103 | } -------------------------------------------------------------------------------- /src/app/main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; 3 | import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; 4 | import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js'; 5 | import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'; 6 | import { setupUI } from './ui'; 7 | import { createScene } from './scene'; 8 | 9 | document.addEventListener('DOMContentLoaded', async () => { 10 | const container = document.getElementById('app') 11 | 12 | // User needs to interact with the page before audio will play 13 | container.addEventListener('click', toggleAudio); 14 | 15 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 16 | renderer.setClearColor(0); 17 | renderer.setSize(container.clientWidth, container.clientHeight); 18 | renderer.setPixelRatio(devicePixelRatio); 19 | renderer.shadowMap.enabled = true; 20 | renderer.shadowMap.type = THREE.PCFShadowMap; 21 | renderer.toneMapping = THREE.NeutralToneMapping; 22 | renderer.toneMappingExposure = 2; 23 | container.appendChild(renderer.domElement); 24 | 25 | const { scene, environment, tree, camera, controls } = await createScene(renderer); 26 | 27 | const composer = new EffectComposer(renderer); 28 | 29 | composer.addPass(new RenderPass(scene, camera)); 30 | 31 | const smaaPass = new SMAAPass( 32 | container.clientWidth * renderer.getPixelRatio(), 33 | container.clientHeight * renderer.getPixelRatio()); 34 | composer.addPass(smaaPass); 35 | 36 | composer.addPass(new OutputPass()); 37 | 38 | const clock = new THREE.Clock(); 39 | function animate() { 40 | // Update time for wind sway shaders 41 | const t = clock.getElapsedTime(); 42 | tree.update(t); 43 | scene.getObjectByName('Forest').children.forEach((o) => o.update(t)); 44 | environment.update(t); 45 | 46 | controls.update(); 47 | composer.render(); 48 | requestAnimationFrame(animate); 49 | } 50 | 51 | function resize() { 52 | renderer.setSize(container.clientWidth, container.clientHeight); 53 | smaaPass.setSize(container.clientWidth, container.clientHeight); 54 | composer.setSize(container.clientWidth, container.clientHeight); 55 | camera.aspect = container.clientWidth / container.clientHeight; 56 | camera.updateProjectionMatrix(); 57 | } 58 | 59 | window.addEventListener('resize', resize); 60 | 61 | setupUI(tree, environment, renderer, scene, camera, controls, 'Ash Medium'); 62 | animate(); 63 | resize(); 64 | 65 | document.getElementById('audio-status').style.display = 'block'; 66 | }); 67 | 68 | window.toggleAudio = function () { 69 | document.getElementById('app').removeEventListener('click', toggleAudio); 70 | 71 | if (window.isAudioPlaying) { 72 | window.isAudioPlaying = false; 73 | document.getElementById('audio-status').src = "icon_muted.png"; 74 | document.getElementById('background-audio').pause(); 75 | } else { 76 | window.isAudioPlaying = true; 77 | document.getElementById('audio-status').src = "icon_playing.png"; 78 | document.getElementById('background-audio').play(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/app/clouds.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export class Clouds extends THREE.Mesh { 4 | constructor() { 5 | super(); 6 | 7 | this.material = new THREE.MeshBasicMaterial({ 8 | transparent: true, // Allow alpha blending if needed 9 | opacity: 0.9, 10 | fog: true, 11 | }); 12 | 13 | this.material.onBeforeCompile = (shader) => { 14 | shader.uniforms.uTime = { value: 0.0 }; 15 | 16 | shader.vertexShader = ` 17 | varying vec2 vUv; 18 | varying vec3 vWorldPosition; 19 | ` + shader.vertexShader; 20 | 21 | shader.fragmentShader = ` 22 | uniform float uTime; 23 | varying vec2 vUv; 24 | varying vec3 vWorldPosition; 25 | ` + shader.fragmentShader; 26 | 27 | shader.vertexShader = shader.vertexShader.replace( 28 | '#include ', 29 | `#include 30 | vUv = uv; 31 | vWorldPosition = worldPosition.xyz; 32 | ` 33 | ); 34 | 35 | shader.fragmentShader = shader.fragmentShader.replace( 36 | `void main() {`, 37 | `// 2D Simplex noise function 38 | vec3 permute(vec3 x) { 39 | return mod(((x*34.0)+1.0)*x, 289.0); 40 | } 41 | 42 | float snoise(vec2 v){ 43 | const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); 44 | vec2 i = floor(v + dot(v, C.yy) ); 45 | vec2 x0 = v - i + dot(i, C.xx); 46 | vec2 i1; 47 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 48 | vec4 x12 = x0.xyxy + C.xxzz; 49 | x12.xy -= i1; 50 | i = mod(i, 289.0); 51 | vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 )); 52 | vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0); 53 | m = m*m ; 54 | m = m*m ; 55 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 56 | vec3 h = abs(x) - 0.5; 57 | vec3 ox = floor(x + 0.5); 58 | vec3 a0 = x - ox; 59 | m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ); 60 | vec3 g; 61 | g.x = a0.x * x0.x + h.x * x0.y; 62 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 63 | return 130.0 * dot(m, g); 64 | } 65 | 66 | void main() {`, 67 | ); 68 | 69 | shader.fragmentShader = shader.fragmentShader.replace( 70 | '#include ', 71 | ` 72 | float n = snoise(vUv * 5.0 + uTime / 40.0) + snoise(vUv * 10.0 + uTime / 30.0); 73 | float cloud = smoothstep(0.2, 0.8, 0.5 * n + 0.4); 74 | vec4 cloudColor = vec4(1.0, 1.0, 1.0, 1.0); 75 | diffuseColor = vec4(1.0, 1.0, 1.0, cloud * opacity / (0.01 * length(vWorldPosition))); 76 | ` 77 | ); 78 | 79 | this.material.userData.shader = shader; 80 | }; 81 | 82 | // Create a quad to apply the cloud shader to 83 | this.geometry = new THREE.PlaneGeometry(2000, 2000); 84 | } 85 | 86 | update(elapsedTime) { 87 | const shader = this.material.userData.shader; 88 | if (shader) { 89 | shader.uniforms.uTime.value = elapsedTime; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/app/noise.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | function mod3(v) { 4 | return new THREE.Vector3( 5 | v.x - Math.floor(v.x / 289.0) * 289.0, 6 | v.y - Math.floor(v.y / 289.0) * 289.0, 7 | v.z - Math.floor(v.z / 289.0) * 289.0 8 | ); 9 | } 10 | 11 | function permute3(v) { 12 | return mod3(new THREE.Vector3( 13 | ((v.x * 34.0) + 1.0) * v.x, 14 | ((v.y * 34.0) + 1.0) * v.y, 15 | ((v.z * 34.0) + 1.0) * v.z 16 | )); 17 | } 18 | 19 | /** 20 | * 21 | * @param {THREE.Vector2} v 22 | * @returns 23 | */ 24 | export function simplex2d(v) { 25 | // Good 26 | const C = new THREE.Vector4( 27 | 0.211324865405187, 28 | 0.366025403784439, 29 | -0.577350269189626, 30 | 0.024390243902439 31 | ); 32 | 33 | // Good 34 | let i = new THREE.Vector2( 35 | Math.floor(v.x + C.y * (v.x + v.y)), 36 | Math.floor(v.y + C.y * (v.x + v.y)) 37 | ); 38 | 39 | // Good 40 | let x0 = new THREE.Vector2( 41 | v.x - i.x + C.x * (i.x + i.y), 42 | v.y - i.y + C.x * (i.x + i.y), 43 | ); 44 | 45 | // Good 46 | let i1 = new THREE.Vector2( 47 | (x0.x > x0.y) ? 1.0 : 0.0, 48 | (x0.x > x0.y) ? 0.0 : 1.0 49 | ); 50 | 51 | let x12 = new THREE.Vector4( 52 | x0.x + C.x - i1.x, 53 | x0.y + C.x - i1.y, 54 | x0.x + C.z, 55 | x0.y + C.z 56 | ); 57 | 58 | i = new THREE.Vector2( 59 | i.x - Math.floor(i.x * (1.0 / 289.0)) * 289.0, 60 | i.y - Math.floor(i.y * (1.0 / 289.0)) * 289.0 61 | ); 62 | 63 | let p = new THREE.Vector3( 64 | i.y, 65 | i.y + i1.y, 66 | i.y + 1.0 67 | ); 68 | p = permute3(p); 69 | p = permute3(new THREE.Vector3( 70 | p.x + i.x, 71 | p.y + i.x + i1.x, 72 | p.z + i.x + 1.0 73 | )); 74 | 75 | let m = new THREE.Vector3( 76 | Math.max(0.0, 0.5 - x0.dot(x0)), 77 | Math.max(0.0, 0.5 - (x12.x * x12.x + x12.y * x12.y)), 78 | Math.max(0.0, 0.5 - (x12.z * x12.z + x12.w * x12.w)) 79 | ); 80 | m = new THREE.Vector3( 81 | m.x * m.x, 82 | m.y * m.y, 83 | m.z * m.z 84 | ); 85 | m = new THREE.Vector3( 86 | m.x * m.x, 87 | m.y * m.y, 88 | m.z * m.z 89 | ); 90 | 91 | 92 | let x = new THREE.Vector3( 93 | 2.0 * ((p.x * C.w) - Math.floor(p.x * C.w)) - 1.0, 94 | 2.0 * ((p.y * C.w) - Math.floor(p.y * C.w)) - 1.0, 95 | 2.0 * ((p.z * C.w) - Math.floor(p.z * C.w)) - 1.0 96 | ); 97 | 98 | let h = new THREE.Vector3( 99 | Math.abs(x.x) - 0.5, 100 | Math.abs(x.y) - 0.5, 101 | Math.abs(x.z) - 0.5 102 | ) 103 | 104 | let ox = new THREE.Vector3( 105 | Math.floor(x.x + 0.5), 106 | Math.floor(x.y + 0.5), 107 | Math.floor(x.z + 0.5) 108 | ); 109 | 110 | let a0 = new THREE.Vector3( 111 | x.x - ox.x, 112 | x.y - ox.y, 113 | x.z - ox.z 114 | ); 115 | 116 | m = new THREE.Vector3( 117 | m.x * (1.79284291400159 - 0.85373472095314 * (a0.x * a0.x + h.x * h.x)), 118 | m.y * (1.79284291400159 - 0.85373472095314 * (a0.y * a0.y + h.y * h.y)), 119 | m.z * (1.79284291400159 - 0.85373472095314 * (a0.z * a0.z + h.z * h.z)), 120 | ); 121 | 122 | let g = new THREE.Vector3( 123 | a0.x * x0.x + h.x * x0.y, 124 | a0.y * x12.x + h.y * x12.y, 125 | a0.z * x12.z + h.z * x12.w 126 | ); 127 | 128 | const n = 130.0 * m.dot(g); 129 | 130 | return n; 131 | } 132 | -------------------------------------------------------------------------------- /src/app/scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; 3 | import { Tree, TreePreset } from '@dgreenheck/ez-tree'; 4 | import { Environment } from './environment'; 5 | 6 | function sleep(ms) { 7 | return new Promise(resolve => setTimeout(resolve, ms)); 8 | } 9 | 10 | function paintUI() { 11 | return new Promise(resolve => requestAnimationFrame(resolve)); 12 | } 13 | 14 | /** 15 | * Creates a new instance of the Three.js scene 16 | * @param {THREE.WebGLRenderer} renderer 17 | * @returns 18 | */ 19 | export async function createScene(renderer) { 20 | const scene = new THREE.Scene(); 21 | scene.fog = new THREE.FogExp2(0x94b9f8, 0.0015); 22 | 23 | const environment = new Environment(); 24 | scene.add(environment); 25 | 26 | const camera = new THREE.PerspectiveCamera( 27 | 60, 28 | window.innerWidth / window.innerHeight, 29 | 0.1, 30 | 2000, 31 | ); 32 | camera.position.set(100, 20, 0); 33 | 34 | const controls = new OrbitControls(camera, renderer.domElement); 35 | controls.enableDamping = true; 36 | controls.enablePan = true; 37 | controls.minPolarAngle = Math.PI / 2 - 0.2; 38 | controls.maxPolarAngle = Math.PI / 2 + 0.13; 39 | controls.minDistance = 10; 40 | controls.maxDistance = 150; 41 | controls.target.set(0, 25, 0); 42 | controls.update(); 43 | 44 | const tree = new Tree(); 45 | tree.loadPreset('Ash Medium'); 46 | tree.generate(); 47 | tree.castShadow = true; 48 | tree.receiveShadow = true; 49 | scene.add(tree); 50 | 51 | // Add a forest of trees in the background 52 | const forest = new THREE.Group(); 53 | forest.name = 'Forest'; 54 | 55 | const logoElement = document.getElementById('logo'); 56 | const progressElement = document.getElementById('loading-text'); 57 | 58 | logoElement.style.clipPath = `inset(100% 0% 0% 0%)`; 59 | progressElement.innerHTML = 'LOADING... 0%'; 60 | 61 | const treeCount = 100; 62 | const minDistance = 175; 63 | const maxDistance = 500; 64 | 65 | function createTree() { 66 | const r = minDistance + Math.random() * maxDistance; 67 | const theta = 2 * Math.PI * Math.random(); 68 | const presets = Object.keys(TreePreset); 69 | const index = Math.floor(Math.random() * presets.length); 70 | 71 | const t = new Tree(); 72 | t.position.set(r * Math.cos(theta), 0, r * Math.sin(theta)); 73 | t.loadPreset(presets[index]); 74 | t.options.seed = 10000 * Math.random(); 75 | t.generate(); 76 | t.castShadow = true; 77 | t.receiveShadow = true; 78 | 79 | forest.add(t); 80 | } 81 | 82 | async function loadTrees(i) { 83 | while (i < treeCount) { 84 | createTree(); 85 | 86 | const progress = Math.floor(100 * (i + 1) / treeCount); 87 | 88 | // Update progress UI 89 | logoElement.style.clipPath = `inset(${100 - progress}% 0% 0% 0%)`; 90 | progressElement.innerText = `LOADING... ${progress}%`; 91 | 92 | // Wait for the next animation frame to continue 93 | await paintUI(); 94 | 95 | i++; 96 | } 97 | 98 | // All trees are loaded, hide loading screen 99 | await sleep(300); 100 | logoElement.style.clipPath = `inset(0% 0% 0% 0%)`; 101 | document.getElementById('loading-screen').style.display = 'none'; 102 | } 103 | 104 | // Start the tree loading process 105 | await loadTrees(0); 106 | 107 | scene.add(forest); 108 | 109 | return { 110 | scene, 111 | environment, 112 | tree, 113 | camera, 114 | controls 115 | } 116 | } -------------------------------------------------------------------------------- /src/lib/textures.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import birchAo from './assets/bark/birch_ao_1k.jpg'; 4 | import birchColor from './assets/bark/birch_color_1k.jpg'; 5 | import birchNormal from './assets/bark/birch_normal_1k.jpg'; 6 | import birchRoughness from './assets/bark/birch_roughness_1k.jpg'; 7 | 8 | import oakAo from './assets/bark/oak_ao_1k.jpg'; 9 | import oakColor from './assets/bark/oak_color_1k.jpg'; 10 | import oakNormal from './assets/bark/oak_normal_1k.jpg'; 11 | import oakRoughness from './assets/bark/oak_roughness_1k.jpg'; 12 | 13 | import pineAo from './assets/bark/pine_ao_1k.jpg'; 14 | import pineColor from './assets/bark/pine_color_1k.jpg'; 15 | import pineNormal from './assets/bark/pine_normal_1k.jpg'; 16 | import pineRoughness from './assets/bark/pine_roughness_1k.jpg'; 17 | 18 | import willowAo from './assets/bark/willow_ao_1k.jpg'; 19 | import willowColor from './assets/bark/willow_color_1k.jpg'; 20 | import willowNormal from './assets/bark/willow_normal_1k.jpg'; 21 | import willowRoughness from './assets/bark/willow_roughness_1k.jpg'; 22 | 23 | import ashLeaves from './assets/leaves/ash_color.png'; 24 | import aspenLeaves from './assets/leaves/aspen_color.png'; 25 | import oakLeaves from './assets/leaves/oak_color.png'; 26 | import pineLeaves from './assets/leaves/pine_color.png'; 27 | 28 | const textureLoader = new THREE.TextureLoader(); 29 | 30 | /** 31 | * Gets a bark texture for the specified bark type 32 | * @param {string} barkType 33 | * @param {'ao' | 'color' | 'normal' | 'roughness'} fileType 34 | * @param {THREE.Vector2} scale 35 | * @returns 36 | */ 37 | export function getBarkTexture(barkType, fileType, scale = { x: 1, y: 1 }) { 38 | const texture = textures.bark[barkType][fileType]; 39 | texture.wrapS = THREE.RepeatWrapping; 40 | texture.wrapT = THREE.RepeatWrapping; 41 | texture.repeat.x = scale.x; 42 | texture.repeat.y = 1 / scale.y; 43 | return texture; 44 | } 45 | 46 | /** 47 | * Gets the leaf texture for the specified leaf type 48 | * @param {string} leafType 49 | * @returns 50 | */ 51 | export function getLeafTexture(leafType) { 52 | return textures.leaves[leafType]; 53 | } 54 | 55 | /** 56 | * 57 | * @param {string} url Path to texture 58 | * @param {THREE.Vector2} scale Scale of the texture repeat 59 | * @param {boolean} srgb Set to true to set texture color space to SRGB 60 | * @returns {THREE.Texture} 61 | */ 62 | const loadTexture = (url, srgb = true) => { 63 | const texture = textureLoader.load(url); 64 | texture.premultiplyAlpha = true; 65 | if (srgb) { 66 | texture.colorSpace = THREE.SRGBColorSpace; 67 | } 68 | 69 | return texture; 70 | }; 71 | 72 | const textures = { 73 | "bark": { 74 | "birch": { 75 | "ao": loadTexture(birchAo, false), 76 | "color": loadTexture(birchColor), 77 | "normal": loadTexture(birchNormal, false), 78 | "roughness": loadTexture(birchRoughness, false), 79 | }, 80 | "oak": { 81 | "ao": loadTexture(oakAo, false), 82 | "color": loadTexture(oakColor), 83 | "normal": loadTexture(oakNormal, false), 84 | "roughness": loadTexture(oakRoughness, false), 85 | }, 86 | "pine": { 87 | "ao": loadTexture(pineAo, false), 88 | "color": loadTexture(pineColor), 89 | "normal": loadTexture(pineNormal, false), 90 | "roughness": loadTexture(pineRoughness, false), 91 | }, 92 | "willow": { 93 | "ao": loadTexture(willowAo, false), 94 | "color": loadTexture(willowColor), 95 | "normal": loadTexture(willowNormal, false), 96 | "roughness": loadTexture(willowRoughness, false), 97 | } 98 | }, 99 | "leaves": { 100 | "ash": loadTexture(ashLeaves), 101 | "aspen": loadTexture(aspenLeaves), 102 | "oak": loadTexture(oakLeaves), 103 | "pine": loadTexture(pineLeaves) 104 | } 105 | }; -------------------------------------------------------------------------------- /src/lib/options.js: -------------------------------------------------------------------------------- 1 | import { BarkType, Billboard, LeafType, TreeType } from './enums'; 2 | 3 | export default class TreeOptions { 4 | constructor() { 5 | this.seed = 0; 6 | this.type = TreeType.Deciduous; 7 | 8 | // Bark parameters 9 | this.bark = { 10 | // The bark texture 11 | type: BarkType.Oak, 12 | 13 | // Tint of the tree trunk 14 | tint: 0xffffff, 15 | 16 | // Use face normals for shading instead of vertex normals 17 | flatShading: false, 18 | 19 | // Apply texture to bark 20 | textured: true, 21 | 22 | // Scale for the texture 23 | textureScale: { x: 1, y: 1 }, 24 | }; 25 | 26 | // Branch parameters 27 | this.branch = { 28 | // Number of branch recursion levels. 0 = trunk only 29 | levels: 3, 30 | 31 | // Angle of the child branches relative to the parent branch (degrees) 32 | angle: { 33 | 1: 70, 34 | 2: 60, 35 | 3: 60, 36 | }, 37 | 38 | // Number of children per branch level 39 | children: { 40 | 0: 7, 41 | 1: 7, 42 | 2: 5, 43 | }, 44 | 45 | // External force encouraging tree growth in a particular direction 46 | force: { 47 | direction: { x: 0, y: 1, z: 0 }, 48 | strength: 0.01, 49 | }, 50 | 51 | // Amount of curling/twisting at each branch level 52 | gnarliness: { 53 | 0: 0.15, 54 | 1: 0.2, 55 | 2: 0.3, 56 | 3: 0.02, 57 | }, 58 | 59 | // Length of each branch level 60 | length: { 61 | 0: 20, 62 | 1: 20, 63 | 2: 10, 64 | 3: 1, 65 | }, 66 | 67 | // Radius of each branch level 68 | radius: { 69 | 0: 1.5, 70 | 1: 0.7, 71 | 2: 0.7, 72 | 3: 0.7, 73 | }, 74 | 75 | // Number of sections per branch level 76 | sections: { 77 | 0: 12, 78 | 1: 10, 79 | 2: 8, 80 | 3: 6, 81 | }, 82 | 83 | // Number of radial segments per branch level 84 | segments: { 85 | 0: 8, 86 | 1: 6, 87 | 2: 4, 88 | 3: 3, 89 | }, 90 | 91 | // Defines where child branches start forming on the parent branch 92 | start: { 93 | 1: 0.4, 94 | 2: 0.3, 95 | 3: 0.3, 96 | }, 97 | 98 | // Taper at each branch level 99 | taper: { 100 | 0: 0.7, 101 | 1: 0.7, 102 | 2: 0.7, 103 | 3: 0.7, 104 | }, 105 | 106 | // Amount of twist at each branch level 107 | twist: { 108 | 0: 0, 109 | 1: 0, 110 | 2: 0, 111 | 3: 0, 112 | }, 113 | }; 114 | 115 | // Leaf parameters 116 | this.leaves = { 117 | // Leaf texture to use 118 | type: LeafType.Oak, 119 | 120 | // Whether to use single or double/perpendicular billboards 121 | billboard: Billboard.Double, 122 | 123 | // Angle of leaves relative to parent branch (degrees) 124 | angle: 10, 125 | 126 | // Number of leaves 127 | count: 1, 128 | 129 | // Where leaves start to grow on the length of the branch (0 to 1) 130 | start: 0, 131 | 132 | // Size of the leaves 133 | size: 2.5, 134 | 135 | // Variance in leaf size between each instance 136 | sizeVariance: 0.7, 137 | 138 | // Tint color for the leaves 139 | tint: 0xffffff, 140 | 141 | // Controls transparency of leaf texture 142 | alphaTest: 0.5, 143 | }; 144 | } 145 | 146 | /** 147 | * Copies the values from source into this object 148 | * @param {TreeOptions} source 149 | */ 150 | copy(source, target = this) { 151 | for (let key in source) { 152 | if (source.hasOwnProperty(key) && target.hasOwnProperty(key)) { 153 | if (typeof source[key] === 'object' && source[key] !== null) { 154 | this.copy(source[key], target[key]); 155 | } else { 156 | target[key] = source[key]; 157 | } 158 | } 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | EZ-Tree | Procedural Tree Generator 21 | 23 | 24 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
LOADING... 0%
49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 | 57 |
58 |
59 | App Icon 60 |

EZ-Tree

61 |

Procedural Tree Generator

62 |

Made by Dan Greenheck

63 | www.dangreenheck.com 64 | 76 | 77 |
78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/app/skybox.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { degToRad } from 'three/src/math/MathUtils.js'; 3 | import fragmentShader from './shaders/skybox.frag?raw'; 4 | import vertexShader from './shaders/skybox.vert?raw'; 5 | 6 | export class SkyboxOptions { 7 | constructor() { 8 | /** 9 | * Azimuth of the sun in degrees 10 | */ 11 | this.sunAzimuth = 90; 12 | 13 | /** 14 | * Elevation of the sun in degrees 15 | */ 16 | this.sunElevation = 30; 17 | 18 | /** 19 | * Color of the sun 20 | */ 21 | this.sunColor = new THREE.Color(0xffe5b0).convertLinearToSRGB(); 22 | 23 | /** 24 | * Size of the sun in the sky 25 | */ 26 | this.sunSize = 1; 27 | 28 | /** 29 | * Color of the sky in the lower part of the sky 30 | */ 31 | this.skyColorLow = new THREE.Color(0x6fa2ef).convertLinearToSRGB(); 32 | 33 | /** 34 | * Color of the sun in the higher part of the sky 35 | */ 36 | this.skyColorHigh = new THREE.Color(0x2053ff).convertLinearToSRGB(); 37 | } 38 | } 39 | 40 | /** 41 | * Configurable skybox with sun and built-in lighting 42 | */ 43 | export class Skybox extends THREE.Mesh { 44 | /** 45 | * 46 | * @param {SkyboxOptions} options 47 | */ 48 | constructor(options = new SkyboxOptions()) { 49 | super(); 50 | 51 | this.name = 'Skybox'; 52 | 53 | // Create a box geometry and apply the skybox material 54 | this.geometry = new THREE.SphereGeometry(900, 900, 900); 55 | 56 | // Create the skybox material with the shaders 57 | this.material = new THREE.ShaderMaterial({ 58 | vertexShader, 59 | fragmentShader, 60 | uniforms: { 61 | uSunAzimuth: { value: options.sunAzimuth }, 62 | uSunElevation: { value: options.sunElevation }, 63 | uSunColor: { value: options.sunColor }, 64 | uSkyColorLow: { value: options.skyColorLow }, 65 | uSkyColorHigh: { value: options.skyColorHigh }, 66 | uSunSize: { value: options.sunSize } 67 | }, 68 | side: THREE.BackSide 69 | }); 70 | 71 | this.sun = new THREE.DirectionalLight(); 72 | this.sun.intensity = 5; 73 | this.sun.color = options.sunColor; 74 | this.sun.position.set(50, 100, 50); 75 | this.sun.castShadow = true; 76 | this.sun.shadow.camera.left = -100; 77 | this.sun.shadow.camera.right = 100; 78 | this.sun.shadow.camera.top = 100; 79 | this.sun.shadow.camera.bottom = -100; 80 | this.sun.shadow.mapSize = new THREE.Vector2(512, 512); 81 | this.sun.shadow.bias = -0.001; 82 | this.sun.shadow.normalBias = 0.2; 83 | this.add(this.sun); 84 | 85 | const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); 86 | this.add(ambientLight); 87 | 88 | this.updateSunPosition(); 89 | } 90 | 91 | updateSunPosition() { 92 | const el = degToRad(this.sunElevation); 93 | const az = degToRad(this.sunAzimuth); 94 | 95 | this.sun.position.set( 96 | 100 * Math.cos(el) * Math.sin(az), 97 | 100 * Math.sin(el), 98 | 100 * Math.cos(el) * Math.cos(az) 99 | ); 100 | } 101 | 102 | /** 103 | * @returns {number} 104 | */ 105 | get sunAzimuth() { 106 | return this.material.uniforms.uSunAzimuth.value; 107 | } 108 | 109 | set sunAzimuth(azimuth) { 110 | this.material.uniforms.uSunAzimuth.value = azimuth; 111 | this.updateSunPosition(); 112 | } 113 | 114 | /** 115 | * @returns {number} 116 | */ 117 | get sunElevation() { 118 | return this.material.uniforms.uSunElevation.value; 119 | } 120 | 121 | set sunElevation(elevation) { 122 | this.material.uniforms.uSunElevation.value = elevation; 123 | this.updateSunPosition(); 124 | } 125 | 126 | /** 127 | * @returns {THREE.Color} 128 | */ 129 | get sunColor() { 130 | return this.material.uniforms.uSunColor.value; 131 | } 132 | 133 | set sunColor(color) { 134 | this.material.uniforms.uSunColor.value = color; 135 | this.sun.color = color; 136 | } 137 | 138 | /** 139 | * @returns {THREE.Color} 140 | */ 141 | get skyColorLow() { 142 | return this.material.uniforms.uSkyColorLow.value; 143 | } 144 | 145 | set skyColorLow(color) { 146 | this.material.uniforms.uSkyColorLow.value = color; 147 | } 148 | 149 | /** 150 | * @returns {THREE.Color} 151 | */ 152 | get skyColorHigh() { 153 | return this.material.uniforms.uSkyColorHigh.value; 154 | } 155 | 156 | set skyColorHigh(color) { 157 | this.material.uniforms.uSkyColorHigh.value = color; 158 | } 159 | 160 | /** 161 | * @returns {number} 162 | */ 163 | get sunSize() { 164 | return this.material.uniforms.uSunSize.value; 165 | } 166 | 167 | set sunSize(size) { 168 | this.material.uniforms.uSunSize.value = size; 169 | } 170 | } -------------------------------------------------------------------------------- /src/app/ground.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GrassOptions } from './grass'; 3 | 4 | let loaded = false; 5 | let _grassTexture = null; 6 | let _dirtTexture = null; 7 | let _dirtNormal = null; 8 | 9 | /** 10 | * 11 | * @returns {Promise} 12 | */ 13 | async function fetchAssets() { 14 | if (loaded) return; 15 | 16 | const textureLoader = new THREE.TextureLoader(); 17 | 18 | _grassTexture = await textureLoader.loadAsync('grass.jpg'); 19 | _grassTexture.wrapS = THREE.RepeatWrapping; 20 | _grassTexture.wrapT = THREE.RepeatWrapping; 21 | _grassTexture.colorSpace = THREE.SRGBColorSpace; 22 | 23 | _dirtTexture = await textureLoader.loadAsync('dirt_color.jpg'); 24 | _dirtTexture.wrapS = THREE.RepeatWrapping; 25 | _dirtTexture.wrapT = THREE.RepeatWrapping; 26 | _dirtTexture.colorSpace = THREE.SRGBColorSpace; 27 | 28 | _dirtNormal = await textureLoader.loadAsync('dirt_normal.jpg'); 29 | _dirtNormal.wrapS = THREE.RepeatWrapping; 30 | _dirtNormal.wrapT = THREE.RepeatWrapping; 31 | 32 | loaded = true; 33 | } 34 | 35 | export class Ground extends THREE.Mesh { 36 | constructor(options = new GrassOptions()) { 37 | super(); 38 | 39 | /** 40 | * @type {GrassOptions} 41 | */ 42 | this.options = options; 43 | 44 | fetchAssets().then(() => { 45 | // Ground plane with procedural grass/dirt texture 46 | this.material = new THREE.MeshPhongMaterial({ 47 | emissive: new THREE.Color(0xffffff), 48 | emissiveIntensity: 0.01, 49 | normalMap: _dirtNormal, 50 | shininess: 0.1 51 | }); 52 | 53 | this.material.onBeforeCompile = (shader) => { 54 | shader.uniforms.uNoiseScale = { value: this.options.scale }; 55 | shader.uniforms.uPatchiness = { value: this.options.patchiness }; 56 | shader.uniforms.uGrassTexture = { value: _grassTexture }; 57 | shader.uniforms.uDirtTexture = { value: _dirtTexture }; 58 | 59 | // Add varyings and uniforms to vertex/fragment shaders 60 | shader.vertexShader = ` 61 | varying vec3 vWorldPosition; 62 | ` + shader.vertexShader; 63 | 64 | shader.fragmentShader = ` 65 | varying vec3 vWorldPosition; 66 | uniform float uNoiseScale; 67 | uniform float uPatchiness; 68 | uniform sampler2D uGrassTexture; 69 | uniform sampler2D uDirtTexture; 70 | ` + shader.fragmentShader; 71 | 72 | shader.vertexShader = shader.vertexShader.replace( 73 | '#include ', 74 | `#include 75 | vWorldPosition = worldPosition.xyz; 76 | ` 77 | ); 78 | 79 | // Add custom shader code for the ground 80 | shader.fragmentShader = shader.fragmentShader.replace( 81 | `void main() {`, 82 | ` 83 | vec3 mod289(vec3 x) { 84 | return x - floor(x * (1.0 / 289.0)) * 289.0; 85 | } 86 | 87 | vec2 mod289(vec2 x) { 88 | return x - floor(x * (1.0 / 289.0)) * 289.0; 89 | } 90 | 91 | vec3 permute(vec3 x) { 92 | return mod289(((x * 34.0) + 1.0) * x); 93 | } 94 | 95 | float simplex2d(vec2 v) { 96 | const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); 97 | vec2 i = floor(v + dot(v, C.yy)); 98 | vec2 x0 = v - i + dot(i, C.xx); 99 | vec2 i1; 100 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 101 | vec4 x12 = x0.xyxy + C.xxzz; 102 | x12.xy -= i1; 103 | 104 | i = mod289(i); 105 | vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); 106 | 107 | vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); 108 | m = m * m; 109 | m = m * m; 110 | 111 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 112 | vec3 h = abs(x) - 0.5; 113 | vec3 ox = floor(x + 0.5); 114 | vec3 a0 = x - ox; 115 | 116 | m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); 117 | 118 | vec3 g; 119 | g.x = a0.x * x0.x + h.x * x0.y; 120 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 121 | return 130.0 * dot(m, g); 122 | } 123 | 124 | void main() {`, 125 | ); 126 | 127 | shader.fragmentShader = shader.fragmentShader.replace( 128 | '#include ', 129 | ` 130 | vec2 uv = vec2(vWorldPosition.x, vWorldPosition.z); 131 | vec3 grassColor = texture2D(uGrassTexture, uv / 30.0).rgb; 132 | vec3 dirtColor = texture2D(uDirtTexture, uv / 30.0).rgb; 133 | 134 | // Generate base noise for the texture 135 | float n = 0.5 + 0.5 * simplex2d(uv / uNoiseScale); 136 | float s = smoothstep(uPatchiness - 0.1 , uPatchiness + 0.1, n); 137 | 138 | // Blend between grass and dirt based on the noise value 139 | vec4 sampledDiffuseColor = vec4(mix(grassColor, dirtColor, s), 1.0); 140 | diffuseColor *= sampledDiffuseColor; 141 | ` 142 | ); 143 | 144 | shader.fragmentShader = shader.fragmentShader.replace( 145 | '#include ', 146 | ` 147 | vec3 mapN = texture2D( normalMap, uv / 30.0 ).xyz * 2.0 - 1.0; 148 | mapN.xy *= normalScale; 149 | 150 | normal = normalize( tbn * mapN ); 151 | ` 152 | ); 153 | 154 | this.material.userData.shader = shader; 155 | }; 156 | 157 | this.geometry = new THREE.PlaneGeometry(2000, 2000); 158 | this.rotation.x = -Math.PI / 2; 159 | this.receiveShadow = true; 160 | }); 161 | } 162 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EZ-Tree 2 | 3 | ![NPM Version](https://img.shields.io/npm/v/%40dgreenheck%2Fez-tree) 4 | ![NPM Downloads](https://img.shields.io/npm/dw/%40dgreenheck%2Fez-tree) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/dgreenheck/ez-tree) 6 | ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/dangreenheck) 7 | ![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCrdx_EU_Wx8_uBfqO0cI-9Q) 8 | 9 |

10 | 11 |

12 | 13 | # About 14 | EZ-Tree is a procedural tree generator with dozens of tunable parameters. The standalone tree generation code is published as a library and can be imported into your own application for dynamically generating trees on demand. Additionally, there is a standalone web app which allows you to create trees within the browser and export as .PNG or .GLB files. 15 | 16 | # App 17 | https://eztree.dev 18 | 19 | # Installation 20 | 21 | ```js 22 | npm i @dgreenheck/ez-tree 23 | ``` 24 | 25 | # Usage 26 | 27 | ```js 28 | // Create new instance 29 | const tree = new Tree(); 30 | 31 | // Set parameters 32 | tree.options.seed = 12345; 33 | tree.options.trunk.length = 20; 34 | tree.options.branch.levels = 3; 35 | 36 | // Generate tree and add to your Three.js scene 37 | tree.generate(); 38 | scene.add(tree); 39 | ``` 40 | 41 | Any time the tree parameters are changed, you must call `generate()` to regenerate the geometry. 42 | 43 | # Running Standalone App Locally 44 | 45 | To run the standalone app locally, you first need to build the EZ-Tree library before running the app. 46 | 47 | ```bash 48 | npm install 49 | npm run app 50 | ``` 51 | 52 | # Running App with Docker 53 | 54 | ```bash 55 | docker compose build 56 | docker compose up -d 57 | ``` 58 | 59 | # Tree Parameters 60 | 61 | The `TreeOptions` class defines an options object that controls various parameters of a procedurally generated tree. Each property of this object allows for customization of the tree's appearance, including bark, branches, and leaves. Below is a detailed explanation of each property of the `TreeOptions` object. 62 | 63 | ## General Properties 64 | 65 | - **`seed`**: Sets the initial value for random generation, ensuring consistent tree generation when using the same seed. 66 | - **`type`**: Defines the type of the tree, which can be set to one of the options from the `TreeType` enumeration (e.g., `TreeType.Deciduous`). 67 | 68 | ## Bark Parameters 69 | 70 | The `bark` object controls the appearance and properties of the tree trunk. 71 | 72 | - **`type`**: Specifies the type of bark texture to use, selected from the `BarkType` enumeration (e.g., `BarkType.Oak`). 73 | - **`tint`**: Determines the color tint applied to the bark, defined as a hexadecimal color value (e.g., `0xffffff` for white). 74 | - **`flatShading`**: Boolean property indicating whether to use flat shading (`true`) or smooth shading (`false`) for the bark. 75 | - **`textured`**: Boolean value that indicates if a texture is applied to the bark (`true` or `false`). 76 | - **`textureScale`**: Controls the scale of the bark texture in both the `x` and `y` axes. It is an object with properties `x` and `y` to define the scaling factors. 77 | 78 | ## Branch Parameters 79 | 80 | The `branch` object defines parameters for the trunk and branch levels of the tree. 81 | 82 | - **`levels`**: Number of recursive branch levels. Setting this to `0` creates only the trunk, while higher values add more branches. 83 | - **`angle`**: Defines the angle, in degrees, at which child branches grow relative to their parent branch. This is specified separately for each level. 84 | - **`children`**: Specifies the number of child branches at each level, with the index (`0`, `1`, `2`, etc.) representing the level. 85 | - **`force`**: Represents an external directional force encouraging tree growth, defined by `direction` (a vector object `{ x, y, z }`) and `strength` (a numeric value). 86 | - **`gnarliness`**: Defines how twisted or curled each branch level should be, specified for each level. 87 | - **`length`**: Length of the branches at each level. This is an object with keys representing each level. 88 | - **`radius`**: Radius (or thickness) of the branches at each level. 89 | - **`sections`**: Number of segments along the length of each branch level, controlling the resolution of the branch mesh. 90 | - **`segments`**: Number of radial segments that make up each branch, with a higher value resulting in a smoother cylinder. 91 | - **`start`**: Specifies where along the parent branch (as a fraction from `0` to `1`) the child branches should start forming. 92 | - **`taper`**: Controls the tapering of the branches at each level. A value between `0` and `1` defines the reduction in radius from base to tip. 93 | - **`twist`**: Defines the amount of twisting applied to each branch level. 94 | 95 | ## Leaf Parameters 96 | 97 | The `leaves` object defines properties that control the appearance and placement of leaves. 98 | 99 | - **`type`**: Specifies the type of leaf texture, selected from the `LeafType` enumeration (e.g., `LeafType.Oak`). 100 | - **`billboard`**: Defines how leaves are rendered. The `Billboard` enumeration can be set to `Single` or `Double` to indicate single or perpendicular double-sided leaves. 101 | - **`angle`**: Defines the angle of the leaves relative to the parent branch, in degrees. 102 | - **`count`**: Number of leaves to generate. 103 | - **`start`**: Specifies where along the length of the branch (as a value between `0` and `1`) leaves should start growing. 104 | - **`size`**: Size of the leaves, represented as a numeric value. 105 | - **`sizeVariance`**: Specifies how much variance in size each leaf instance should have, making the leaves look more natural. 106 | - **`tint`**: Tint color applied to the leaves, defined as a hexadecimal color value (e.g., `0xffffff` for white). 107 | - **`alphaTest`**: Sets the alpha threshold for leaf transparency, controlling the transparency of the leaf textures. 108 | 109 | -------------------------------------------------------------------------------- /src/app/public/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Dumbeldor'; 3 | src: url('/dumbeldor.ttf'), format('ttf'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0; 11 | background-color: #273940; 12 | width: 100%; 13 | height: 100%; 14 | overflow: hidden; 15 | font-family: 'Dumbeldor', sans-serif; 16 | } 17 | 18 | h1 { 19 | font-size: 3em; 20 | text-decoration: underline; 21 | } 22 | 23 | h2 { 24 | font-size: 2em; 25 | } 26 | 27 | h3 { 28 | font-size: 1.5em; 29 | } 30 | 31 | a { 32 | color: white; 33 | font-size: 1.5em; 34 | } 35 | 36 | #root { 37 | width: 100%; 38 | height: 100%; 39 | } 40 | 41 | #app { 42 | height: 100%; 43 | } 44 | 45 | #ui-container { 46 | position: fixed; 47 | top: 8px; 48 | right: 8px; 49 | width: 300px; 50 | } 51 | 52 | /* Tweakpane theme */ 53 | :root { 54 | --tp-base-background-color: hsla(0, 0%, 10%, 0.80); 55 | --tp-base-shadow-color: hsla(0, 0%, 0%, 0.20); 56 | --tp-button-background-color: hsla(0, 0%, 80%, 1.00); 57 | --tp-button-background-color-active: hsla(0, 0%, 100%, 1.00); 58 | --tp-button-background-color-focus: hsla(0, 0%, 95%, 1.00); 59 | --tp-button-background-color-hover: hsla(0, 0%, 85%, 1.00); 60 | --tp-button-foreground-color: hsla(0, 0%, 0%, 0.80); 61 | --tp-container-background-color: hsla(0, 0%, 0%, 0.30); 62 | --tp-container-background-color-active: hsla(0, 0%, 0%, 0.60); 63 | --tp-container-background-color-focus: hsla(0, 0%, 0%, 0.50); 64 | --tp-container-background-color-hover: hsla(0, 0%, 0%, 0.40); 65 | --tp-container-foreground-color: hsla(0, 0%, 100%, 0.50); 66 | --tp-groove-foreground-color: hsla(0, 0%, 0%, 0.20); 67 | --tp-input-background-color: hsla(0, 0%, 0%, 0.30); 68 | --tp-input-background-color-active: hsla(0, 0%, 0%, 0.60); 69 | --tp-input-background-color-focus: hsla(0, 0%, 0%, 0.50); 70 | --tp-input-background-color-hover: hsla(0, 0%, 0%, 0.40); 71 | --tp-input-foreground-color: hsla(0, 0%, 100%, 0.50); 72 | --tp-label-foreground-color: hsla(0, 0%, 100%, 0.50); 73 | --tp-monitor-background-color: hsla(0, 0%, 0%, 0.30); 74 | --tp-monitor-foreground-color: hsla(0, 0%, 100%, 0.30); 75 | } 76 | 77 | .about-icon { 78 | position: fixed; 79 | top: 18px; 80 | left: 18px; 81 | width: 40px; 82 | height: 34px; 83 | cursor: pointer; 84 | } 85 | 86 | .audio-icon { 87 | display: none; 88 | position: fixed; 89 | top: 16px; 90 | left: 80px; 91 | width: 40px; 92 | height: 40px; 93 | cursor: pointer; 94 | } 95 | 96 | #loading-screen { 97 | background-image: url("/background.webp"); 98 | width: 100%; 99 | height: 100%; 100 | position: fixed; 101 | z-index: 9999; 102 | background-repeat: no-repeat; 103 | background-size: cover; 104 | background-position: center; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | flex-direction: column; 109 | padding: 64px; 110 | box-sizing: border-box; 111 | } 112 | 113 | #logo { 114 | position: relative; 115 | margin: 64px; 116 | width: 100%; 117 | max-width: 600px; 118 | aspect-ratio: 1 / 1; 119 | clip-path: inset(100% 0 0 0); 120 | will-change: clip-path; 121 | } 122 | 123 | #loading-text { 124 | font-size: 5em; 125 | color: white; 126 | font-family: 'Dumbeldor', serif; 127 | } 128 | 129 | /* Overlay */ 130 | 131 | /* Overlay */ 132 | .overlay { 133 | position: fixed; 134 | top: 0; 135 | left: 0; 136 | width: 100%; 137 | height: 100%; 138 | background: rgba(0, 0, 0, 0.85); 139 | display: flex; 140 | align-items: center; 141 | justify-content: center; 142 | opacity: 0; 143 | pointer-events: none; 144 | transition: opacity 0.4s ease; 145 | } 146 | 147 | .overlay.active { 148 | opacity: 1; 149 | pointer-events: auto; 150 | } 151 | 152 | /* Dialog */ 153 | .about-dialog { 154 | background: linear-gradient(135deg, #1e1e1e, #444); 155 | color: #fff; 156 | border-radius: 32px; 157 | width: 90%; 158 | max-width: 500px; 159 | padding: 32px; 160 | margin: 16px; 161 | text-align: center; 162 | position: relative; 163 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); 164 | border: 4px solid white; 165 | } 166 | 167 | .about-dialog img { 168 | width: 128px; 169 | height: 128px; 170 | border-radius: 16px; 171 | } 172 | 173 | /* Social Icons */ 174 | .social-icons { 175 | display: flex; 176 | justify-content: center; 177 | gap: 16px; 178 | margin-top: 32px; 179 | } 180 | 181 | .social-icons a { 182 | color: #fff; 183 | font-size: 24px; 184 | transition: color 0.3s; 185 | } 186 | 187 | .social-icons a:hover { 188 | color: #00acee; 189 | } 190 | 191 | /* Close Button */ 192 | .close-button { 193 | margin-top: 24px; 194 | padding: 12px 24px; 195 | border: none; 196 | background: #555; 197 | color: #fff; 198 | border-radius: 8px; 199 | cursor: pointer; 200 | transition: background 0.3s; 201 | } 202 | 203 | .close-button:hover { 204 | background: #777; 205 | } 206 | 207 | @media (max-width: 800px) { 208 | #root { 209 | display: flex; 210 | flex-direction: column; 211 | } 212 | 213 | #app { 214 | height: 50vh; 215 | } 216 | 217 | #ui-container { 218 | position: relative; 219 | width: 100%; 220 | overflow-y: auto; 221 | overscroll-behavior: none; 222 | top: auto; 223 | right: auto; 224 | zoom: 1.3; 225 | } 226 | 227 | .about-icon { 228 | position: fixed; 229 | top: 12px; 230 | left: 8px; 231 | width: 30px; 232 | height: 26px; 233 | cursor: pointer; 234 | } 235 | 236 | .audio-icon { 237 | display: none; 238 | position: fixed; 239 | top: 12px; 240 | left: 46px; 241 | width: 30px; 242 | height: 30px; 243 | cursor: pointer; 244 | } 245 | } 246 | 247 | @media (max-width: 675px) { 248 | #loading-text { 249 | font-size: 4em; 250 | } 251 | } 252 | 253 | @media (max-width: 575px) { 254 | #loading-text { 255 | font-size: 3em; 256 | } 257 | } 258 | 259 | @media (max-width: 475px) { 260 | #loading-text { 261 | font-size: 2em; 262 | } 263 | } -------------------------------------------------------------------------------- /src/app/grass.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GLTFLoader } from 'three/examples/jsm/Addons.js'; 3 | import { simplex2d } from './noise'; 4 | 5 | let loaded = false; 6 | let _grassMesh = null; 7 | let _blueFlower = null; 8 | let _whiteFlower = null; 9 | let _yellowFlower = null; 10 | 11 | export class GrassOptions { 12 | /** 13 | * Number of grass instances 14 | */ 15 | instanceCount = 5000; 16 | 17 | /** 18 | * Maximum number of grass instances 19 | */ 20 | maxInstanceCount = 25000; 21 | 22 | /** 23 | * Number of flowers to generate (per color) 24 | */ 25 | flowerCount = 50; 26 | 27 | /** 28 | * Size of the grass patches 29 | */ 30 | scale = 100; 31 | 32 | /** 33 | * Patchiness of the grass 34 | */ 35 | patchiness = 0.7; 36 | 37 | /** 38 | * Scale factor for the grass model 39 | */ 40 | size = { x: 5, y: 4, z: 5 }; 41 | 42 | /** 43 | * Maximum variation in the grass size 44 | */ 45 | sizeVariation = { x: 1, y: 2, z: 1 }; 46 | 47 | /** 48 | * Strength of wind along each axis 49 | */ 50 | windStrength = { x: 0.3, y: 0, z: 0.3 }; 51 | 52 | /** 53 | * Oscillation frequency for wind movement 54 | */ 55 | windFrequency = 1.0; 56 | 57 | /** 58 | * Controls how localized wind effects are 59 | */ 60 | windScale = 400.0; 61 | } 62 | 63 | export class Grass extends THREE.Object3D { 64 | constructor(options = new GrassOptions()) { 65 | super(); 66 | 67 | /** 68 | * @type {GrassOptions} 69 | */ 70 | this.options = options; 71 | 72 | this.flowers = new THREE.Group(); 73 | this.add(this.flowers); 74 | 75 | this.fetchAssets().then(() => { 76 | this.generateGrass(); 77 | this.generateFlowers(_whiteFlower); 78 | this.generateFlowers(_blueFlower); 79 | this.generateFlowers(_yellowFlower); 80 | }); 81 | } 82 | 83 | get instanceCount() { 84 | return this.grassMesh?.count ?? this.options.instanceCount; 85 | } 86 | 87 | set instanceCount(value) { 88 | this.grassMesh.count = value; 89 | } 90 | 91 | /** 92 | * 93 | * @returns {Promise} 94 | */ 95 | async fetchAssets() { 96 | if (loaded) return; 97 | 98 | const gltfLoader = new GLTFLoader(); 99 | 100 | _grassMesh = (await gltfLoader.loadAsync('grass.glb')).scene.children[0]; 101 | _whiteFlower = (await gltfLoader.loadAsync('flower_white.glb')).scene.children[0]; 102 | _blueFlower = (await gltfLoader.loadAsync('flower_blue.glb')).scene.children[0]; 103 | _yellowFlower = (await gltfLoader.loadAsync('flower_yellow.glb')).scene.children[0]; 104 | 105 | // The flower is composed of multiple meshes with different materials. Append the 106 | // wind shader code to each material 107 | [_whiteFlower, _blueFlower, _yellowFlower].forEach((mesh) => { 108 | mesh.traverse((o) => { 109 | if (o.isMesh && o.material) { 110 | if (o.material.map) { 111 | o.material = new THREE.MeshPhongMaterial({ map: o.material.map }); 112 | } 113 | this.appendWindShader(o.material); 114 | } 115 | }); 116 | }); 117 | 118 | loaded = true; 119 | } 120 | 121 | update(elapsedTime) { 122 | this.traverse((o) => { 123 | if (o.isMesh && o.material?.userData.shader) { 124 | o.material.userData.shader.uniforms.uTime.value = elapsedTime; 125 | } 126 | }); 127 | } 128 | 129 | generateGrass() { 130 | const grassMaterial = new THREE.MeshPhongMaterial({ 131 | map: _grassMesh.material.map, 132 | // Add some emission so grass has some color when not lit 133 | emissive: new THREE.Color(0x308040), 134 | emissiveIntensity: 0.05, 135 | transparent: false, 136 | alphaTest: 0.5, 137 | depthTest: true, 138 | depthWrite: true, 139 | side: THREE.DoubleSide 140 | }); 141 | 142 | this.appendWindShader(grassMaterial, true); 143 | 144 | // Decrease grass brightness 145 | grassMaterial.color.multiplyScalar(0.6); 146 | 147 | this.grassMesh = new THREE.InstancedMesh( 148 | _grassMesh.geometry, 149 | grassMaterial, 150 | this.options.maxInstanceCount); 151 | 152 | this.generateGrassInstances(); 153 | 154 | this.add(this.grassMesh); 155 | } 156 | 157 | generateGrassInstances() { 158 | const dummy = new THREE.Object3D(); 159 | 160 | let count = 0; 161 | for (let i = 0; i < this.options.maxInstanceCount; i++) { 162 | const r = 10 + Math.random() * 500; 163 | const theta = Math.random() * 2.0 * Math.PI; 164 | 165 | // Set position randomly 166 | const p = new THREE.Vector3( 167 | r * Math.cos(theta), 168 | 0, 169 | r * Math.sin(theta) 170 | ); 171 | 172 | const n = 0.5 + 0.5 * simplex2d(new THREE.Vector2( 173 | p.x / this.options.scale, 174 | p.z / this.options.scale 175 | )); 176 | 177 | if (n > this.options.patchiness && Math.random() + 0.6 > this.options.patchiness) { continue; } 178 | 179 | dummy.position.copy(p); 180 | 181 | // Set rotation randomly 182 | dummy.rotation.set( 183 | 0, 184 | 2 * Math.PI * Math.random(), 185 | 0 186 | ); 187 | 188 | // Set scale randomly 189 | dummy.scale.set( 190 | this.options.sizeVariation.x * Math.random() + this.options.size.x, 191 | this.options.sizeVariation.y * Math.random() + this.options.size.y, 192 | this.options.sizeVariation.z * Math.random() + this.options.size.z 193 | ); 194 | 195 | // Apply the transformation to the instance 196 | dummy.updateMatrix(); 197 | 198 | const color = new THREE.Color( 199 | 0.25 + Math.random() * 0.1, 200 | 0.3 + Math.random() * 0.3, 201 | 0.1); 202 | 203 | this.grassMesh.setMatrixAt(count, dummy.matrix); 204 | this.grassMesh.setColorAt(count, color); 205 | count++; 206 | } 207 | 208 | // Set count to only show up to `instanceCount` instances 209 | this.grassMesh.count = this.options.instanceCount; 210 | 211 | this.grassMesh.receiveShadow = true; 212 | this.grassMesh.castShadow = true; 213 | 214 | // Ensure the transformation is updated in the GPU 215 | this.grassMesh.instanceMatrix.needsUpdate = true; 216 | this.grassMesh.instanceColor.needsUpdate = true; 217 | } 218 | 219 | /** 220 | * 221 | * @param {THREE.Mesh} flowerMesh 222 | */ 223 | generateFlowers(flowerMesh) { 224 | for (let i = 0; i < this.options.flowerCount; i++) { 225 | const r = 10 + Math.random() * 200; 226 | const theta = Math.random() * 2.0 * Math.PI; 227 | 228 | // Set position randomly 229 | const p = new THREE.Vector3( 230 | r * Math.cos(theta), 231 | 0, 232 | r * Math.sin(theta) 233 | ); 234 | 235 | const n = 0.5 + 0.5 * simplex2d(new THREE.Vector2( 236 | p.x / this.options.scale, 237 | p.z / this.options.scale 238 | )); 239 | 240 | if (n > this.options.patchiness && Math.random() + 0.8 > this.options.patchiness) { continue; } 241 | 242 | const flower = flowerMesh.clone(); 243 | flower.position.copy(p); 244 | flower.rotation.set(0, 2 * Math.PI * Math.random(), 0); 245 | const scale = 0.02 + 0.03 * Math.random(); 246 | flower.scale.set(scale, scale, scale); 247 | 248 | this.flowers.add(flower); 249 | } 250 | } 251 | 252 | /** 253 | * 254 | * @param {THREE.Material} material 255 | */ 256 | appendWindShader(material, instanced = false) { 257 | material.onBeforeCompile = (shader) => { 258 | shader.uniforms.uTime = { value: 0 }; 259 | shader.uniforms.uWindStrength = { value: this.options.windStrength }; 260 | shader.uniforms.uWindFrequency = { value: this.options.windFrequency }; 261 | shader.uniforms.uWindScale = { value: this.options.windScale }; 262 | 263 | shader.vertexShader = ` 264 | uniform float uTime; 265 | uniform vec3 uWindStrength; 266 | uniform float uWindFrequency; 267 | uniform float uWindScale; 268 | ` + shader.vertexShader; 269 | 270 | // Add code for simplex noise 271 | shader.vertexShader = shader.vertexShader.replace( 272 | `void main() {`, 273 | ` 274 | vec3 mod289(vec3 x) { 275 | return x - floor(x * (1.0 / 289.0)) * 289.0; 276 | } 277 | 278 | vec2 mod289(vec2 x) { 279 | return x - floor(x * (1.0 / 289.0)) * 289.0; 280 | } 281 | 282 | vec3 permute(vec3 x) { 283 | return mod289(((x * 34.0) + 1.0) * x); 284 | } 285 | 286 | float simplex2d(vec2 v) { 287 | const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439); 288 | vec2 i = floor(v + dot(v, C.yy)); 289 | vec2 x0 = v - i + dot(i, C.xx); 290 | vec2 i1; 291 | i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); 292 | vec4 x12 = x0.xyxy + C.xxzz; 293 | x12.xy -= i1; 294 | 295 | i = mod289(i); 296 | vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); 297 | 298 | vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); 299 | m = m * m; 300 | m = m * m; 301 | 302 | vec3 x = 2.0 * fract(p * C.www) - 1.0; 303 | vec3 h = abs(x) - 0.5; 304 | vec3 ox = floor(x + 0.5); 305 | vec3 a0 = x - ox; 306 | 307 | m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); 308 | 309 | vec3 g; 310 | g.x = a0.x * x0.x + h.x * x0.y; 311 | g.yz = a0.yz * x12.xz + h.yz * x12.yw; 312 | return 130.0 * dot(m, g); 313 | } 314 | 315 | void main() {`, 316 | ); 317 | 318 | // To make code reusable for grass and flowers, conditionally multiply by instanceMatrix 319 | let vertexShader = instanced ? 320 | ` 321 | vec4 mvPosition = instanceMatrix * vec4(transformed, 1.0); 322 | float windOffset = 2.0 * 3.14 * simplex2d((modelMatrix * mvPosition).xz / uWindScale); 323 | vec3 windSway = position.y * uWindStrength * 324 | sin(uTime * uWindFrequency + windOffset) * 325 | cos(uTime * 1.4 * uWindFrequency + windOffset); 326 | 327 | mvPosition.xyz += windSway; 328 | mvPosition = modelViewMatrix * mvPosition; 329 | 330 | gl_Position = projectionMatrix * mvPosition; 331 | ` : 332 | ` 333 | vec4 mvPosition = vec4(transformed, 1.0); 334 | float windOffset = 2.0 * 3.14 * simplex2d((modelMatrix * mvPosition).xz / uWindScale); 335 | vec3 windSway = 0.2 * position.y * uWindStrength * 336 | sin(uTime * uWindFrequency + windOffset) * 337 | cos(uTime * 1.4 * uWindFrequency + windOffset); 338 | 339 | mvPosition.xyz += windSway; 340 | mvPosition = modelViewMatrix * mvPosition; 341 | 342 | gl_Position = projectionMatrix * mvPosition; 343 | `; 344 | 345 | // worldPosition = modelMatrix * instanceMatrix * position; 346 | // worldWindDirection = model 347 | shader.vertexShader = shader.vertexShader.replace( 348 | `#include `, 349 | vertexShader 350 | ); 351 | 352 | material.userData.shader = shader; 353 | }; 354 | } 355 | } -------------------------------------------------------------------------------- /src/app/ui.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; 3 | import { Pane } from 'tweakpane'; 4 | import { BarkType, Billboard, LeafType, TreePreset, Tree, TreeType } from '@dgreenheck/ez-tree'; 5 | import { Environment } from './environment'; 6 | import { OrbitControls } from 'three/examples/jsm/Addons.js'; 7 | import { version } from '../../package.json'; 8 | 9 | const exporter = new GLTFExporter(); 10 | let pane = null; 11 | 12 | /** 13 | * Setups the UI 14 | * @param {Tree} tree 15 | * @param {Environment} environment 16 | * @param {THREE.WebGLRenderer} renderer 17 | * @param {THREE.Scene} scene 18 | * @param {THREE.Camera} camera 19 | * @param {OrbitControls} controls 20 | * @param {String} initialPreset 21 | */ 22 | export function setupUI(tree, environment, renderer, scene, camera, controls, initialPreset) { 23 | 24 | // Remove old event listener and dispose old pane 25 | pane?.off('change'); 26 | pane?.dispose(); 27 | 28 | pane = new Pane({ container: document.getElementById('ui-container'), title: 'EZ Tree' }); 29 | 30 | const onChange = () => { 31 | tree.generate(); 32 | tree.traverse((o) => { 33 | if (o.material) { 34 | o.material.needsUpdate = true; 35 | } 36 | }); 37 | }; 38 | 39 | 40 | const tab = pane.addTab({ 41 | pages: [ 42 | { title: 'Parameters' }, 43 | { title: 'Import/Export' } 44 | ] 45 | }); 46 | 47 | const treeFolder = tab.pages[0].addFolder({ title: 'Tree', expanded: true }); 48 | 49 | // Update tree and material on change 50 | treeFolder.on('change', onChange); 51 | 52 | // Preset dropdown 53 | treeFolder.addBlade({ 54 | view: 'list', 55 | label: 'preset', 56 | options: Object.keys(TreePreset).map(p => ({ text: p, value: p })), 57 | value: initialPreset 58 | }).on('change', (e) => { 59 | tree.loadPreset(e.value) 60 | pane.refresh(); 61 | }); 62 | 63 | treeFolder.addBinding(tree.options, 'seed', { min: 0, max: 65536, step: 1 }); 64 | 65 | // Bark folder 66 | const barkFolder = treeFolder.addFolder({ title: 'Bark', expanded: false }); 67 | barkFolder.addBinding(tree.options.bark, 'type', { options: BarkType }); 68 | barkFolder.addBinding(tree.options.bark, 'tint', { view: 'color' }); 69 | barkFolder.addBinding(tree.options.bark, 'flatShading'); 70 | barkFolder.addBinding(tree.options.bark, 'textured'); 71 | barkFolder.addBinding(tree.options.bark.textureScale, 'x', { min: 0.5, max: 5 }); 72 | barkFolder.addBinding(tree.options.bark.textureScale, 'y', { min: 0.5, max: 5 }); 73 | 74 | // Branch folder 75 | const branchFolder = treeFolder.addFolder({ title: 'Branches', expanded: false }); 76 | 77 | branchFolder.addBinding(tree.options, 'type', { options: TreeType }); 78 | branchFolder.addBinding(tree.options.branch, 'levels', { min: 0, max: 3, step: 1 }); 79 | 80 | const branchAngleFolder = branchFolder.addFolder({ title: 'Angle', expanded: false }); 81 | branchAngleFolder.addBinding(tree.options.branch.angle, '1', { min: 0, max: 180 }); 82 | branchAngleFolder.addBinding(tree.options.branch.angle, '2', { min: 0, max: 180 }); 83 | branchAngleFolder.addBinding(tree.options.branch.angle, '3', { min: 0, max: 180 }); 84 | 85 | const childrenFolder = branchFolder.addFolder({ title: 'Children', expanded: false }); 86 | childrenFolder.addBinding(tree.options.branch.children, '0', { min: 0, max: 100, step: 1 }); 87 | childrenFolder.addBinding(tree.options.branch.children, '1', { min: 0, max: 10, step: 1 }); 88 | childrenFolder.addBinding(tree.options.branch.children, '2', { min: 0, max: 5, step: 1 }); 89 | 90 | const gnarlinessFolder = branchFolder.addFolder({ title: 'Gnarliness', expanded: false }); 91 | gnarlinessFolder.addBinding(tree.options.branch.gnarliness, '0', { min: -0.5, max: 0.5 }); 92 | gnarlinessFolder.addBinding(tree.options.branch.gnarliness, '1', { min: -0.5, max: 0.5 }); 93 | gnarlinessFolder.addBinding(tree.options.branch.gnarliness, '2', { min: -0.5, max: 0.5 }); 94 | gnarlinessFolder.addBinding(tree.options.branch.gnarliness, '3', { min: -0.5, max: 0.5 }); 95 | 96 | const forceFolder = branchFolder.addFolder({ title: 'Growth Direction', expanded: false }); 97 | forceFolder.addBinding(tree.options.branch.force.direction, 'x', { min: -1, max: 1 }); 98 | forceFolder.addBinding(tree.options.branch.force.direction, 'y', { min: -1, max: 1 }); 99 | forceFolder.addBinding(tree.options.branch.force.direction, 'z', { min: -1, max: 1 }); 100 | forceFolder.addBinding(tree.options.branch.force, 'strength', { min: -0.1, max: 0.1, step: 0.001 }); 101 | 102 | const lengthFolder = branchFolder.addFolder({ title: 'Length', expanded: false }); 103 | lengthFolder.addBinding(tree.options.branch.length, '0', { min: 0.1, max: 100 }); 104 | lengthFolder.addBinding(tree.options.branch.length, '1', { min: 0.1, max: 100 }); 105 | lengthFolder.addBinding(tree.options.branch.length, '2', { min: 0.1, max: 100 }); 106 | lengthFolder.addBinding(tree.options.branch.length, '3', { min: 0.1, max: 100 }); 107 | 108 | const branchRadiusFolder = branchFolder.addFolder({ title: 'Radius', expanded: false }); 109 | branchRadiusFolder.addBinding(tree.options.branch.radius, '0', { min: 0.1, max: 5 }); 110 | branchRadiusFolder.addBinding(tree.options.branch.radius, '1', { min: 0.1, max: 5 }); 111 | branchRadiusFolder.addBinding(tree.options.branch.radius, '2', { min: 0.1, max: 5 }); 112 | branchRadiusFolder.addBinding(tree.options.branch.radius, '3', { min: 0.1, max: 5 }); 113 | 114 | const sectionsFolder = branchFolder.addFolder({ title: 'Sections', expanded: false }); 115 | sectionsFolder.addBinding(tree.options.branch.sections, '0', { min: 1, max: 20, step: 1 }); 116 | sectionsFolder.addBinding(tree.options.branch.sections, '1', { min: 1, max: 20, step: 1 }); 117 | sectionsFolder.addBinding(tree.options.branch.sections, '2', { min: 1, max: 20, step: 1 }); 118 | sectionsFolder.addBinding(tree.options.branch.sections, '3', { min: 1, max: 20, step: 1 }); 119 | 120 | const segmentsFolder = branchFolder.addFolder({ title: 'Segments', expanded: false }); 121 | segmentsFolder.addBinding(tree.options.branch.segments, '0', { min: 3, max: 16, step: 1 }); 122 | segmentsFolder.addBinding(tree.options.branch.segments, '1', { min: 3, max: 16, step: 1 }); 123 | segmentsFolder.addBinding(tree.options.branch.segments, '2', { min: 3, max: 16, step: 1 }); 124 | segmentsFolder.addBinding(tree.options.branch.segments, '3', { min: 3, max: 16, step: 1 }); 125 | 126 | const branchStartFolder = branchFolder.addFolder({ title: 'Start', expanded: false }); 127 | branchStartFolder.addBinding(tree.options.branch.start, '1', { min: 0, max: 1 }); 128 | branchStartFolder.addBinding(tree.options.branch.start, '2', { min: 0, max: 1 }); 129 | branchStartFolder.addBinding(tree.options.branch.start, '3', { min: 0, max: 1 }); 130 | 131 | const taperFolder = branchFolder.addFolder({ title: 'Taper', expanded: false }); 132 | taperFolder.addBinding(tree.options.branch.taper, '0', { min: 0, max: 1 }); 133 | taperFolder.addBinding(tree.options.branch.taper, '1', { min: 0, max: 1 }); 134 | taperFolder.addBinding(tree.options.branch.taper, '2', { min: 0, max: 1 }); 135 | taperFolder.addBinding(tree.options.branch.taper, '3', { min: 0, max: 1 }); 136 | 137 | const twistFolder = branchFolder.addFolder({ title: 'Twist', expanded: false }); 138 | twistFolder.addBinding(tree.options.branch.twist, '0', { min: -0.5, max: 0.5 }); 139 | twistFolder.addBinding(tree.options.branch.twist, '1', { min: -0.5, max: 0.5 }); 140 | twistFolder.addBinding(tree.options.branch.twist, '2', { min: -0.5, max: 0.5 }); 141 | twistFolder.addBinding(tree.options.branch.twist, '3', { min: -0.5, max: 0.5 }); 142 | 143 | const leavesFolder = treeFolder.addFolder({ title: 'Leaves', expanded: false }); 144 | leavesFolder.addBinding(tree.options.leaves, 'type', { options: LeafType }); 145 | leavesFolder.addBinding(tree.options.leaves, 'tint', { view: 'color' }); 146 | leavesFolder.addBinding(tree.options.leaves, 'billboard', { options: Billboard }); 147 | leavesFolder.addBinding(tree.options.leaves, 'angle', { min: 0, max: 100, step: 1 }); 148 | leavesFolder.addBinding(tree.options.leaves, 'count', { min: 0, max: 100, step: 1 }); 149 | leavesFolder.addBinding(tree.options.leaves, 'start', { min: 0, max: 1 }); 150 | leavesFolder.addBinding(tree.options.leaves, 'size', { min: 0, max: 10 }); 151 | leavesFolder.addBinding(tree.options.leaves, 'sizeVariance', { min: 0, max: 1 }); 152 | leavesFolder.addBinding(tree.options.leaves, 'alphaTest', { min: 0, max: 1 }); 153 | 154 | /** CAMERA */ 155 | const cameraFolder = tab.pages[0].addFolder({ title: 'Camera', expanded: false }); 156 | cameraFolder.addBinding(controls, 'autoRotate'); 157 | cameraFolder.addBinding(controls, 'autoRotateSpeed', { min: 0, max: 2 }); 158 | 159 | /** ENVIRONMENT */ 160 | 161 | const environmentFolder = tab.pages[0].addFolder({ title: 'Environment', expanded: false }); 162 | environmentFolder.addBinding(environment.skybox, 'sunAzimuth', { label: 'sunAngle', min: 0, max: 360 }); 163 | environmentFolder.addBinding(environment.grass, 'instanceCount', { label: 'grassCount', min: 0, max: 25000, step: 1 }); 164 | 165 | /** INFO */ 166 | 167 | const infoFolder = tab.pages[0].addFolder({ title: 'Info', expanded: false }); 168 | 169 | infoFolder.addBinding(tree, 'vertexCount', { 170 | label: 'vertices', 171 | format: (v) => v.toFixed(0), 172 | readonly: true, 173 | }); 174 | 175 | infoFolder.addBinding(tree, 'triangleCount', { 176 | label: 'triangles', 177 | format: (v) => v.toFixed(0), 178 | readonly: true, 179 | }); 180 | 181 | infoFolder.addBlade({ 182 | view: 'text', 183 | label: 'version', 184 | parse: (v) => String(v), 185 | value: version, 186 | }); 187 | 188 | /** Export **/ 189 | 190 | tab.pages[1].addButton({ title: 'Save Preset' }).on('click', () => { 191 | const link = document.getElementById('downloadLink'); 192 | const json = JSON.stringify(tree.options, null, 2); 193 | const blob = new Blob([json], { type: 'application/json' }); 194 | link.href = URL.createObjectURL(blob); 195 | link.download = 'tree.json'; 196 | link.click(); 197 | }); 198 | 199 | tab.pages[1].addButton({ title: 'Load Preset' }).on('click', () => { 200 | document.getElementById('fileInput').click(); 201 | }); 202 | 203 | tab.pages[1].addButton({ title: 'Export GLB' }).on('click', () => { 204 | exporter.parse( 205 | tree, 206 | (glb) => { 207 | const blob = new Blob([glb], { type: 'application/octet-stream' }); 208 | const url = window.URL.createObjectURL(blob); 209 | const link = document.getElementById('downloadLink'); 210 | link.href = url; 211 | link.download = 'tree.glb'; 212 | link.click(); 213 | }, 214 | (err) => { 215 | console.error(err); 216 | }, 217 | { binary: true } 218 | ); 219 | }); 220 | 221 | tab.pages[1].addButton({ title: 'Export PNG' }).on('click', () => { 222 | renderer.setClearColor(0, 0); // Set background to transparent 223 | 224 | // Disable fog 225 | const fog = scene.fog; 226 | scene.fog = null; 227 | 228 | // Hide all objects in the scene except for the tree 229 | scene.traverse((o) => { 230 | if (o.name === 'Skybox') { 231 | // Temporarily flip the skybox so it doesn't render 232 | o.material.side = THREE.FrontSide; 233 | } else if (o.isMesh) { 234 | o.visible = false 235 | } 236 | }); 237 | tree.traverse((o) => o.visible = true); 238 | 239 | // Render the scene to texture 240 | renderer.render(scene, camera); 241 | 242 | const link = document.getElementById('downloadLink'); 243 | link.href = renderer.domElement.toDataURL('image/png'); 244 | link.download = 'tree.png'; 245 | link.click(); 246 | 247 | // Restore defaults 248 | renderer.setClearColor(0); 249 | scene.fog = fog; 250 | scene.traverse((o) => { 251 | if (o.name === 'Skybox') { 252 | o.material.side = THREE.BackSide; 253 | } 254 | o.visible = true; 255 | }); 256 | }); 257 | 258 | // Read tree parameters from JSON 259 | document 260 | .getElementById('fileInput') 261 | .addEventListener('change', function (event) { 262 | const file = event.target.files[0]; 263 | if (file) { 264 | const reader = new FileReader(); 265 | reader.onload = function (e) { 266 | try { 267 | tree.options = JSON.parse(e.target.result); 268 | tree.generate(); 269 | setupUI(tree, environment, renderer, scene, camera, controls, initialPreset) 270 | } catch (error) { 271 | console.error('Error parsing JSON:', error); 272 | } 273 | }; 274 | reader.onerror = function (e) { 275 | console.error('Error reading file:', e); 276 | }; 277 | reader.readAsText(file); 278 | } 279 | }); 280 | } 281 | -------------------------------------------------------------------------------- /src/lib/tree.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import RNG from './rng'; 3 | import { Branch } from './branch'; 4 | import { Billboard, TreeType } from './enums'; 5 | import TreeOptions from './options'; 6 | import { loadPreset } from './presets/index'; 7 | import { getBarkTexture, getLeafTexture } from './textures'; 8 | 9 | export class Tree extends THREE.Group { 10 | /** 11 | * @type {RNG} 12 | */ 13 | rng; 14 | 15 | /** 16 | * @type {TreeOptions} 17 | */ 18 | options; 19 | 20 | /** 21 | * @type {Branch[]} 22 | */ 23 | branchQueue = []; 24 | 25 | /** 26 | * @param {TreeOptions} params 27 | */ 28 | constructor(options = new TreeOptions()) { 29 | super(); 30 | this.name = 'Tree'; 31 | this.branchesMesh = new THREE.Mesh(); 32 | this.leavesMesh = new THREE.Mesh(); 33 | this.add(this.branchesMesh); 34 | this.add(this.leavesMesh); 35 | this.options = options; 36 | } 37 | 38 | update(elapsedTime) { 39 | const leafShader = this.leavesMesh.material.userData.shader; 40 | if (leafShader) { 41 | leafShader.uniforms.uTime.value = elapsedTime; 42 | } 43 | } 44 | 45 | /** 46 | * Loads a preset tree from JSON 47 | * @param {string} preset 48 | */ 49 | loadPreset(name) { 50 | const json = loadPreset(name); 51 | this.loadFromJson(json); 52 | } 53 | 54 | /** 55 | * Loads a tree from JSON 56 | * @param {TreeOptions} json 57 | */ 58 | loadFromJson(json) { 59 | this.options.copy(json); 60 | this.generate(); 61 | } 62 | 63 | /** 64 | * Generate a new tree 65 | */ 66 | generate() { 67 | // Clean up old geometry 68 | this.branches = { 69 | verts: [], 70 | normals: [], 71 | indices: [], 72 | uvs: [], 73 | windFactor: [] 74 | }; 75 | 76 | this.leaves = { 77 | verts: [], 78 | normals: [], 79 | indices: [], 80 | uvs: [], 81 | }; 82 | 83 | this.rng = new RNG(this.options.seed); 84 | 85 | // Create the trunk of the tree first 86 | this.branchQueue.push( 87 | new Branch( 88 | new THREE.Vector3(), 89 | new THREE.Euler(), 90 | this.options.branch.length[0], 91 | this.options.branch.radius[0], 92 | 0, 93 | this.options.branch.sections[0], 94 | this.options.branch.segments[0], 95 | ), 96 | ); 97 | 98 | while (this.branchQueue.length > 0) { 99 | const branch = this.branchQueue.shift(); 100 | this.generateBranch(branch); 101 | } 102 | 103 | this.createBranchesGeometry(); 104 | this.createLeavesGeometry(); 105 | } 106 | 107 | /** 108 | * Generates a new branch 109 | * @param {Branch} branch 110 | * @returns 111 | */ 112 | generateBranch(branch) { 113 | // Used later for geometry index generation 114 | const indexOffset = this.branches.verts.length / 3; 115 | 116 | let sectionOrientation = branch.orientation.clone(); 117 | let sectionOrigin = branch.origin.clone(); 118 | let sectionLength = 119 | branch.length / 120 | branch.sectionCount / 121 | (this.options.type === 'Deciduous' ? this.options.branch.levels - 1 : 1); 122 | 123 | // This information is used for generating child branches after the branch 124 | // geometry has been constructed 125 | let sections = []; 126 | 127 | for (let i = 0; i <= branch.sectionCount; i++) { 128 | let sectionRadius = branch.radius; 129 | 130 | // If final section of final level, set radius to effecively zero 131 | if ( 132 | i === branch.sectionCount && 133 | branch.level === this.options.branch.levels 134 | ) { 135 | sectionRadius = 0.001; 136 | } else if (this.options.type === TreeType.Deciduous) { 137 | sectionRadius *= 138 | 1 - this.options.branch.taper[branch.level] * (i / branch.sectionCount); 139 | } else if (this.options.type === TreeType.Evergreen) { 140 | // Evergreens do not have a terminal branch so they have a taper of 1 141 | sectionRadius *= 1 - (i / branch.sectionCount); 142 | } 143 | 144 | // Create the segments that make up this section. 145 | let first; 146 | for (let j = 0; j < branch.segmentCount; j++) { 147 | let angle = (2.0 * Math.PI * j) / branch.segmentCount; 148 | 149 | // Create the segment vertex 150 | const vertex = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle)) 151 | .multiplyScalar(sectionRadius) 152 | .applyEuler(sectionOrientation) 153 | .add(sectionOrigin); 154 | 155 | const normal = new THREE.Vector3(Math.cos(angle), 0, Math.sin(angle)) 156 | .applyEuler(sectionOrientation) 157 | .normalize(); 158 | 159 | const uv = new THREE.Vector2( 160 | j / branch.segmentCount, 161 | (i % 2 === 0) ? 0 : 1, 162 | ); 163 | 164 | this.branches.verts.push(...Object.values(vertex)); 165 | this.branches.normals.push(...Object.values(normal)); 166 | this.branches.uvs.push(...Object.values(uv)); 167 | 168 | if (j === 0) { 169 | first = { vertex, normal, uv }; 170 | } 171 | } 172 | 173 | // Duplicate the first vertex so there is continuity in the UV mapping 174 | this.branches.verts.push(...Object.values(first.vertex)); 175 | this.branches.normals.push(...Object.values(first.normal)); 176 | this.branches.uvs.push(1, first.uv.y); 177 | 178 | // Use this information later on when generating child branches 179 | sections.push({ 180 | origin: sectionOrigin.clone(), 181 | orientation: sectionOrientation.clone(), 182 | radius: sectionRadius, 183 | }); 184 | 185 | sectionOrigin.add( 186 | new THREE.Vector3(0, sectionLength, 0).applyEuler(sectionOrientation), 187 | ); 188 | 189 | // Perturb the orientation of the next section randomly. The higher the 190 | // gnarliness, the larger potential perturbation 191 | const gnarliness = 192 | Math.max(1, 1 / Math.sqrt(sectionRadius)) * 193 | this.options.branch.gnarliness[branch.level]; 194 | 195 | sectionOrientation.x += this.rng.random(gnarliness, -gnarliness); 196 | sectionOrientation.z += this.rng.random(gnarliness, -gnarliness); 197 | 198 | // Apply growth force to the branch 199 | const qSection = new THREE.Quaternion().setFromEuler(sectionOrientation); 200 | 201 | const qTwist = new THREE.Quaternion().setFromAxisAngle( 202 | new THREE.Vector3(0, 1, 0), 203 | this.options.branch.twist[branch.level], 204 | ); 205 | 206 | const qForce = new THREE.Quaternion().setFromUnitVectors( 207 | new THREE.Vector3(0, 1, 0), 208 | new THREE.Vector3().copy(this.options.branch.force.direction), 209 | ); 210 | 211 | qSection.multiply(qTwist); 212 | qSection.rotateTowards( 213 | qForce, 214 | this.options.branch.force.strength / sectionRadius, 215 | ); 216 | 217 | sectionOrientation.setFromQuaternion(qSection); 218 | } 219 | 220 | this.generateBranchIndices(indexOffset, branch); 221 | 222 | // Deciduous trees have a terminal branch that grows out of the 223 | // end of the parent branch 224 | if (this.options.type === 'deciduous') { 225 | const lastSection = sections[sections.length - 1]; 226 | 227 | if (branch.level < this.options.branch.levels) { 228 | this.branchQueue.push( 229 | new Branch( 230 | lastSection.origin, 231 | lastSection.orientation, 232 | this.options.branch.length[branch.level + 1], 233 | lastSection.radius, 234 | branch.level + 1, 235 | // Section count and segment count must be same as parent branch 236 | // since the child branch is growing from the end of the parent branch 237 | branch.sectionCount, 238 | branch.segmentCount, 239 | ), 240 | ); 241 | } else { 242 | this.generateLeaf(lastSection.origin, lastSection.orientation); 243 | } 244 | } 245 | 246 | // If we are on the last branch level, generate leaves 247 | if (branch.level === this.options.branch.levels) { 248 | this.generateLeaves(sections); 249 | } else if (branch.level < this.options.branch.levels) { 250 | this.generateChildBranches( 251 | this.options.branch.children[branch.level], 252 | branch.level + 1, 253 | sections); 254 | } 255 | } 256 | 257 | /** 258 | * Generate branches from a parent branch 259 | * @param {number} count The number of child branches to generate 260 | * @param {number} level The level of the child branches 261 | * @param {{ 262 | * origin: THREE.Vector3, 263 | * orientation: THREE.Euler, 264 | * radius: number 265 | * }[]} sections The parent branch's sections 266 | * @returns 267 | */ 268 | generateChildBranches(count, level, sections) { 269 | const radialOffset = this.rng.random(); 270 | 271 | for (let i = 0; i < count; i++) { 272 | // Determine how far along the length of the parent branch the child 273 | // branch should originate from (0 to 1) 274 | let childBranchStart = this.rng.random(1.0, this.options.branch.start[level]); 275 | 276 | // Find which sections are on either side of the child branch origin point 277 | // so we can determine the origin, orientation and radius of the branch 278 | const sectionIndex = Math.floor(childBranchStart * (sections.length - 1)); 279 | let sectionA, sectionB; 280 | sectionA = sections[sectionIndex]; 281 | if (sectionIndex === sections.length - 1) { 282 | sectionB = sectionA; 283 | } else { 284 | sectionB = sections[sectionIndex + 1]; 285 | } 286 | 287 | // Find normalized distance from section A to section B (0 to 1) 288 | const alpha = 289 | (childBranchStart - sectionIndex / (sections.length - 1)) / 290 | (1 / (sections.length - 1)); 291 | 292 | // Linearly interpolate origin from section A to section B 293 | const childBranchOrigin = new THREE.Vector3().lerpVectors( 294 | sectionA.origin, 295 | sectionB.origin, 296 | alpha, 297 | ); 298 | 299 | // Linearly interpolate radius 300 | const childBranchRadius = 301 | this.options.branch.radius[level] * 302 | ((1 - alpha) * sectionA.radius + alpha * sectionB.radius); 303 | 304 | // Linearlly interpolate the orientation 305 | const qA = new THREE.Quaternion().setFromEuler(sectionA.orientation); 306 | const qB = new THREE.Quaternion().setFromEuler(sectionB.orientation); 307 | const parentOrientation = new THREE.Euler().setFromQuaternion( 308 | qB.slerp(qA, alpha), 309 | ); 310 | 311 | // Calculate the angle offset from the parent branch and the radial angle 312 | const radialAngle = 2.0 * Math.PI * (radialOffset + i / count); 313 | const q1 = new THREE.Quaternion().setFromAxisAngle( 314 | new THREE.Vector3(1, 0, 0), 315 | this.options.branch.angle[level] / (180 / Math.PI), 316 | ); 317 | const q2 = new THREE.Quaternion().setFromAxisAngle( 318 | new THREE.Vector3(0, 1, 0), 319 | radialAngle, 320 | ); 321 | const q3 = new THREE.Quaternion().setFromEuler(parentOrientation); 322 | 323 | const childBranchOrientation = new THREE.Euler().setFromQuaternion( 324 | q3.multiply(q2.multiply(q1)), 325 | ); 326 | 327 | let childBranchLength = 328 | this.options.branch.length[level] * 329 | (this.options.type === TreeType.Evergreen 330 | ? 1.0 - childBranchStart 331 | : 1.0); 332 | 333 | this.branchQueue.push( 334 | new Branch( 335 | childBranchOrigin, 336 | childBranchOrientation, 337 | childBranchLength, 338 | childBranchRadius, 339 | level, 340 | this.options.branch.sections[level], 341 | this.options.branch.segments[level], 342 | ), 343 | ); 344 | } 345 | } 346 | 347 | /** 348 | * Logic for spawning child branches from a parent branch's section 349 | * @param {{ 350 | * origin: THREE.Vector3, 351 | * orientation: THREE.Euler, 352 | * radius: number 353 | * }[]} sections The parent branch's sections 354 | * @returns 355 | */ 356 | generateLeaves(sections) { 357 | const radialOffset = this.rng.random(); 358 | 359 | for (let i = 0; i < this.options.leaves.count; i++) { 360 | // Determine how far along the length of the parent 361 | // branch the leaf should originate from (0 to 1) 362 | let leafStart = this.rng.random(1.0, this.options.leaves.start); 363 | 364 | // Find which sections are on either side of the child branch origin point 365 | // so we can determine the origin, orientation and radius of the branch 366 | const sectionIndex = Math.floor(leafStart * (sections.length - 1)); 367 | let sectionA, sectionB; 368 | sectionA = sections[sectionIndex]; 369 | if (sectionIndex === sections.length - 1) { 370 | sectionB = sectionA; 371 | } else { 372 | sectionB = sections[sectionIndex + 1]; 373 | } 374 | 375 | // Find normalized distance from section A to section B (0 to 1) 376 | const alpha = 377 | (leafStart - sectionIndex / (sections.length - 1)) / 378 | (1 / (sections.length - 1)); 379 | 380 | // Linearly interpolate origin from section A to section B 381 | const leafOrigin = new THREE.Vector3().lerpVectors( 382 | sectionA.origin, 383 | sectionB.origin, 384 | alpha, 385 | ); 386 | 387 | // Linearlly interpolate the orientation 388 | const qA = new THREE.Quaternion().setFromEuler(sectionA.orientation); 389 | const qB = new THREE.Quaternion().setFromEuler(sectionB.orientation); 390 | const parentOrientation = new THREE.Euler().setFromQuaternion( 391 | qB.slerp(qA, alpha), 392 | ); 393 | 394 | // Calculate the angle offset from the parent branch and the radial angle 395 | const radialAngle = 2.0 * Math.PI * (radialOffset + i / this.options.leaves.count); 396 | const q1 = new THREE.Quaternion().setFromAxisAngle( 397 | new THREE.Vector3(1, 0, 0), 398 | this.options.leaves.angle / (180 / Math.PI), 399 | ); 400 | const q2 = new THREE.Quaternion().setFromAxisAngle( 401 | new THREE.Vector3(0, 1, 0), 402 | radialAngle, 403 | ); 404 | const q3 = new THREE.Quaternion().setFromEuler(parentOrientation); 405 | 406 | const leafOrientation = new THREE.Euler().setFromQuaternion( 407 | q3.multiply(q2.multiply(q1)), 408 | ); 409 | 410 | this.generateLeaf(leafOrigin, leafOrientation); 411 | } 412 | } 413 | 414 | /** 415 | * Generates a leaves 416 | * @param {THREE.Vector3} origin The starting point of the branch 417 | * @param {THREE.Euler} orientation The starting orientation of the branch 418 | */ 419 | generateLeaf(origin, orientation) { 420 | let i = this.leaves.verts.length / 3; 421 | 422 | // Width and length of the leaf quad 423 | let leafSize = 424 | this.options.leaves.size * 425 | (1 + 426 | this.rng.random( 427 | this.options.leaves.sizeVariance, 428 | -this.options.leaves.sizeVariance, 429 | )); 430 | 431 | const W = leafSize; 432 | const L = leafSize; 433 | 434 | const createLeaf = (rotation) => { 435 | // Create quad vertices 436 | const v = [ 437 | new THREE.Vector3(-W / 2, L, 0), 438 | new THREE.Vector3(-W / 2, 0, 0), 439 | new THREE.Vector3(W / 2, 0, 0), 440 | new THREE.Vector3(W / 2, L, 0), 441 | ].map((v) => 442 | v 443 | .applyEuler(new THREE.Euler(0, rotation, 0)) 444 | .applyEuler(orientation) 445 | .add(origin), 446 | ); 447 | 448 | this.leaves.verts.push( 449 | v[0].x, 450 | v[0].y, 451 | v[0].z, 452 | v[1].x, 453 | v[1].y, 454 | v[1].z, 455 | v[2].x, 456 | v[2].y, 457 | v[2].z, 458 | v[3].x, 459 | v[3].y, 460 | v[3].z, 461 | ); 462 | 463 | const n = new THREE.Vector3(0, 0, 1).applyEuler(orientation); 464 | this.leaves.normals.push( 465 | n.x, 466 | n.y, 467 | n.z, 468 | n.x, 469 | n.y, 470 | n.z, 471 | n.x, 472 | n.y, 473 | n.z, 474 | n.x, 475 | n.y, 476 | n.z, 477 | ); 478 | this.leaves.uvs.push(0, 1, 0, 0, 1, 0, 1, 1); 479 | this.leaves.indices.push(i, i + 1, i + 2, i, i + 2, i + 3); 480 | i += 4; 481 | }; 482 | 483 | createLeaf(0); 484 | if (this.options.leaves.billboard === Billboard.Double) { 485 | createLeaf(Math.PI / 2); 486 | } 487 | } 488 | 489 | /** 490 | * Generates the indices for branch geometry 491 | * @param {Branch} branch 492 | */ 493 | generateBranchIndices(indexOffset, branch) { 494 | // Build geometry each section of the branch (cylinder without end caps) 495 | let v1, v2, v3, v4; 496 | const N = branch.segmentCount + 1; 497 | for (let i = 0; i < branch.sectionCount; i++) { 498 | // Build the quad for each segment of the section 499 | for (let j = 0; j < branch.segmentCount; j++) { 500 | v1 = indexOffset + i * N + j; 501 | // The last segment wraps around back to the starting segment, so omit j + 1 term 502 | v2 = indexOffset + i * N + (j + 1); 503 | v3 = v1 + N; 504 | v4 = v2 + N; 505 | this.branches.indices.push(v1, v3, v2, v2, v3, v4); 506 | } 507 | } 508 | } 509 | 510 | /** 511 | * Generates the geometry for the branches 512 | */ 513 | createBranchesGeometry() { 514 | const g = new THREE.BufferGeometry(); 515 | g.setAttribute( 516 | 'position', 517 | new THREE.BufferAttribute(new Float32Array(this.branches.verts), 3), 518 | ); 519 | g.setAttribute( 520 | 'normal', 521 | new THREE.BufferAttribute(new Float32Array(this.branches.normals), 3), 522 | ); 523 | g.setAttribute( 524 | 'uv', 525 | new THREE.BufferAttribute(new Float32Array(this.branches.uvs), 2), 526 | ); 527 | g.setIndex( 528 | new THREE.BufferAttribute(new Uint16Array(this.branches.indices), 1), 529 | ); 530 | g.computeBoundingSphere(); 531 | 532 | const mat = new THREE.MeshPhongMaterial({ 533 | name: 'branches', 534 | flatShading: this.options.bark.flatShading, 535 | color: new THREE.Color(this.options.bark.tint), 536 | }); 537 | 538 | if (this.options.bark.textured) { 539 | mat.aoMap = getBarkTexture(this.options.bark.type, 'ao', this.options.bark.textureScale); 540 | mat.map = getBarkTexture(this.options.bark.type, 'color', this.options.bark.textureScale); 541 | mat.normalMap = getBarkTexture(this.options.bark.type, 'normal', this.options.bark.textureScale); 542 | mat.roughnessMap = getBarkTexture(this.options.bark.type, 'roughness', this.options.bark.textureScale); 543 | } 544 | 545 | this.branchesMesh.geometry.dispose(); 546 | this.branchesMesh.geometry = g; 547 | this.branchesMesh.material.dispose(); 548 | this.branchesMesh.material = mat; 549 | this.branchesMesh.castShadow = true; 550 | this.branchesMesh.receiveShadow = true; 551 | } 552 | 553 | /** 554 | * Generates the geometry for the leaves 555 | */ 556 | createLeavesGeometry() { 557 | const g = new THREE.BufferGeometry(); 558 | g.setAttribute( 559 | 'position', 560 | new THREE.BufferAttribute(new Float32Array(this.leaves.verts), 3), 561 | ); 562 | g.setAttribute( 563 | 'uv', 564 | new THREE.BufferAttribute(new Float32Array(this.leaves.uvs), 2), 565 | ); 566 | g.setIndex( 567 | new THREE.BufferAttribute(new Uint16Array(this.leaves.indices), 1), 568 | ); 569 | g.computeVertexNormals(); 570 | g.computeBoundingSphere(); 571 | 572 | const mat = new THREE.MeshPhongMaterial({ 573 | name: 'leaves', 574 | map: getLeafTexture(this.options.leaves.type), 575 | color: new THREE.Color(this.options.leaves.tint), 576 | side: THREE.DoubleSide, 577 | alphaTest: this.options.leaves.alphaTest, 578 | dithering: true 579 | }); 580 | 581 | // Add custom shader code for branch swaying 582 | mat.onBeforeCompile = (shader) => { 583 | shader.uniforms.uTime = { value: 0 }; 584 | shader.uniforms.uWindStrength = { value: new THREE.Vector3(0.5, 0, 0.5) }; 585 | shader.uniforms.uWindFrequency = { value: 0.5 }; 586 | shader.uniforms.uWindScale = { value: 70 }; 587 | 588 | shader.vertexShader = ` 589 | uniform float uTime; 590 | uniform vec3 uWindStrength; 591 | uniform float uWindFrequency; 592 | uniform float uWindScale; 593 | ` + shader.vertexShader; 594 | 595 | // Add code for simplex noise 596 | shader.vertexShader = shader.vertexShader.replace( 597 | `void main() {`, 598 | ` 599 | // GLSL Simplex Noise 3D 600 | // Source: https://github.com/ashima/webgl-noise 601 | 602 | vec3 mod289(vec3 x) { 603 | return x - floor(x * (1.0 / 289.0)) * 289.0; 604 | } 605 | 606 | vec4 mod289(vec4 x) { 607 | return x - floor(x * (1.0 / 289.0)) * 289.0; 608 | } 609 | 610 | vec4 permute(vec4 x) { 611 | return mod289(((x*34.0)+1.0)*x); 612 | } 613 | 614 | vec4 taylorInvSqrt(vec4 r) { 615 | return 1.79284291400159 - 0.85373472095314 * r; 616 | } 617 | 618 | vec3 fade(vec3 t) { 619 | return t*t*t*(t*(t*6.0-15.0)+10.0); 620 | } 621 | 622 | // Classic Simplex Noise 3D 623 | float simplex3(vec3 v) { 624 | const vec2 C = vec2(1.0/6.0, 1.0/3.0); 625 | const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); 626 | 627 | // First corner 628 | vec3 i = floor(v + dot(v, C.yyy) ); 629 | vec3 x0 = v - i + dot(i, C.xxx); 630 | 631 | // Other corners 632 | vec3 g = step(x0.yzx, x0.xyz); 633 | vec3 l = 1.0 - g; 634 | vec3 i1 = min( g.xyz, l.zxy ); 635 | vec3 i2 = max( g.xyz, l.zxy ); 636 | 637 | // x0 = x0 - 0. + 0.0 * C 638 | vec3 x1 = x0 - i1 + C.xxx; 639 | vec3 x2 = x0 - i2 + C.yyy; // 2.0 * C.x = 1/3 = C.y 640 | vec3 x3 = x0 - D.yyy; // -1.0 + 3.0 * C.x = -0.5 641 | 642 | // Permutations 643 | i = mod289(i); 644 | vec4 p = permute( permute( permute( 645 | i.z + vec4(0.0, i1.z, i2.z, 1.0 )) 646 | + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) 647 | + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); 648 | 649 | // Gradients: 7x7 points over a square, mapped onto an octahedron. 650 | // The ring size 17*17 = 289 is close to the mapping's singularity. 651 | float n_ = 0.142857142857; // 1.0/7.0 652 | vec3 ns = n_ * D.wyz - D.xzx; 653 | 654 | vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) 655 | 656 | vec4 x_ = floor(j * ns.z); 657 | vec4 y_ = floor(j - 7.0 * x_ ); // mod(j,N) 658 | 659 | vec4 x = x_ *ns.x + ns.yyyy; 660 | vec4 y = y_ *ns.x + ns.yyyy; 661 | vec4 h = 1.0 - abs(x) - abs(y); 662 | 663 | vec4 b0 = vec4( x.xy, y.xy ); 664 | vec4 b1 = vec4( x.zw, y.zw ); 665 | 666 | vec4 s0 = floor(b0)*2.0 + 1.0; 667 | vec4 s1 = floor(b1)*2.0 + 1.0; 668 | vec4 sh = -step(h, vec4(0.0)); 669 | 670 | vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; 671 | vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; 672 | 673 | vec3 g0 = vec3(a0.xy,h.x); 674 | vec3 g1 = vec3(a0.zw,h.y); 675 | vec3 g2 = vec3(a1.xy,h.z); 676 | vec3 g3 = vec3(a1.zw,h.w); 677 | 678 | // Normalise gradients 679 | vec4 norm = taylorInvSqrt(vec4(dot(g0,g0), dot(g1,g1), dot(g2,g2), dot(g3,g3))); 680 | g0 *= norm.x; 681 | g1 *= norm.y; 682 | g2 *= norm.z; 683 | g3 *= norm.w; 684 | 685 | // Mix contributions from the four corners 686 | vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); 687 | m = m * m; 688 | return 42.0 * dot( m*m, vec4( dot(g0,x0), dot(g1,x1), 689 | dot(g2,x2), dot(g3,x3) ) ); 690 | } 691 | 692 | void main() {`, 693 | ); 694 | 695 | shader.vertexShader = shader.vertexShader.replace( 696 | `#include `, 697 | ` 698 | vec4 mvPosition = vec4(transformed, 1.0); 699 | 700 | float windOffset = 2.0 * 3.14 * simplex3(mvPosition.xyz / uWindScale); 701 | vec3 windSway = uv.y * uWindStrength * ( 702 | 0.5 * sin(uTime * uWindFrequency + windOffset) + 703 | 0.3 * sin(2.0 * uTime * uWindFrequency + 1.3 * windOffset) + 704 | 0.2 * sin(5.0 * uTime * uWindFrequency + 1.5 * windOffset) 705 | ); 706 | mvPosition.xyz += windSway; 707 | 708 | mvPosition = modelViewMatrix * mvPosition; 709 | gl_Position = projectionMatrix * mvPosition; 710 | ` 711 | ); 712 | 713 | mat.userData.shader = shader; 714 | }; 715 | 716 | this.leavesMesh.geometry.dispose(); 717 | this.leavesMesh.geometry = g; 718 | this.leavesMesh.material.dispose(); 719 | 720 | this.leavesMesh.material = mat; 721 | 722 | this.leavesMesh.castShadow = true; 723 | this.leavesMesh.receiveShadow = true; 724 | } 725 | 726 | get vertexCount() { 727 | return (this.branches.verts.length + this.leaves.verts.length) / 3; 728 | } 729 | 730 | get triangleCount() { 731 | return (this.branches.indices.length + this.leaves.indices.length) / 3; 732 | } 733 | } 734 | --------------------------------------------------------------------------------