# Stalagmite Cluster

> A cluster of low-poly cave stalagmites rising from a mound of flowstone: tapered limestone spires ringed with drip bulges and pale wet tips, the tallest in the middle with smaller ones around it and a few nubs at the base. Height, number of spires, rock color and seed are adjustable for caves, mines, dungeons and underground levels.

![Stalagmite Cluster preview](https://codels.download/m/stalagmite.png)

Multi-view sheet (3/4, front, side, top): https://codels.download/m/stalagmite.sheet.png

## Facts

- Category: Nature › Rocks & Cliffs
- Size (W × H × D): 2.05 × 2.23 × 1.78 m — units are meters, +Y up, front faces +Z
- Origin: center of the base (y = 0)
- Triangles: 1,008 · Meshes: 15 · Materials: 4
- License: CC0-1.0 (Codels built-in)
- Tags: rock, stalagmite, low-poly, stylized, fantasy, mining, cave, dungeon, adventure, roguelike, rpg, background, decoration, obstacle, stone, cluster, tall, game-ready, mobile-friendly, parametric, color-gray, color-orange, size-medium, poly-low
- Page: https://codels.download/models/stalagmite

## Files

| Format | URL |
|---|---|
| three.js ES module (`createStalagmite()`) | https://codels.download/m/stalagmite.js |
| React Three Fiber component (`<Stalagmite />`) | https://codels.download/m/stalagmite.jsx |
| Instancing module (`createStalagmiteInstances(count, place)`, one draw call per material) | https://codels.download/m/stalagmite.instanced.js |
| Standalone HTML demo | https://codels.download/m/stalagmite.html |
| glTF binary | https://codels.download/m/stalagmite.glb |
| glTF binary, Meshopt-compressed (2–3× smaller; GLTFLoader needs `setMeshoptDecoder(MeshoptDecoder)`) | https://codels.download/m/stalagmite.glb?compress=meshopt |
| glTF JSON | https://codels.download/m/stalagmite.gltf |
| OBJ (vertex colors) + MTL | https://codels.download/m/stalagmite.obj · https://codels.download/m/stalagmite.mtl |
| STL | https://codels.download/m/stalagmite.stl |
| CodelSpec JSON | https://codels.download/m/stalagmite.json |

## Parameters

Append them as query parameters to any URL above to get a customized variant (e.g. `?height=...`).

| Param | Type | Range / options | Default | Current |
|---|---|---|---|---|
| `height` | number | 0.8 – 4 | 2.2 | 2.2 |
| `count` | int | 1 – 7 | 5 | 5 |
| `color` | color | #rrggbb | #b09a7e | #b09a7e |
| `seed` | int | 1 – 30 | 3 | 3 |

## Parts

- cluster (group)
  - spire1 (group)
    - body (lathe · rock)
    - tip (lathe · tip)
  - spire2 (group)
    - body (lathe · rockAlt)
    - tip (lathe · tip)
  - spire3 (group)
    - body (lathe · rock)
    - tip (lathe · tip)
  - spire4 (group)
    - body (lathe · rockAlt)
    - tip (lathe · tip)
  - spire5 (group)
    - body (lathe · rock)
    - tip (lathe · tip)
  - flowstone (sphere · rockDark)
  - nub1 (cone · rockAlt)
  - nub2 (cone · rockAlt)
  - nub3 (cone · rockAlt)
  - nub4 (cone · rockAlt)

## How to use

Save the code below as `stalagmite.js`, then:

```js
import { createStalagmite } from './stalagmite.js';
const model = createStalagmite();
scene.add(model);
```

## Code

```js
/**
 * Stalagmite Cluster — Codels 3D model library (https://codels.download/models/stalagmite)
 * License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
 * Triangles: 1,008 · Size (W × H × D): 2.05 × 2.23 × 1.78 m
 *
 * Usage:
 *   import { createStalagmite } from './stalagmite.js';
 *   
 *   const model = createStalagmite();
 *   model.position.set(0, 0, 0);
 *   scene.add(model);
 */
import * as THREE from 'three';

const deg = THREE.MathUtils.degToRad;

// Deterministic vertex displacement (organic low-poly look).
function jitter(geometry, amount, seed) {
  const pos = geometry.attributes.position;
  const h = (a, b, c, s) => {
    let x = Math.imul(a, 73856093) ^ Math.imul(b, 19349663) ^ Math.imul(c, 83492791) ^ Math.imul(s, 2654435761);
    x = Math.imul(x ^ (x >>> 16), 2246822507);
    x = Math.imul(x ^ (x >>> 13), 3266489909);
    return ((x ^ (x >>> 16)) >>> 0) / 4294967295 - 0.5;
  };
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
    const qx = Math.round(x * 1000), qy = Math.round(y * 1000), qz = Math.round(z * 1000);
    pos.setXYZ(i, x + h(qx, qy, qz, seed) * 2 * amount, y + h(qx, qy, qz, seed + 1) * 2 * amount, z + h(qx, qy, qz, seed + 2) * 2 * amount);
  }
  pos.needsUpdate = true;
  geometry.computeVertexNormals();
  return geometry;
}

// Revolve a 2D profile [[radius, y], ...] around the Y axis.
function lathe(points, segments, phiStart = 0, phiLength = Math.PI * 2) {
  return new THREE.LatheGeometry(points.map(([x, y]) => new THREE.Vector2(x, y)), segments, phiStart, phiLength);
}

export function createStalagmite() {
  const root = new THREE.Group();
  root.name = 'Stalagmite';

  // Materials
  const rockMat = new THREE.MeshStandardMaterial({ name: 'rock', color: 0xb09a7e, roughness: 0.85, flatShading: true });
  const rockAltMat = new THREE.MeshStandardMaterial({ name: 'rockAlt', color: 0x8a7e74, roughness: 0.9, flatShading: true });
  const rockDarkMat = new THREE.MeshStandardMaterial({ name: 'rockDark', color: 0x655c51, roughness: 0.95, flatShading: true });
  const tipMat = new THREE.MeshStandardMaterial({ name: 'tip', color: 0xc5bcb2, roughness: 0.35, flatShading: true });

  // Parts
  const cluster = new THREE.Group();
  cluster.name = 'cluster';
  cluster.position.set(0, 0.0651, 0);
  root.add(cluster);
  const spire1 = new THREE.Group();
  spire1.name = 'spire1';
  spire1.position.set(0, -0.0351, 0);
  spire1.rotation.set(deg(0.3132), deg(104.4596), deg(2.4458));
  cluster.add(spire1);
  const body = new THREE.Mesh(jitter(lathe([[0, 0], [0.4387, 0], [0.3948, 0.11], [0.3244, 0.3394], [0.2634, 0.5689], [0.2541, 0.7983], [0.2067, 1.0277], [0.1401, 1.2571], [0.1079, 1.4866], [0.0771, 1.716]], 7), 0.0219, 39), rockMat);
  body.name = 'body';
  spire1.add(body);
  const tip = new THREE.Mesh(lathe([[0, 1.705], [0.0834, 1.705], [0.0746, 1.826], [0.0439, 2.046], [0, 2.2]], 7), tipMat);
  tip.name = 'tip';
  spire1.add(tip);
  const spire2 = new THREE.Group();
  spire2.name = 'spire2';
  spire2.position.set(-0.6572, -0.0186, -0.1027);
  spire2.rotation.set(deg(-1.2191), deg(265.2578), deg(7.799));
  cluster.add(spire2);
  const body2 = new THREE.Mesh(jitter(lathe([[0, 0], [0.2329, 0], [0.2096, 0.0574], [0.164, 0.1772], [0.146, 0.297], [0.1387, 0.4167], [0.1043, 0.5365], [0.0737, 0.6563], [0.0606, 0.776], [0.0404, 0.8958]], 7), 0.0116, 40), rockAltMat);
  body2.name = 'body';
  spire2.add(body2);
  const tip2 = new THREE.Mesh(lathe([[0, 0.8901], [0.0443, 0.8901], [0.0396, 0.9532], [0.0233, 1.0681], [0, 1.1485]], 7), tipMat);
  tip2.name = 'tip';
  spire2.add(tip2);
  const spire3 = new THREE.Group();
  spire3.name = 'spire3';
  spire3.position.set(0.755, -0.02, -0.1099);
  spire3.rotation.set(deg(-1.1902), deg(305.4421), deg(-8.1752));
  cluster.add(spire3);
  const body3 = new THREE.Mesh(jitter(lathe([[0, 0], [0.2495, 0], [0.2245, 0.0635], [0.1858, 0.1958], [0.1493, 0.3281], [0.1438, 0.4605], [0.1182, 0.5928], [0.08, 0.7251], [0.0609, 0.8575], [0.0438, 0.9898]], 7), 0.0125, 41), rockMat);
  body3.name = 'body';
  spire3.add(body3);
  const tip3 = new THREE.Mesh(lathe([[0, 0.9835], [0.0474, 0.9835], [0.0424, 1.0533], [0.0249, 1.1802], [0, 1.269]], 7), tipMat);
  tip3.name = 'tip';
  spire3.add(tip3);
  const spire4 = new THREE.Group();
  spire4.name = 'spire4';
  spire4.position.set(-0.787, -0.0137, -0.1521);
  spire4.rotation.set(deg(-1.8478), deg(176.5189), deg(9.5602));
  cluster.add(spire4);
  const body4 = new THREE.Mesh(jitter(lathe([[0, 0], [0.1712, 0], [0.1541, 0.042], [0.1322, 0.1295], [0.1016, 0.217], [0.0955, 0.3046], [0.0827, 0.3921], [0.0564, 0.4796], [0.0404, 0.5672], [0.0297, 0.6547]], 7), 0.0086, 42), rockAltMat);
  body4.name = 'body';
  spire4.add(body4);
  const tip4 = new THREE.Mesh(lathe([[0, 0.6505], [0.0325, 0.6505], [0.0291, 0.6967], [0.0171, 0.7806], [0, 0.8394]], 7), tipMat);
  tip4.name = 'tip';
  spire4.add(tip4);
  const spire5 = new THREE.Group();
  spire5.name = 'spire5';
  spire5.position.set(0.8594, -0.0153, 0.126);
  spire5.rotation.set(deg(1.4017), deg(121.4567), deg(-9.5564));
  cluster.add(spire5);
  const body5 = new THREE.Mesh(jitter(lathe([[0, 0], [0.1914, 0], [0.1723, 0.047], [0.1419, 0.1449], [0.1361, 0.2429], [0.1035, 0.3409], [0.0782, 0.4388], [0.0696, 0.5368], [0.0514, 0.6348], [0.0286, 0.7327]], 7), 0.0096, 43), rockMat);
  body5.name = 'body';
  spire5.add(body5);
  const tip5 = new THREE.Mesh(lathe([[0, 0.728], [0.0364, 0.728], [0.0325, 0.7797], [0.0191, 0.8736], [0, 0.9394]], 7), tipMat);
  tip5.name = 'tip';
  spire5.add(tip5);
  const flowstone = new THREE.Mesh(jitter(new THREE.SphereGeometry(1.007, 10, 3, 0, Math.PI * 2, 0, Math.PI / 2), 0.0604, 3), rockDarkMat);
  flowstone.name = 'flowstone';
  flowstone.scale.set(1, 0.3496, 0.9);
  cluster.add(flowstone);
  const nub1 = new THREE.Mesh(jitter(new THREE.ConeGeometry(0.1134, 0.2519, 6), 0.0126, 87), rockAltMat);
  nub1.name = 'nub1';
  nub1.position.set(0.5438, 0.126, -0.7357);
  nub1.rotation.set(deg(-4.8505), deg(145.322), deg(4.2362));
  cluster.add(nub1);
  const nub2 = new THREE.Mesh(jitter(new THREE.ConeGeometry(0.1043, 0.2319, 6), 0.0116, 88), rockAltMat);
  nub2.name = 'nub2';
  nub2.position.set(-0.7489, 0.116, -0.3439);
  nub2.rotation.set(deg(8.3396), deg(27.0106), deg(-7.3671));
  cluster.add(nub2);
  const nub3 = new THREE.Mesh(jitter(new THREE.ConeGeometry(0.1147, 0.2548, 6), 0.0127, 89), rockAltMat);
  nub3.name = 'nub3';
  nub3.position.set(-0.7039, 0.1274, -0.4375);
  nub3.rotation.set(deg(7.6159), deg(215.2691), deg(4.8426));
  cluster.add(nub3);
  const nub4 = new THREE.Mesh(jitter(new THREE.ConeGeometry(0.0726, 0.1614, 6), 0.0081, 90), rockAltMat);
  nub4.name = 'nub4';
  nub4.position.set(0.8483, 0.0807, -0.2927);
  nub4.rotation.set(deg(-5.0622), deg(244.7684), deg(-0.1702));
  cluster.add(nub4);

  root.traverse((o) => {
    if (o.isMesh) {
      o.castShadow = true;
      o.receiveShadow = true;
    }
  });
  return root;
}

export default createStalagmite;
```

---
Codels — 3D models as code for three.js. Search more: https://codels.download/api/v1/models?q=<query> · Guide: https://codels.download/llms.txt
