three_js_terrain 0.1.0 copy "three_js_terrain: ^0.1.0" to clipboard
three_js_terrain: ^0.1.0 copied to clipboard

A type of three_js api that allows users to create terrain for their projects.

example/lib/main.dart

import 'dart:typed_data';

import 'package:css/css.dart';
import 'package:example/change_image.dart';
import 'package:example/gui.dart';
import 'package:flutter/material.dart';
import 'package:three_js/three_js.dart' as three;
import 'package:three_js_geometry/three_js_geometry.dart';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;

import 'dart:math' as math;
import 'dart:async';
import 'package:three_js_terrain/three_js_terrain.dart' as terrain;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: CSS.darkTheme,
      home: const TerrainPage(),
    );
  }
}

class TerrainPage extends StatefulWidget {
  const TerrainPage({super.key});
  @override
  State<TerrainPage> createState() => _TerrainPageState();
}

class _TerrainPageState extends State<TerrainPage> {
  late three.ThreeJS threeJs;
  late three.OrbitControls orbit;
  late three.FirstPersonControls controls;
  late three.PerspectiveCamera cameraPersp;
  late Gui gui;
  three.Material? blend;
  late Uint8List heightmap;
  
  @override
  void initState() {
    gui = Gui((){
      setState(() {
        
      });
    });
    threeJs = three.ThreeJS(
      onSetupComplete: (){setState(() {});},
      setup: setup,
    );
    super.initState();
  }
  @override
  void dispose() {
    threeJs.dispose();
    controls.dispose();
    orbit.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          threeJs.build(),
          if(heightMapImage != null)Positioned(
            top: 20,
            left: 20,
            child: Image.memory(
              heightMapImage!,
              width: 120,
              fit: BoxFit.fitHeight,
            )
          ),
          if(heightMapImage != null)Positioned(
            top: 20,
            right: 20,
            child: SizedBox(
              height: threeJs.height,
              width: 240,
              child: gui.render(context)
            )
          )
        ]
      )
    );
  }

  late three.DirectionalLight skyLight;
  late three.Mesh water;
  late three.DirectionalLight light;
  late three.Mesh skyDome;
  late three.Mesh sand;
  three.Object3D? terrainScene;
  Uint8List? heightMapImage;

  Future<void> setup() async{
    threeJs.scene = three.Scene();
    //threeJs.scene.fog = three.FogExp2(0x868293, 0.0007);

    threeJs.camera = three.PerspectiveCamera(60, threeJs.width / threeJs.height, 1, 10000);
    threeJs.scene.add(threeJs.camera);

    threeJs.camera.position.x = 449;
    threeJs.camera.position.y = 311;
    threeJs.camera.position.z = 376;
    threeJs.camera.rotation.x = -52 * math.pi / 180;
    threeJs.camera.rotation.y = 35 * math.pi / 180;
    threeJs.camera.rotation.z = 37 * math.pi / 180;

    orbit = three.OrbitControls(threeJs.camera, threeJs.globalKey);

    //setupControls();
    
    await getHeightMapFromImage();
    await setupWorld();
    await settings();
    setupGui();

    threeJs.addAnimationEvent((dt){
      orbit.update();
    });
  }

  void setupControls() {
    final fpsCamera = three.PerspectiveCamera(60, threeJs.width / threeJs.height, 1, 10000);
    threeJs.scene.add(fpsCamera);
    controls = three.FirstPersonControls(camera: fpsCamera, listenableKey: threeJs.globalKey);
    controls.enabled = false;
    controls.movementSpeed = 100;
    controls.lookSpeed = 0.075;
  }
  void setupGui(){
    gui.addFolder('Heightmap')
    ..open()
    ..addDropDown(
      guiSettings,
      'heightmap', 
      ['Brownian', 'Cosine', 'CosineLayers', 'DiamondSquare', 'Fault', 'heightmap.png', 'Hill', 'HillIsland', 'influences', 'Particles', 'Perlin', 'PerlinDiamond', 'PerlinLayers', 'Simplex', 'SimplexLayers', 'Value', 'Weierstrass', 'Worley'],
    ).onFinishChange((){regenerate(blend);})
    ..addDropDown(
      guiSettings,
      'easing', 
      ['Linear', 'EaseIn', 'EaseInWeak', 'EaseOut', 'EaseInOut', 'InEaseOut']
    ).onFinishChange((){regenerate(blend);})
    ..addDropDown(
      guiSettings,
      'smoothing', 
      ['Conservative (0.5)', 'Conservative (1)', 'Conservative (10)', 'Gaussian (0.5, 7)', 'Gaussian (1.0, 7)', 'Gaussian (1.5, 7)', 'Gaussian (1.0, 5)', 'Gaussian (1.0, 11)', 'GaussianBox', 'Mean (0)', 'Mean (1)', 'Mean (8)', 'Median', 'None']
    ).onChange((val) {
      applySmoothing(val, lastOptions);
      scatterMeshes();
      if (lastOptions.heightmap != null) {
        terrain.Terrain.toHeightmap(terrainScene!.children[0].geometry!.attributes['position'].array.toDartList(), lastOptions);
      }
    })
    ..addSlider(guiSettings,'segments', 7, 127).onFinishChange((){regenerate(blend);})
    ..addSlider(guiSettings,'steps', 1, 8).onFinishChange((){regenerate(blend);})
    ..addCheckBox(guiSettings,'turbulent').onFinishChange((){regenerate(blend);});

    var decoFolder = gui.addFolder('Decoration');
    decoFolder.addDropDown(guiSettings,'texture', ['Blended', 'Grayscale', 'Wireframe']).onFinishChange((){regenerate(blend);});
    decoFolder.addDropDown(guiSettings,'scattering', ['Altitude', 'Linear', 'Cosine', 'CosineLayers', 'DiamondSquare', 'Particles', 'Perlin', 'PerlinAltitude', 'Simplex', 'Value', 'Weierstrass', 'Worley']).onFinishChange(scatterMeshes);
    decoFolder.addSlider(guiSettings,'spread', 0, 100)..step(1)..onFinishChange(scatterMeshes);
    decoFolder.addColor(guiSettings,'lightColor').onChange((val) {
      skyLight.color?.setFromHex32(val);
    });
    var sizeFolder = gui.addFolder('Size');
    sizeFolder.addSlider(guiSettings,'size', 1024, 3072)..step(256)..onFinishChange((){regenerate(blend);});
    sizeFolder.addSlider(guiSettings,'maxHeight', 2, 300)..step(2)..onFinishChange((){regenerate(blend);});
    sizeFolder.addSlider(guiSettings,'ratio', 0.2, 2)..step(0.05)..onFinishChange((){regenerate(blend);});

    var edgesFolder = gui.addFolder('Edges');
    edgesFolder.addDropDown(guiSettings,'edgeType', ['Box', 'Radial']).onFinishChange((){regenerate(blend);});
    edgesFolder.addDropDown(guiSettings,'edgeDirection', ['Normal', 'Up', 'Down']).onFinishChange((){regenerate(blend);});
    edgesFolder.addDropDown(guiSettings,'edgeCurve', ['Linear', 'EaseIn', 'EaseOut', 'EaseInOut']).onFinishChange((){regenerate(blend);});
    edgesFolder.addSlider(guiSettings,'edgeDistance', 0, 512)..step(32)..onFinishChange((){regenerate(blend);});

    gui.addFolder('Other')
    ..addFunction('Scatter meshes').onFinishChange((){scatterMeshes();})
    ..addFunction('Regenerate').onFinishChange((){regenerate(blend);});
  }
  Future<void> setupWorld() async{
    three.TextureLoader().fromAsset('assets/sky1.jpg').then((t1) {
      t1?.minFilter = three.LinearFilter; // Texture is not a power-of-two size; use smoother interpolation.
      skyDome = three.Mesh(
        three.SphereGeometry(8192, 16, 16, 0, math.pi*2, 0, math.pi*0.5),
        three.MeshBasicMaterial.fromMap({'map': t1, 'side': three.BackSide, 'fog': false})
      );
      skyDome.position.y = -99;
      threeJs.scene.add(skyDome);
    });

    water = three.Mesh(
      three.PlaneGeometry(16384+1024, 16384+1024, 16, 16),
      three.MeshLambertMaterial.fromMap({'color': 0x006ba0, 'transparent': true, 'opacity': 0.6})
    );
    water.position.y = -99;
    water.rotation.x = -0.5 * math.pi;
    threeJs.scene.add(water);

    skyLight = three.DirectionalLight(0xe8bdb0, 1.5);
    skyLight.position.setValues(2950, 2625, -160); // Sun on the sky texture
    threeJs.scene.add(skyLight);

    light = three.DirectionalLight(0xc3eaff, 0.75);
    light.position.setValues(-1, -0.5, -1);
    threeJs.scene.add(light);
  }

  three.Object3D buildTree() {
    final green = three.MeshLambertMaterial.fromMap({ 'color': 0x2d4c1e });

    final c0 = three.Mesh(
      CylinderGeometry(2, 2, 12, 6, 1, true),
      three.MeshLambertMaterial.fromMap({ 'color': 0x3d2817 }) // brown
    );
    c0.position.setY(6);

    final c1 = three.Mesh(CylinderGeometry(0, 10, 14, 8), green);
    c1.position.setY(18);
    final c2 = three.Mesh(CylinderGeometry(0, 9, 13, 8), green);
    c2.position.setY(25);
    final c3 = three.Mesh(CylinderGeometry(0, 8, 12, 8), green);
    c3.position.setY(32);

    final s = three.Object3D();
    s.add(c0);
    s.add(c1);
    s.add(c2);
    s.add(c3);
    s.scale.setValues(5, 1.25, 5);

    return s;
  }

  void applySmoothing(smoothing, terrain.TerrainOptions o) {
    three.Object3D m = terrainScene!.children[0];
    Float32List g = terrain.Terrain.toArray1D(m.geometry!.attributes['position'].array.toDartList());
    if (smoothing == 'Conservative (0.5)') terrain.Terrain.smoothConservative(g, o, 0.5);
    if (smoothing == 'Conservative (1)') terrain.Terrain.smoothConservative(g, o, 1);
    if (smoothing == 'Conservative (10)'){ terrain.Terrain.smoothConservative(g, o, 10);}
    else if (smoothing == 'Gaussian (0.5, 7)'){ terrain.Gaussian(g, o, 0.5, 7);}
    else if (smoothing == 'Gaussian (1.0, 7)'){ terrain.Gaussian(g, o, 1, 7);}
    else if (smoothing == 'Gaussian (1.5, 7)'){ terrain.Gaussian(g, o, 1.5, 7);}
    else if (smoothing == 'Gaussian (1.0, 5)'){ terrain.Gaussian(g, o, 1, 5);}
    else if (smoothing == 'Gaussian (1.0, 11)'){ terrain.Gaussian(g, o, 1, 11);}
    else if (smoothing == 'GaussianBox'){ terrain.GaussianBoxBlur(g, o, 1, 3);}
    else if (smoothing == 'Mean (0)'){ terrain.Terrain.smooth(g, o, 0);}
    else if (smoothing == 'Mean (1)'){ terrain.Terrain.smooth(g, o, 1);}
    else if (smoothing == 'Mean (8)'){ terrain.Terrain.smooth(g, o, 8);}
    else if (smoothing == 'Median'){ terrain.Terrain.smoothMedian(g, o);}
    terrain.Terrain.fromArray1D(m.geometry!.attributes['position'].array.toDartList(), g);
    terrain.Terrain.normalize(m, o);
  }

  void customInfluences(Float32List g, terrain.TerrainOptions options) {
    final clonedOptions = terrain.TerrainOptions();
    for (final opt in options.keys) {
      if (options.containsKey(opt)) {
        clonedOptions[opt] = options[opt];
      }
    }
    clonedOptions.maxHeight = options.maxHeight! * 0.67;
    clonedOptions.minHeight = options.minHeight! * 0.67;
    terrain. Generators.diamondSquare(g, clonedOptions);

    var radius = math.min(options.xSize, options.ySize) * 0.21,
        height = options.maxHeight! * 0.8;
    terrain.Terrain.influence(
      g, options,
      terrain.Terrain.influences[terrain.InfluenceType.hill],
      0.25, 0.25,
      radius, height,
      three.AdditiveBlending,
      terrain.Easing.linear
    );
    terrain.Terrain.influence(
      g, options,
      terrain.Terrain.influences[terrain.InfluenceType.mesa],
      0.75, 0.75,
      radius, height,
      three.SubtractiveBlending,
      terrain.Easing.easeInStrong
    );
    terrain.Terrain.influence(
      g, options,
      terrain.Terrain.influences[terrain.InfluenceType.flat],
      0.75, 0.25,
      radius, options.maxHeight,
      three.NormalBlending,
      terrain.Easing.easeIn
    );
    terrain.Terrain.influence(
      g, options,
      terrain.Terrain.influences[terrain.InfluenceType.volcano],
      0.25, 0.75,
      radius, options.maxHeight,
      three.NormalBlending,
      terrain.Easing.easeInStrong
    );
  }

  terrain.TerrainOptions lastOptions = terrain.TerrainOptions();
  three.Object3D? decoScene;
  void after(Float32List vertices, terrain.TerrainOptions options) {
    if (guiSettings['edgeDirection'] != 'Normal') {
      (guiSettings['edgeType'] == 'Box' ? terrain.Terrain.edges : terrain.Terrain.radialEdges)(
        vertices,
        options,
        guiSettings['edgeDirection'] == 'Up' ? true : false,
        guiSettings['edgeType'] == 'Box' ? guiSettings['edgeDistance'] : math.min(options.xSize, options.ySize) * 0.5 - guiSettings['edgeDistance'],
        terrain.Easing.fromString(guiSettings['edgeCurve'])
      );
    }
  }

  Map<String,dynamic> guiSettings = {
    'lightColor': 0xe8bdb0,
    'easing': 'Linear',
    'heightmap': 'Perlin',
    'smoothing': 'None',
    'maxHeight': 200.0,
    'segments': 63.0,
    'steps': 1.0,
    'turbulent': false,
    'size': 1024.0,
    'sky': true,
    'texture': 'Blended',
    'edgeDirection': 'Normal',
    'edgeType': 'Box',
    'edgeDistance': 256.0,
    'edgeCurve': 'EaseInOut',
    'ratio': 1.0,
    'flightMode':false,//useFPS;
    'spread': 60.0,
    'scattering':'Linear',//'PerlinAltitude';
  };

  Future<void> getHeightMapFromImage() async{
    final ByteData fileData = await rootBundle.load('assets/heightmap.png');
    final bytes = fileData.buffer.asUint8List();
    img.Image? image = img.decodeImage(bytes);
    heightmap = image!.getBytes();
  }

  Future<void> settings() async{
    guiSettings['lightColor'] = skyLight.color!.getHex();
    // var elevationGraph = document.getElementById('elevation-graph'),
    //     slopeGraph = document.getElementById('slope-graph'),
    //     analyticsValues = document.getElementsByClassName('value');

    three.TextureLoader loader = three.TextureLoader();
    final t1 = await loader.fromAsset('assets/sand1.jpg');
    t1?.wrapS = t1.wrapT = three.RepeatWrapping;
    sand = three.Mesh(
      three.PlaneGeometry(16384+1024, 16384+1024, 64, 64),
      three.MeshLambertMaterial.fromMap({'map': t1})
    );
    sand.position.y = -101;
    sand.rotation.x = -0.5 * math.pi;
    threeJs.scene.add(sand);

    final t2 = await loader.fromAsset('assets/grass1.jpg');
    final t3 = await loader.fromAsset('assets/stone1.jpg');
    final t4 = await loader.fromAsset('assets/snow1.jpg');

    blend = terrain.Terrain.generateBlendedMaterial([
      terrain.TerrainTextures(texture: t1!),
      terrain.TerrainTextures(texture: t2!, levels: [-80, -35, 20, 50]),
      terrain.TerrainTextures(texture: t3!, levels: [20, 50, 60, 85]),
      terrain.TerrainTextures(texture: t4!, glsl: '1.0 - smoothstep(65.0 + smoothstep(-256.0, 256.0, vPosition.x) * 10.0, 80.0, vPosition.z)'),
      terrain.TerrainTextures(texture: t3, glsl: 'slope > 0.7853981633974483 ? 0.2 : 1.0 - smoothstep(0.47123889803846897, 0.7853981633974483, slope) + 0.2'), // between 27 and 45 degrees
    ]);

    regenerate(blend);
    scatterMeshes();
  }

  void scatterMeshes() {
    var mesh = buildTree();
      var s = guiSettings['segments'].toInt(),
          sprd,
          randomness;
      var o = terrain.TerrainOptions(
        xSegments: s,
        ySegments: (s * guiSettings['ratio']).round(),
      );
      if (guiSettings['scattering'] == 'Linear') {
        sprd = guiSettings['spread'] * 0.0005;
        randomness = (k){return math.Random().nextDouble();};
      }
      else if (guiSettings['scattering'] == 'Altitude') {
        sprd = altitudeSpread;
      }
      else if (guiSettings['scattering'] == 'PerlinAltitude') {
        sprd = ((){
          var h = terrain.Terrain.scatterHelper(terrain.Generators.perlin, o, 2, 0.125)(),
              hs = terrain.Easing.inEaseOut(guiSettings['spread'] * 0.01);
          return (three.Vector3 v, double k, three.Vector3 v2, int i) {
            var rv = h[k.toInt()],
                place = false;
            if (rv < hs) {
              place = true;
            }
            else if (rv < hs + 0.2) {
              place = terrain.Easing.easeInOut((rv - hs) * 5) * hs < math.Random().nextDouble();
            }
            return math.Random().nextDouble() < altitudeProbability(v.z) * 5 && place;
          };
        })();
      }
      else {
        sprd = terrain.Easing.inEaseOut(guiSettings['spread']*0.01) * (guiSettings['scattering'] == 'Worley' ? 1 : 0.5);
        final l = terrain.Terrain.scatterHelper(terrain.Terrain.fromString(guiSettings['scattering'])!, o, 2, 0.125);
        randomness = (k){return l()[k.toInt()];};
      }
      var geo = terrainScene!.children[0].geometry!;
      if(decoScene != null){
        terrainScene!.remove(decoScene!);
      }
      decoScene = terrain.Terrain.scatterMeshes(geo, terrain.ScatterOptions(
        mesh: mesh,
        w: s.toDouble(),
        h: (s * guiSettings['ratio']).roundToDouble(),
        spread: sprd is double ?sprd:0.025,
        spreadFunction: sprd is double ?null:sprd,
        smoothSpread: guiSettings['scattering'] == 'Linear' ? 0 : 0.2,
        randomness: randomness,
        maxSlope: 0.6283185307179586, // 36deg or 36 / 180 * Math.PI, about the angle of repose of earth
        maxTilt: 0.15707963267948966, //  9deg or  9 / 180 * Math.PI. Trees grow up regardless of slope but we can allow a small variation
      ));
      if (decoScene != null) {
        terrainScene!.add(decoScene);
      }
    }

  void regenerate(three.Material? blend){
    var mat = three.MeshBasicMaterial.fromMap({'color': 0x5566aa, 'wireframe': true});
    var gray = three.MeshPhongMaterial.fromMap({ 'color': 0x88aaaa, 'specular': 0x444455, 'shininess': 10 });

    var s = guiSettings['segments'].toInt(),//int.parse(segments, 10),
        h = guiSettings['heightmap'] == 'heightmap.png';
    var o = terrain.TerrainOptions(
      after: after,
      easing: terrain.Easing.fromString(guiSettings['easing'])!,
      heightmap: h? heightmap:guiSettings['heightmap'] == 'influences' ? customInfluences :terrain.Terrain.fromString(guiSettings['heightmap']),//heightMapImage,//h ? heightmapImage : (heightmap == 'influences' ? customInfluences : THREE.Terrain[heightmap]),
      material: guiSettings['texture'] == 'Wireframe' ? mat : (guiSettings['texture'] == 'Blended' ? blend : gray),
      maxHeight: guiSettings['maxHeight'] - 100,
      minHeight: -100,
      steps: guiSettings['steps'].toInt(),
      stretch: true,
      turbulent: guiSettings['turbulent'],
      xSize: guiSettings['size'].toDouble(),
      ySize: (guiSettings['size'] * guiSettings['ratio']).roundToDouble(),
      xSegments: s,
      ySegments: (s * guiSettings['ratio']).round(),
    );
    if(terrainScene != null){
      threeJs.scene.remove(terrainScene!);
    }
    terrainScene = terrain.Terrain.create(o);
    applySmoothing(guiSettings['smoothing'], o);
    threeJs.scene.add(terrainScene);
    skyDome.visible = sand.visible = water.visible = guiSettings['texture'] != 'Wireframe';
    // var he = document.getElementById('heightmap');
    // if (he != null) {
    //   o.heightmap = he;
      heightMapImage = terrain.Terrain.toHeightmap(terrainScene!.children[0].geometry!.attributes['position'].array.toDartList(), o);
    // }
    heightMapImage = rgba2bitmap(heightMapImage!, o.xSegments+1, o.ySegments+1);
    lastOptions = o;
    
    scatterMeshes();
  }
  double altitudeProbability(double z) {
    if (z > -80 && z < -50){ return terrain.Easing.easeInOut((z + 80) / (-50 + 80)) * guiSettings['spread'] * 0.002;}
    else if (z > -50 && z < 20) {return guiSettings['spread'] * 0.002;}
    else if (z > 20 && z < 50) {return terrain.Easing.easeInOut((z - 20) / (50 - 20)) * guiSettings['spread'] * 0.002;}
    return 0;
  }
  bool altitudeSpread(three.Vector3 v,double k,three.Vector3 v2,int i){//double v, double k) {
    return k % 4 == 0 && math.Random().nextDouble() < altitudeProbability(v.z);
  }
}
1
likes
150
points
93
downloads

Publisher

unverified uploader

Weekly Downloads

A type of three_js api that allows users to create terrain for their projects.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, image, three_js_core, three_js_math

More

Packages that depend on three_js_terrain