scatterMeshes static method

Object3D scatterMeshes(
  1. BufferGeometry geometry,
  2. ScatterOptions options
)

Scatter a mesh across the terrain.

BufferGeometry geometry The terrain's geometry (or the highest-resolution version of it). TerrainOptions options A map of settings that controls how the meshes are scattered, with the following properties:

  • mesh: A THREE.Mesh instance to scatter across the terrain.
  • spread: A number or a function that affects where meshes are placed. If it is a number, it represents the percent of faces of the terrain onto which a mesh should be placed. If it is a function, it takes a vertex from the terrain and the key of a related face and returns a boolean indicating whether to place a mesh on that face or not. An example could be function(v, k) { return v.z > 0 && !(k % 4); }. Defaults to 0.025.
  • smoothSpread: If the spread option is a number, this affects how much placement is "eased in." Specifically, if the randomness function returns a value for a face that is within smoothSpread percentiles above spread, then the probability that a mesh is placed there is interpolated between zero and spread. This creates a "thinning" effect near the edges of clumps, if the randomness function creates clumps.
  • scene: A THREE.Object3D instance to which the scattered meshes will be added. This is expected to be either a return value of a call to THREE.Terrain() or added to that return value; otherwise the position and rotation of the meshes will be wrong.
  • sizeVariance: The percent by which instances of the mesh can be scaled up or down when placed on the terrain.
  • randomness: If options.spread is a number, then this property is a function that determines where meshes are placed. Specifically, it returns an array of numbers, where each number is the probability that a mesh is NOT placed on the corresponding face. Valid values include Math.random and the return value of a call to THREE.Terrain.ScatterHelper.
  • maxSlope: The angle in radians between the normal of a face of the terrain and the "up" vector above which no mesh will be placed on the related face. Defaults to ~0.63, which is 36 degrees.
  • maxTilt: The maximum angle in radians a mesh can be tilted away from the "up" vector (towards the normal vector of the face of the terrain). Defaults to Infinity (meshes will point towards the normal).
  • w: The number of horizontal segments of the terrain.
  • h: The number of vertical segments of the terrain.

@return {THREE.Object3D} An Object3D containing the scattered meshes. This is the value of the options.scene parameter if passed. This is expected to be either a return value of a call to THREE.Terrain() or added to that return value; otherwise the position and rotation of the meshes will be wrong.

Implementation

static Object3D scatterMeshes(BufferGeometry geometry, ScatterOptions options) {
  options.scene ??= Object3D();

  final spreadIsNumber = options.spreadFunction == null,
      spreadRange = 1 / options.smoothSpread,
      doubleSizeVariance = options.sizeVariance*2,
      vertex1 = Vector3.zero(),
      vertex2 = Vector3.zero(),
      vertex3 = Vector3.zero(),
      faceNormal = Vector3.zero(),
      up = options.mesh.up.clone().applyAxisAngle(Vector3(1, 0, 0), 0.5*math.pi);
  final dynamic randomHeightmap;
  dynamic randomness;
  if (spreadIsNumber) {
    randomHeightmap = options.randomness;
    randomness = (k) { return randomHeightmap(k) ?? math.Random().nextDouble();};
  }

  geometry = geometry.toNonIndexed();
  final gArray = geometry.attributes['position'].array;
  for (int i = 0; i < geometry.attributes['position'].array.length; i += 9) {
    vertex1.setValues(gArray[i + 0], gArray[i + 1], gArray[i + 2]);
    vertex2.setValues(gArray[i + 3], gArray[i + 4], gArray[i + 5]);
    vertex3.setValues(gArray[i + 6], gArray[i + 7], gArray[i + 8]);
    Triangle.staticGetNormal(vertex1, vertex2, vertex3, faceNormal);

    bool place = false;
    if (spreadIsNumber) {
      final rv = randomness(i/9);
      if (rv < options.spread) {
        place = true;
      }
      else if (rv < options.spread + options.smoothSpread) {
        // Interpolate rv between spread and spread + smoothSpread,
        // then multiply that "easing" value by the probability
        // that a mesh would get placed on a given face.
        place = Easing.easeInOut((rv - options.spread)*spreadRange)*options.spread > math.Random().nextDouble();
      }
    }
    else {
      place = options.spreadFunction!(vertex1, i / 9, faceNormal, i);
    }
    if (place) {
      // Don't place a mesh if the angle is too steep.
      if (faceNormal.angleTo(up) > options.maxSlope) {
        continue;
      }
      final mesh = options.mesh.clone();
      mesh.position.add2(vertex1, vertex2).add(vertex3).divideScalar(3);
      if (options.maxTilt > 0) {
        final normal = mesh.position.clone().add(faceNormal);
        mesh.lookAt(normal);
        final tiltAngle = faceNormal.angleTo(up);
        if (tiltAngle > options.maxTilt) {
          final ratio = options.maxTilt / tiltAngle;
          mesh.rotation.x *= ratio;
          mesh.rotation.y *= ratio;
          mesh.rotation.z *= ratio;
        }
      }
      mesh.rotation.x += 90 / 180*math.pi;
      mesh.rotateY(math.Random().nextDouble()*2*math.pi);
      if (options.sizeVariance != 0) {
        final variance = math.Random().nextDouble()*doubleSizeVariance - options.sizeVariance;
        mesh.scale.x = mesh.scale.z = 1 + variance;
        mesh.scale.y += variance;
      }

      mesh.updateMatrix();
      options.scene?.add(mesh);
    }
  }

  return options.scene!;
}