1.4K tris8.00 × 1.81 × 6.34 m
Use it in code
Remix in the editorsand-dune.js · 185 lines
Self-contained ES module — only depends on three. Call the factory and add the group to your scene.
/**
* Sand Dune — Codels 3D model library
* License: CC0-1.0 · Units: meters · Y-up · Front: +Z · Origin: center of the base (y = 0)
* Triangles: 1,412 · Size (W × H × D): 8.00 × 1.81 × 6.34 m
*
* Usage:
* import { createSandDune } from './sand-dune.js';
*
* const model = createSandDune();
* 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);
}
// Extrude a 2D outline along Z (centered). bevel = [thickness, size, segments].
function extrude(outline, depth, holes = [], bevel = null) {
const shape = new THREE.Shape(outline.map(([x, y]) => new THREE.Vector2(x, y)));
for (const hole of holes) shape.holes.push(new THREE.Path(hole.map(([x, y]) => new THREE.Vector2(x, y))));
const options = bevel
? { depth, bevelEnabled: true, bevelThickness: bevel[0], bevelSize: bevel[1], bevelSegments: bevel[2] }
: { depth, bevelEnabled: false };
return new THREE.ExtrudeGeometry(shape, options).translate(0, 0, -depth / 2);
}
// Tube along a smooth curve through the given points.
function tube(path, radius, tubularSegments, radialSegments, closed = false) {
const curve = new THREE.CatmullRomCurve3(path.map(([x, y, z]) => new THREE.Vector3(x, y, z)), closed);
return new THREE.TubeGeometry(curve, tubularSegments, radius, radialSegments, closed);
}
export function createSandDune() {
const root = new THREE.Group();
root.name = 'SandDune';
// Materials
const sandMat = new THREE.MeshStandardMaterial({ name: 'sand', color: 0xd39d52, roughness: 0.95, flatShading: true, side: THREE.DoubleSide });
const slipMat = new THREE.MeshStandardMaterial({ name: 'slip', color: 0xad8143, roughness: 0.95, flatShading: true, side: THREE.DoubleSide });
const rippleMat = new THREE.MeshStandardMaterial({ name: 'ripple', color: 0xddb378, roughness: 0.95, flatShading: true });
const stoneMat = new THREE.MeshStandardMaterial({ name: 'stone', color: 0xc9bba3, roughness: 0.9, flatShading: true });
const shrubMat = new THREE.MeshStandardMaterial({ name: 'shrub', color: 0x8a7a4a, roughness: 0.95, flatShading: true });
// Parts
const dune = new THREE.Group();
dune.name = 'dune';
root.add(dune);
const crest = new THREE.Mesh(extrude([[4.14, 0], [3.312, 0.234], [2.484, 0.576], [1.656, 0.954], [0.828, 1.368], [0, 1.8], [-0.342, 1.368], [-0.684, 0.954], [-1.026, 0.576], [-1.368, 0.234], [-1.71, 0]], 2), sandMat);
crest.name = 'crest';
crest.position.set(-0.21, 0, 0);
crest.rotation.set(0, deg(-90), 0);
dune.add(crest);
const windwardRight = new THREE.Mesh(lathe([[1, 0], [0.8, 0.13], [0.6, 0.32], [0.4, 0.53], [0.2, 0.76], [0, 1]], 7, 0, Math.PI / 2), sandMat);
windwardRight.name = 'windwardRight';
windwardRight.position.set(0.79, 0, 0);
windwardRight.scale.set(2.58, 1.8, 4.14);
dune.add(windwardRight);
const slipRight = new THREE.Mesh(lathe([[1, 0], [0.8, 0.13], [0.6, 0.32], [0.4, 0.53], [0.2, 0.76], [0, 1]], 7, Math.PI / 2, Math.PI / 2), slipMat);
slipRight.name = 'slipRight';
slipRight.position.set(0.79, 0, 0);
slipRight.scale.set(2.58, 1.8, 1.71);
dune.add(slipRight);
const slipLeft = new THREE.Mesh(lathe([[1, 0], [0.8, 0.13], [0.6, 0.32], [0.4, 0.53], [0.2, 0.76], [0, 1]], 7, Math.PI, Math.PI / 2), slipMat);
slipLeft.name = 'slipLeft';
slipLeft.position.set(-1.21, 0, 0);
slipLeft.scale.set(3.42, 1.8, 1.71);
dune.add(slipLeft);
const windwardLeft = new THREE.Mesh(lathe([[1, 0], [0.8, 0.13], [0.6, 0.32], [0.4, 0.53], [0.2, 0.76], [0, 1]], 7, Math.PI * 1.5, Math.PI / 2), sandMat);
windwardLeft.name = 'windwardLeft';
windwardLeft.position.set(-1.21, 0, 0);
windwardLeft.scale.set(3.42, 1.8, 4.14);
dune.add(windwardLeft);
const ripples = new THREE.Group();
ripples.name = 'ripples';
root.add(ripples);
const ripple1 = new THREE.Mesh(tube([[-2.4071, 0.2965, 2.8321], [-1.8707, 0.2585, 3.174], [-1.2652, 0.2762, 3.2298], [-0.9792, 0.3236, 3.1156], [-0.7252, 0.3313, 3.0969], [-0.4711, 0.288, 3.2018], [-0.2171, 0.2571, 3.2766], [0.0369, 0.2839, 3.2117], [0.291, 0.3291, 3.1023], [0.545, 0.3265, 3.1085], [0.8062, 0.2799, 3.2213], [1.2636, 0.2576, 3.186], [1.6735, 0.2923, 2.8594]], 0.034, 24, 3), rippleMat);
ripple1.name = 'ripple1';
ripples.add(ripple1);
const ripple2 = new THREE.Mesh(tube([[-2.0164, 0.4841, 2.5465], [-1.5304, 0.5209, 2.6093], [-1.1304, 0.5454, 2.5786], [-0.8849, 0.5217, 2.636], [-0.6395, 0.4845, 2.7261], [-0.394, 0.4883, 2.7169], [-0.1486, 0.5276, 2.6218], [0.0969, 0.5448, 2.5802], [0.3423, 0.5147, 2.653], [0.5877, 0.4814, 2.7336], [0.8548, 0.4937, 2.7019], [1.2029, 0.5335, 2.5219], [1.5274, 0.5426, 2.2989]], 0.034, 24, 3), rippleMat);
ripple2.name = 'ripple2';
ripples.add(ripple2);
const ripple3 = new THREE.Mesh(tube([[-2.4292, 0.7544, 1.5105], [-2.061, 0.7495, 1.856], [-1.6414, 0.7138, 2.1379], [-1.1791, 0.6997, 2.2317], [-0.879, 0.7278, 2.1702], [-0.5789, 0.7569, 2.1063], [-0.2789, 0.7444, 2.1337], [0.0212, 0.7086, 2.2121], [0.3213, 0.7019, 2.2268], [0.6214, 0.7342, 2.1562], [0.9431, 0.7581, 2.0893], [1.2912, 0.7387, 1.9899], [1.6292, 0.7043, 1.7669]], 0.034, 24, 3), rippleMat);
ripple3.name = 'ripple3';
ripples.add(ripple3);
const ripple4 = new THREE.Mesh(tube([[-1.928, 0.9441, 1.4567], [-1.6074, 0.922, 1.677], [-1.2395, 0.9329, 1.7205], [-0.9427, 0.9608, 1.6598], [-0.6522, 0.965, 1.6509], [-0.3616, 0.9391, 1.7072], [-0.0711, 0.9213, 1.7463], [0.2195, 0.9375, 1.7108], [0.51, 0.9641, 1.6528], [0.7997, 0.9619, 1.6572], [1.0735, 0.9344, 1.6563], [1.3367, 0.9217, 1.509], [1.539, 0.9424, 1.2023]], 0.034, 24, 3), rippleMat);
ripple4.name = 'ripple4';
ripples.add(ripple4);
const ripple5 = new THREE.Mesh(tube([[-1.7185, 1.1443, 1.1363], [-1.4656, 1.1656, 1.2108], [-1.2139, 1.1794, 1.2222], [-0.9376, 1.1655, 1.25], [-0.6609, 1.1443, 1.2925], [-0.3842, 1.1468, 1.2873], [-0.1075, 1.1694, 1.2421], [0.1693, 1.1789, 1.2231], [0.446, 1.1614, 1.2581], [0.7227, 1.1426, 1.2959], [0.938, 1.15, 1.2589], [1.1129, 1.1728, 1.1215], [1.2698, 1.1776, 0.9539]], 0.034, 24, 3), rippleMat);
ripple5.name = 'ripple5';
ripples.add(ripple5);
const ripple6 = new THREE.Mesh(tube([[-1.6709, 1.3879, 0.5819], [-1.5452, 1.3855, 0.7019], [-1.4045, 1.3702, 0.807], [-1.2408, 1.3645, 0.8512], [-0.9892, 1.3764, 0.8282], [-0.7194, 1.389, 0.8041], [-0.4495, 1.3833, 0.815], [-0.1797, 1.368, 0.8449], [0.0902, 1.3655, 0.85], [0.3601, 1.3792, 0.8228], [0.6299, 1.3894, 0.8032], [0.8399, 1.3807, 0.816], [0.9653, 1.3663, 0.8005]], 0.034, 24, 3), rippleMat);
ripple6.name = 'ripple6';
ripples.add(ripple6);
const dressing = new THREE.Group();
dressing.name = 'dressing';
root.add(dressing);
const stone1 = new THREE.Mesh(jitter(new THREE.DodecahedronGeometry(0.125, 0), 0.0225, 507), stoneMat);
stone1.name = 'stone1';
stone1.position.set(-1.6685, 0.0691, 4.2743);
stone1.rotation.set(deg(-4.5568), deg(46.5823), deg(-7.6821));
stone1.scale.set(1.3268, 0.532, 1);
dressing.add(stone1);
const stone2 = new THREE.Mesh(jitter(new THREE.DodecahedronGeometry(0.1118, 0), 0.0201, 904), stoneMat);
stone2.name = 'stone2';
stone2.position.set(-0.3578, 0.0465, 4.4423);
stone2.rotation.set(deg(-6.581), deg(205.1429), deg(4.4751));
stone2.scale.set(1.3173, 0.4891, 1);
dressing.add(stone2);
const stone3 = new THREE.Mesh(jitter(new THREE.DodecahedronGeometry(0.1435, 0), 0.0258, 582), stoneMat);
stone3.name = 'stone3';
stone3.position.set(0.7597, 0.0779, 4.319);
stone3.rotation.set(deg(-0.6636), deg(209.5477), deg(-6.517));
stone3.scale.set(1.2513, 0.516, 1);
dressing.add(stone3);
const dryShrub = new THREE.Group();
dryShrub.name = 'dryShrub';
dryShrub.position.set(-0.835, 0.0244, 4.476);
dressing.add(dryShrub);
const cylinder19 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2617, 3), shrubMat);
cylinder19.position.set(0.0762, 0.1248, 0.0098);
cylinder19.rotation.set(deg(3.6668), deg(-0.9753), deg(-29.7801));
dryShrub.add(cylinder19);
const cylinder20 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2777, 3), shrubMat);
cylinder20.position.set(0.0371, 0.1248, 0.0673);
cylinder20.rotation.set(deg(28.3098), deg(-3.8414), deg(-15.1481));
dryShrub.add(cylinder20);
const cylinder21 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2717, 3), shrubMat);
cylinder21.position.set(-0.0323, 0.1248, 0.0697);
cylinder21.rotation.set(deg(35.8316), deg(5.1215), deg(15.7521));
dryShrub.add(cylinder21);
const cylinder22 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2964, 3), shrubMat);
cylinder22.position.set(-0.0738, 0.1248, 0.0211);
cylinder22.rotation.set(deg(9.9572), deg(3.3533), deg(37.1467));
dryShrub.add(cylinder22);
const cylinder23 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.3531, 3), shrubMat);
cylinder23.position.set(-0.0706, 0.1248, -0.0302);
cylinder23.rotation.set(deg(-16.6708), deg(-6.4688), deg(42.1833));
dryShrub.add(cylinder23);
const cylinder24 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2783, 3), shrubMat);
cylinder24.position.set(-0.0361, 0.1248, -0.0678);
cylinder24.rotation.set(deg(-37.1324), deg(-6.3592), deg(18.7825));
dryShrub.add(cylinder24);
const cylinder25 = new THREE.Mesh(new THREE.CylinderGeometry(0.0077, 0.0154, 0.2485, 3), shrubMat);
cylinder25.position.set(0.0286, 0.1248, -0.0713);
cylinder25.rotation.set(deg(-34.9331), deg(4.2008), deg(-13.2964));
dryShrub.add(cylinder25);
const dodecahedron26 = new THREE.Mesh(new THREE.DodecahedronGeometry(0.0672, 0), shrubMat);
dodecahedron26.position.set(0, 0.0384, 0);
dryShrub.add(dodecahedron26);
root.traverse((o) => {
if (o.isMesh) {
o.castShadow = true;
o.receiveShadow = true;
}
});
return root;
}
export default createSandDune;
Ratings & comments
0 comments
No comments yet. Share how you used this model or what could be better.
Related models
Same category or similar tags









