The first version of this site already had 101 Indian monuments in the browser, and not one of them was a downloaded model. Each building was a small function: boxes, cylinders, a lathe for the domes, and one shader that painted stone. That story is in I Built 101 Indian Monuments in the Browser Using Only Code.
This is the second pass, with the pictures from the component guides. The masses were already right. Up close, a dome was still a bare bulb, a screen was still a square grid, and the ground around a building was often the wrong trees or the wrong water. The work since then fills in the joinery, gives a few monuments pieces that only they have, and plants each site with what actually stands there.
The pictures below are the ones in components.md. node tools/shoot-components.mjs draws each shared part alone, in plain light. On the site the same part carries shadows, a photo texture and weathering. What each function takes, and which file it lives in, is in architecture.md and realism.md.
The site is still plain files and three.js, with no build step.
Live: monuments-of-india-nu.vercel.app ยท
Code: github.com/amananandrai/monuments-of-india
If the models or the code are useful to you, or you just like them, please give a star to the Github repo and help other people find the project.
How a part gets into a building
Each monument has two files.
-
js/monuments/<name>.jsbuilds the masses: plinth, walls, towers, roofs. -
js/details/<name>.jsadds the finer architecture and the setting, fromaddDetails(f, g).
f is the group the home view frames. g is the whole site, so a river or a town can sit outside the frame and not pull the camera back.
A few rules hold for every part:
- Metres, y up. A part stands on y = 0 and faces +z unless its comment says otherwise.
-
The stone is passed in.
M.marble,M.red,M.graniteand the rest live injs/helpers.js. A one-off stone ismat('#hex', roughness). -
Merge when the part is done.
mergeByMaterial(group)leaves one mesh per material, so a facade of hundreds of small pieces is a handful of draw calls. -
Two faces never share a plane. An overlay sits 2 to 5 cm off its wall.
node check-flicker.mjs <id>fromtools/finds the ones that do not.
Placing a row of windows is a loop around the four faces:
// A window on each face, 11 m out from the centre, the sill 13.6 m up.
for (const a of FACES) {
d.add(onFace(at(windowFrame(2, 4.2, M.cream, { shape: 'flat' }), 0, 13.6, 0), a, 11.01));
}
at moves a part. onFace turns that move onto a wall: angle 0 is the +z face, the next number is how far out, and a third number walks along the face. aroundCircle and alongFace do the same for a ring of brackets or a cornice. Hundreds of identical merlons or columns go through inst(), one geometry drawn many times.
Domes
![]() |
![]() |
|---|---|
| Onion dome: neck, lotus crown, finial | Lotus dome: ribs, a cup of petals, a crescent |
onionDome(r, m) is a lathe of the shared onion profile. From 1.5 m radius upward (4 m on a phone) it adds two more lathes that ride on the same group, so moving, scaling or ribbing the dome carries them along:
export function onionDome(r, m, {
crown = r >= (detail.low ? 4 : 1.5),
neck = r >= (detail.low ? 4 : 1.5),
} = {}) {
const o = lathe(ONION.map(([a, b]) => [a * r, b * r]), m, 48);
if (neck) o.add(lathe(DOME_NECK.map(([a, b]) => [a * r, b * r]), m, 32));
if (crown) o.add(ribbed(lathe(DOME_CROWN.map(([a, b]) => [a * r, b * r]), m, 32), 16, 0.1));
return o;
}
The neck is the moulding where the bulb springs from the drum. The crown is the inverted lotus, sixteen ribs pressed in by ribbed(), which is the mahapadma the finial rises from. { crown: false } or { neck: false } leaves one off where a monument models its own. The Taj does.
Around that sit the other turned shapes: hemi for a plain hemisphere, persianDome for a pointed double curve, lotusDome for an onion with shallow ribs sitting in a cup of petals, petalRing for the petals themselves, and finial, kalash and crescentFinial for whatever crowns it. A temple pot, a Mughal spike and a crescent are three different functions, because they are three different objects.
Kiosks and turrets
![]() |
![]() |
![]() |
|---|---|---|
| Chhatri | Mughal chhatri | Guldastas |
![]() |
![]() |
|
| Minaret | Jharokha |
chhatri(r, h, m, domeM) is the domed kiosk on eight columns that stands on Mughal and Rajput roofs. At full detail each column gets a moulded base and a spreading capital, the eave gets a drip edge, and the dome sits on a low drum. mughalChhatri is the richer one: arches between the columns, a sloping eave, a ribbed lotus dome, and an optional crescent.
guldasta is the Lucknow roof turret, a thin shaft under a tiny onion and a spike. minaret tapers, takes a list of balconies, and is capped by a chhatri. jharokha is the oriel on brackets, screened on three sides, under a roof that can be curved Bengal ('bangla'), domed, or flat. banglaRoof is that curve on its own, for pavilions as well as windows.
Pillars and columns
![]() |
![]() |
![]() |
|---|---|---|
| Classical, plain and Corinthian | Carved temple pillar | Cluster pillar |
![]() |
![]() |
|
| Yali pillar | Column trim on bare shafts |
These are different orders, so they are different functions.
-
column(r, h, m, { order })injs/details/colonial.jsis a classical column: plinth, base, tapering shaft, capital.order: 'corinthian'adds flutes and acanthus.entablatureandcorinthianCapitalare what a colonnade carries above it. -
carvedPillaris a temple pillar: square base, octagonal shaft with two carved bands, a cushion and a capital. -
clusterPillaris the Vijayanagara pillar, a shaft ringed by slender colonnettes.yaliPillarcarries a rearing yali and its rider. Both are sculpted rather than stacked from boxes. -
columnTrim(list, m, { capM })dresses a colonnade that was built as bare instanced shafts: a plinth and moulding under each, a necking ring and an abacus at the head. It is on Agra Fort's Diwan-i-Am, the Padmanabhaswamy corridor, Mahakaleshwar's colonnade and the Secretariat loggias at Rashtrapati Bhavan. -
pilastersare the flat ones, proud of a wall by a few centimetres.
The trim matters because the cheap way to draw forty columns is one instanced cylinder. That reads as a row of poles. Bases and capitals are a second instanced pass, so the shafts stay cheap and the order comes back.
Arches, doors and windows
![]() |
![]() |
![]() |
|---|---|---|
| Arch and its ring | Iwan | Niche |
![]() |
![]() |
![]() |
| Window with a jali | Classical door | Gothic window |
![]() |
||
| Temple doorway |
archWall cuts an opening through a wall. archRing is the moulded band around it. niche is a blind arch with a half-dome hood, deep enough to read as a recess. iwan, in js/details/mughal.js, is the great recessed arch of a gate or a tomb front, with its door. arcadeSkin cuts a wall into a row of arched cells, each a door, a screen or a room. mughalDoorway is the smaller pointed door onto a dim interior.
The traditions that are not Mughal have their own doors, because a fanlight is not a torana.
-
templeDoorwayis a stepped frame, a hood, timber leaves standing open, a lamp in the sanctum, and guardians.doorLeavescan takespikes, the iron points set against war elephants.doorFrameis the nested carved bands of a post-and-lintel door.interioris the dim room seen through it.templePorchis the small open porch in front. -
classicalDoor,porchandpedimentare the colonial and Indo-Saracenic set.gothicDoor,gothicWindowandoculusare the pointed portal, the two-light window and the round window. -
windowFrame(w, h, m, { shape, screen })is a window in a stone surround.screen: jali(m)fills it.
The jali is the part that used to be a square grid. It is now the octagon-and-diamond lattice of Mughal and Rajput screens, one function for the cut and the shading so they cannot drift apart. A 22 cm grid:
float sJaliHole(vec2 uv) {
vec2 p = abs(fract(uv / 0.22) - 0.5);
float oct = step(max(p.x, p.y), 0.34) * step(p.x + p.y, 0.47);
float dia = step(1.0 - p.x - p.y, 0.13);
return max(oct, dia);
}
Up close, when the holes are large enough on screen not to shimmer, the shader discards the opening. The stone around it still casts the shadow. Far away the lattice stays a shading pattern. Modelling every hole on 101 buildings was never going to fit the triangle budget: 650,000 on a desktop, 300,000 on a phone.
The same trick covers the ornament that should stay cheap: frieze for a band of rosettes, carved(m, kind) for 'script', 'calligraphy', 'pietra', 'reliefs' and 'tiles', plus rosette, medallion, sunburst and lettering for the roundels and the raised names. The pattern lives in the shader. The mesh stays a wall.
Eaves, battlements and bands
![]() |
![]() |
![]() |
|---|---|---|
| Chhajja on brackets | Merlons | Railing |
chhajja is the deep sloping eave. chhajjaRing is the round version. bracketGeo is the corbel that carries an eave or a balcony, and placeAll drops one copy at every point in a list. merlons is a row of pointed battlements. On a fort, rampart and bastionCrowns add the parapet, a string course and arrow slits, for a long curtain wall and for round bastions.
railing and railingRing are posts with pierced screens between them. balustrade is the classical version. beltCourse runs a band around a rectangular building, mouldings stacks courses into a plinth or a cornice, dentils is the row of small blocks under that cornice, and panel is a sunk field that can be carved or inlaid. wallWithGaps is a compound wall with openings left for its gates.
Temple towers
![]() |
![]() |
![]() |
|---|---|---|
| Shikhara | Nagara temple | Dravida vimana |
![]() |
![]() |
![]() |
| Gopuram | Mandap | Temple niche |
nagaraTemple({ w, h, m, halls, โฆ }) builds a North Indian temple in one call: sanctum, curved spire, and halls stepping out in front. That is why Somnath, in the first article, fits under 1 KB. shikhara is the spire alone, with nagaraProfile and kalingaProfile as two tapers. bhumijaRows sets miniature spires between the bands of a Bhumija tower.
dravidaVimana is the stepped tower over a South Indian sanctum. Each storey's edge carries miniature shrines: kuta at the corners, shala along the sides, laid out by haraRow. gopuram(w, d, h, tiers, seed) is the gate tower, tier on tier of painted stucco figures. The seed is why Meenakshi's towers are not copies of each other. mandap is a pillared hall under a stepped roof. storeyedHall stacks galleries. pidaRoof is the stepped pyramid of slabs on its own.
templeNiche(w, h, m, { figure }) puts an image in a niche. For a long row, idol is the cheap standing figure in relief. trabeateGate is a post-and-lintel gatehouse, with guardians or carved beasts. dwajasthambha is the gold flagstaff in front of the sanctum. grandStair is the broad flight between side walls, with a landing and guardians.
![]() |
![]() |
![]() |
|---|---|---|
| Grand stair | Figure frieze | Statue, urn, pinnacle |
figure, guardian, statue and figureFrieze are the sculpted ones. The great statues, Patel, Bahubali, Thiruvalluvar and the Nageshwar Shiva, are each one continuous skin, grown from a signed distance field in js/sdf.js and meshed in the browser, so a limb grows out of a body the way it does in a carving.
Forts and monasteries
![]() |
![]() |
![]() |
|---|---|---|
| Fort gate | Rampart and bastion | Monks' houses |
![]() |
||
| Dharma wheel and victory banner |
fortGate is a gatehouse with a real passage, guard niches, and leaves spiked against elephants. towerDetail adds string courses and arrow slits to a round tower. slitRow is a row of slits along a wall. A gate that is only an arch painted on a wall reads as scenery. A passage you could walk reads as a fort.
For the Himalayan monasteries, tibetanBlock is the whitewashed mass with dark windows under a maroon band. quarters dresses a list of those blocks as monks' houses: windows in black surrounds, doors, beam ends, earth roofs, prayer-flag poles. dharmaWheel is the gilded wheel between two deer over an assembly hall. gyaltsen is the victory banner at a roof corner, shabkhep the pleated awning over a window, parapetDiscs the gilded discs along a parapet. tibetanDoor and tibetanPortico are the painted door and the pillared porch.
The ground under a hill monastery comes from js/details/terrain.js: heightField shapes a surface from a height function, rock where it is steep, scrub or grass where it is not, and mountainRing sets a ring of mountains around a valley.
When the shared kit is not the building
Shared parts get a monument into the right family. They do not make the Taj the Taj. The detail file is where a photograph would disagree with a generic kit.
The Taj Mahal. Each face is a pishtaq: a deep pointed arch in a rectangular frame, roofed by a half-dome, with calligraphy in black marble and inlaid flowers in the spandrels. Smaller arched alcoves with pierced screens sit either side. The parapet carries leaf-shaped merlons, the drum a blind arcade, and the four roof chhatris have arches, an eave and a lotus cap. The minarets take three bracketed balconies. The dome's own lotus is modelled here, so the shared crown stays off.
Konark. The chariot plinth is wrapped in friezes of elephants and daily life, above and below the wheels. The Jagamohana stands on carved mouldings, with an image in a niche either side of each doorway. At the steps of the Natamandira, lions rear over elephants. The horses are sculpted.
Hawa Mahal. Each jharokha of the screen projects on a pair of small carved brackets under the sill. The white lime bands along the storeys are carved. A lotus crowns each domed kiosk. Flowering bougainvillea sits at either end of the pavement. The lattice holes stay in the shader, so the stone is still solid and the screens still cast shadows.
Nageshwar's Shiva. About 30 m to the tips of the trishul, seated as Mahayogi. Matted locks, the crescent and the Ganga, the third eye, Vasuki coiled twice around the neck, rudraksha, a tiger skin and the trishul with its damaru are placed on the signed-distance body with snap(), so they neither float nor sink into the skin.
India Gate, Charminar, the Taj, the Gateway of India and the Old Parliament House carry the most of this layer: roundels, lettering, nested arches, bracketed balconies, jalis, eaves, merlons, lotus domes. The others take the carving of their own tradition. Base mouldings and image niches on the temples. Banded bastions and jharokhas on the forts. Stacked iwans and inlaid stars on the Mughal tombs. Capitals, balustrades and eaves on the colonial buildings.
What stands around the building
Everything that is not the monument comes from js/details/realism.js, with the animals in js/details/animals.js. A part takes a list of spots, [x, z] in metres. Variation comes from a seed, so a visit looks the same as the last one. copies() instances the shape and marks it as dressing, which the home view ignores when it frames the building. Fifty trees are a few draw calls.
Twenty-one kinds of tree, one planter
![]() |
![]() |
![]() |
|---|---|---|
| Broadleaf | Wide | Dry |
![]() |
![]() |
![]() |
| Columns and conifers | Palms | Shrubs |
trees(spots, { kind, seed, y, h }) plants every kind. One table, KINDS, holds the default height, the plain crown, the full crown, the leaf and the bark. An unknown kind throws, naming itself. Only the banyan drops aerial roots. Four silhouettes cover the twenty-one: a broadleaf, a wide broadleaf, a dry umbrella, a column, a tiered conifer, and the palm.
| Where | What stands there |
|---|---|
| Delhi's avenues | Jamun |
| Bodh Gaya and Sarnath | Peepal, the bodhi tree |
| Kashmir | Chinar, pale, on a thick trunk |
| Ladakh, by the Indus | Poplar and willow |
| Tawang and Rumtek | Fir |
| Ajanta, and the forests | Teak and sal |
| The south | Tamarind |
| Konark and the Shore Temple | Casuarina behind the palms |
| Jaisalmer, Chittorgarh, the dry ridges | Khejri, dhok, babul, ber |
// Twelve jamun down each side of a path. The seed keeps this avenue the same on every visit.
g.add(trees(
[...along(12, [-58, 14], [-58, 185]), ...along(12, [58, 14], [58, 185])],
{ kind: 'jamun', seed: 6 }
));
along is an avenue. scatter fills a rectangle and stays out of the building. around is a ring.
Every tree starts as a trunk and one crown. That is all a phone keeps. On a desktop, upgradeTrees() replants that monument's trees with a leaning trunk, limbs, and separate leaf clumps, smallest planting first, for as long as the model stays inside its triangle budget. Palms grow fronds with leaflets. Deodars grow drooping tiers. Six specimen trees become full trees. A forest of 160 usually stays plain. Each tree also takes one of three forms of its kind, so a row is not a row of clones.
shrubs has three readings. Plain is a clipped green bush. dry: true is the grey-green scrub of a dry hill. flowers: '#d0306f' is a dark bush with a smaller muted cap, bougainvillea or marigold, deliberately not a bush made entirely of bloom.
Lamps, flags, boats, towns
![]() |
![]() |
![]() |
|---|---|---|
| Lamps, bench, bollards | ASI board | Fountains |
![]() |
![]() |
![]() |
| Prayer flags | Tricolour and pennant | Marigold garlands |
lampPosts are fluted iron posts with a scrolled arm and a glass lantern, lit at night. asiBoard is the blue board on two posts. fountains rise and fall into a ring of foam. umbrellas are the cane and cloth shades on bamboo poles. prayerFlags run from point to point in the five colours. garland hangs marigold strands, orange and yellow. flag is a rippling pennant, saffron or the Nishan Sahib. tricolour is the national flag with the Ashoka Chakra.
![]() |
![]() |
![]() |
|---|---|---|
| Country boats | Ferry launches | Cargo ship |
![]() |
![]() |
![]() |
| Houseboats | Stalls | Town houses |
boats, launches, ships and houseboats take a water height, so a boat floats on its lake. stalls stand outside temple gates and in Laad Bazaar at Charminar. townHouses builds two to five storeys with windows, doors and shop signs. tanks is the share of flat roofs that carry a black water tank, 0.7 by default and 0.15 in the fort town at Jaisalmer. cityAround lines streets on every side of a rectangle and leaves the roads in keep open. At Puri, two rows of houses stand outside Jagannath's outer wall, and the Grand Road stays clear.
![]() |
||
|---|---|---|
| Traffic on the two bridges that have it |
traffic runs only on the Howrah Bridge and the BandraโWorli Sea Link. There are no motor vehicles anywhere else, and no people at all, so the monument stays the subject.
Animals
![]() |
![]() |
![]() |
|---|---|---|
| Nandi | Elephants | Horse and lion |
![]() |
![]() |
|
| Deer | Yaks and camels |
nandi sits before a Shiva temple. elephant can be stone or bronze. rideElephant adds cloth and a howdah. lion is a gate guardian, and the Lion Capital. deer stand at Sarnath and Sikandra. yaks are in the mountains. camels wear saddle cloths in the Thar. horse, scaled down, is the ponies at Kedarnath. The larger animals are one continuous skin, heavier than the rest of the kit, and the slowest of them are meshed while the map sits idle.
Ground, water, and the shade at a wall's foot
The first scenery was generous, and often invented. Mango and coconut stood in the desert. Pools sat where the ground is grass. A lawn ended like a tile.
Water was put back where the site has it, and taken out where it had been added. The Tungabhadra runs north of Hampi's Vittala compound, palms on its bank. A stone-rimmed lotus pond lies south of the Mahabodhi railing. The Shahi Baoli stands east of the Bara Imambara, a walled stepwell through an arched gate. Ramappa's lake lies beyond the temple garden. India Gate lost the canals at its piers. Jallianwala Bagh's flame stands on the grass. Rameswaram's sea sits east of the temple, in the town. Maota Lake at Amber mirrors the fort.
Paths are flagstones. Courses are 70 cm deep, stones are 1.1 m long, every third course is half-length, and the courses shift so no joint runs straight through. Each stone has its own tone. Far off, the joints drop away and the floor stays slightly mottled.
float row = floor(uv.y / 0.7);
float wdt = mod(row, 3.0) < 0.5 ? 0.55 : 1.1;
float xo = uv.x + row * 0.37;
A wall meets the grass in shade, not in a hard line. When a monument is shown, it is drawn once from straight above into a 512ร512 height map (groundShade in js/surface.js). Upward faces darken where that blur stands well above them. Trees are left out of the pass, because they already cast shadows. It runs on phones too.
Ceremonial lawns stay mown: India Gate, the Taj, Victoria, the National War Memorial, both Parliaments. Elsewhere, desktops get grass tufts under about 0.35 m, kept 4 m clear of the monument and off any path.
Close to a building, the camera casts a ray toward its target. If it has entered a solid it steps back out, and it keeps about 1.5 m off a wall in front of it. The home view frames the monument, not the lawns around it, and fifteen monuments open on the view of their best-known photograph: the Taj down its garden, the Red Fort's Lahori Gate, Meenakshi's south gopuram, Hawa Mahal's street front.
Stone that catches the light
The shader still paints joints, veins, brick courses, weathering and a slight tint per piece, from world position, so nothing has to be unwrapped. A one-metre block is the same size on the Qutub Minar as on the walls of Agra Fort. Over that, most stones carry a CC0 photograph from Poly Haven: marble, red and cream sandstone, granite, laterite, dark coursed blocks, fort rubble, plaster, lime, khondalite, concrete and weathered planks.
Each set has a normal map and a roughness map beside the colour, packed by tools/pack-textures.py. On a desktop the shader projects them the same way as the colour, so the stone catches the light as a rough surface. Phones keep the colour map. A set is fetched the first time a monument that uses it is opened, not all of them at startup.
Shadows follow the view. The shadow map used to cover a fixed box around the whole monument, so on a large site one texel was most of a metre. It now stays about three-quarters of the camera's distance from its target, changes in steps so the shadows do not crawl, and is redrawn only when a step is crossed. Desktops start at two device pixels per CSS pixel and step down for that visit if frames come in slower than about 40 a second.
On a phone, below 760 px wide, ambient occlusion and reflections are skipped. Trees stay plain, the ground has no tufts, towns and traffic are thinned, and the heavier carving waits. ?detail=high or ?detail=low on the address chooses.
What I would still not claim
These are interpretations, not survey records. The goal is that someone who knows the building can see its plan, its material and its setting, and that someone who does not can tell a gopuram from a shikhara, and Konark from a generic temple.
The rule from the second pass is small. Repeated joinery lives in one shared part, which is why the pictures above are a catalogue and not 101 separate models. The thing that makes a monument itself lives in that monument's file. The tree and the water are the ones the site's own notes name. A new dome profile does not need a new texture unwrap. A new tree is one row in KINDS.
Adding another monument is still one module and one card in js/data.js.
- Explore: monuments-of-india-nu.vercel.app. Open
#taj-mahal, then#konark-sun-temple,#hawa-mahaland#jaisalmer-fort. - The guides, with every part: components, architecture, the setting, how it is drawn.
- If you found this helpful, or you like the project, star the repo on GitHub.
Which building still looks least like itself? That is the next detail file.
This article was written with AI assistance from the project's source and is disclosed as fully autonomous: it was published without a human rewrite. The component pictures are rendered from the repo by tools/shoot-components.mjs. three.js is MIT. Photo textures are CC0 from Poly Haven. Map data is public domain from Natural Earth.































































Top comments (1)