DEV Community

Cover image for Monuments of India, Part 2: Domes, Arches, Realistic Stone and Finer Details in three.js
amananandrai
amananandrai

Posted on Edited on AI-assisted

Monuments of India, Part 2: Domes, Arches, Realistic Stone and Finer Details in three.js

Zero 3D models used for 101 pure-code monuments

Last time, I proudly showed you 101 Indian monuments built entirely from code. This time I have to confess something: some of them were, let's say, enthusiastic approximations. That story is in I Built 101 Indian Monuments in the Browser Using Only Code.

Qutub Minar was wearing a cupola it never owned. The Taj Mahal had a lovely tomb but no actual garden. Dilwara had one temple where there should have been five. And a few of my buildings had a gorgeous front and, behind it, a back that looked like I'd lost interest halfway.

So I went on a fixing spree. Real doorways and guard niches on the forts. A proper charbagh for the Taj. Backs, sides and windows for the palaces. Towns that crowd around the temples like they've been there for centuries. Stone that catches the light, with veining, weathering and pietra dura. And yes, trees: the right trees. No more mango trees in the Thar Desert. Khejri and babul have been called in.

It is still 101 monuments in three.js, still pure code, and still not a single 3D model file downloaded. Come see what got more real, and help me spot what still looks like it's fibbing about its age.

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.

Procedural Taj Mahal in three.js, seen from the garden: white marble tomb, four minarets and the reflecting pool

TL;DR

  • The masses were already right. The joinery was not. Domes, screens, doors, backs of buildings and the ground around them got a second pass, monument by monument.
  • Shared parts for the family, detail files for the building. One dome, one jali, one gopuram for everyone, plus a file per monument for what only that monument has.
  • Everything is still code. Plain files and three.js, no build step, no model files, 650,000 triangles at most on a desktop and 300,000 on a phone.

In this post

  1. What changed since part 1
  2. How a part gets into a building
  3. Domes · Kiosks and turrets · Pillars and columns
  4. Arches, doors and windows · Eaves, battlements and bands
  5. Temple towers · Forts and monasteries
  6. When the shared kit is not the building
  7. What stands around the building
  8. Stone that catches the light
  9. How I checked the work, and what went wrong
  10. What I would still not claim · Add your own monument

What changed since part 1

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.

Here is the short version, in the order I did it:

  • Real entrances. Forts got passages, guard niches and doors spiked against elephants. Temples got stepped doorways and porches. Mughal tombs got iwans and arcades.
  • A detail file for every monument, so a building has its own finer architecture and its own setting, not just a generic kit.
  • Photograph reviews. I compared the models with photographs in batches and fixed what disagreed: the Taj, Charminar, the Lotus Temple, the Hawa Mahal and many more.
  • Corrections. Qutub Minar lost the cupola it never had and gained Smith's Folly. The Taj Mahal got its full four-quarter charbagh. Dilwara got its other four temples.
  • Backs, sides and windows for palaces, mosques and tombs, so you can orbit them without finding a blank wall.
  • Towns and water around the temples at Kedarnath, Tirumala, Trimbak, Varanasi and Ujjain, plus the Old City round Charminar.
  • The right trees and the right water, per site.
  • Surfaces: photo textures with normal and roughness maps, calligraphy, pietra dura, carved reliefs and banded masonry.
  • Performance: monuments load when you open them, shadows follow the view, and phones get a lighter build.

Before and after

Same monument, same site, rendered by the same code: the left side is the repository before the second pass began, the right side is today.

Qutub Minar. The old tower wore a cupola it never owned. Now it tapers to its real top, and Smith's Folly, the Alai Darwaza, the cloister and Iltutmish's tomb stand around it, on grass with a path and trees.

Qutub Minar before and after: the invented cupola is gone, the tower is corrected and the Qutub complex stands around it

Taj Mahal. The old tomb stood alone on a red plinth. Now it has its pishtaqs, calligraphy, chhatris and a charbagh garden to walk down.

Taj Mahal before and after: the bare tomb versus the tomb with its charbagh garden, cypresses and gateway pavilions

Dilwara. One temple became the whole group of five, each in its own walled court, among trees.

Dilwara Jain temples before and after: one temple versus the group of five

Jaisalmer. A smooth sandstone mound with a ring wall became a fort at its real size on its Trikuta hill, with bastions, the road up to the gate and a town inside the walls.

Jaisalmer Fort before and after: a plain mound with a ring wall versus the fort on its hill with bastions and gates

Hawa Mahal. A flat stepped front with no depth became a screen of carved jharokhas, with the court and ranges behind it, the street in front and the building's sides finished.

Hawa Mahal before and after: a plain stepped front versus the screen of jharokhas with its court and wings

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.

Finished Indian monuments in the browser: Meenakshi Temple gopurams


How a part gets into a building

Each monument has two files.

  • js/monuments/<name>.js builds the masses: plinth, walls, towers, roofs.
  • js/details/<name>.js adds the finer architecture and the setting, from addDetails(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.granite and the rest live in js/helpers.js. A one-off stone is mat('#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> from tools/ 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));
}
Enter fullscreen mode Exit fullscreen mode

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.

Pipeline: monument file, detail file and shared parts are merged by material, drawn by the surface shader and shown in the browser


Domes

A dome is rarely just a dome. The bulb springs from a drum through a moulded neck, and the finial that crowns it tells you the tradition: a Mughal spike, a temple pot, a crescent.

Onion dome with a moulded neck, an inverted lotus crown and a finial Lotus dome: shallow ribs, a cup of petals, and a crescent finial
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;
}
Enter fullscreen mode Exit fullscreen mode

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

The chhatri, a small domed kiosk on columns, is the silhouette of Rajput and Mughal skylines. Take them off a roofline and it stops looking like India.

Chhatri: eight columns, a drip-edge eave and a dome on a low drum Mughal chhatri with arches between the columns and a ribbed lotus dome Guldastas: slender Lucknow roof turrets with a tiny onion and a spike
Chhatri Mughal chhatri Guldastas
Minaret with balconies and a chhatri cap Jharokha: an oriel on brackets, screened on three sides
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 columns, one plain and one Corinthian with flutes and acanthus Carved temple pillar: square base, octagonal shaft, two carved bands Cluster pillar: a shaft ringed by slender colonnettes
Classical, plain and Corinthian Carved temple pillar Cluster pillar
Yali pillar with a rearing beast and its rider Column trim: plinth, moulding, necking ring and abacus on bare shafts
Yali pillar Column trim on bare shafts

These are different orders, so they are different functions.

  • column(r, h, m, { order }) in js/details/colonial.js is a classical column: plinth, base, tapering shaft, capital. order: 'corinthian' adds flutes and acanthus. entablature and corinthianCapital are what a colonnade carries above it.
  • carvedPillar is a temple pillar: square base, octagonal shaft with two carved bands, a cushion and a capital.
  • clusterPillar is the Vijayanagara pillar, a shaft ringed by slender colonnettes. yaliPillar carries 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.
  • pilasters are 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.

Forty columns as forty meshes versus one instanced geometry


Arches, doors and windows

A wall with an arch cut through it and a moulded ring Iwan: a great recessed arch with its door An arched niche with a half-dome hood
Arch and its ring Iwan Niche
Window in a stone surround, filled with a jali screen Classical door in a pedimented surround Gothic two-light window
Window with a jali Classical door Gothic window
Temple doorway: stepped frame, open leaves, a lamp in the sanctum
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.

  • templeDoorway is a stepped frame, a hood, timber leaves standing open, a lamp in the sanctum, and guardians. doorLeaves can take spikes, the iron points set against war elephants. doorFrame is the nested carved bands of a post-and-lintel door. interior is the dim room seen through it. templePorch is the small open porch in front.
  • classicalDoor, porch and pediment are the colonial and Indo-Saracenic set. gothicDoor, gothicWindow and oculus are 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, from a grid to a lattice

A jali is a pierced stone screen. It lets air and light through while keeping a room private, and it is one of the most recognisable things in Mughal and Rajput architecture. It is also 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);
}
Enter fullscreen mode Exit fullscreen mode

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 jali: a plain square grid, the octagon-and-diamond lattice, and the lattice cut into a stone window

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: a deep sloping eave carried on brackets A row of pointed merlons A railing of posts with pierced screens between them
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

A curved Nagara shikhara A whole Nagara temple: sanctum, spire and halls stepping forward A Dravida vimana: stepped storeys with miniature shrines along each edge
Shikhara Nagara temple Dravida vimana
A South Indian gopuram, tier on tier of painted figures A mandap: a pillared hall under a stepped roof A temple niche holding an image
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.

A broad stair between side walls, with a landing A frieze of figures, elephants and horses in relief A statue on a pedestal, with an urn and a pinnacle
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.

Statue of Unity modelled from a signed distance field in three.js

The temples got the biggest structural rethink in this pass. Ram Mandir now has three storeys with cusped arches and a 32-step stair. Somnath's spire rises in seven storeys over a three-tier plinth and a broad stair. Mahakaleshwar has three storeys of shrines under its spire. The shared parts that made this cheap, grandStair, storeyedHall and the cusped arch, are now available to every temple.


Forts and monasteries

A fort gate: a passage through a gatehouse, with towers A rampart and a round bastion with its crown of merlons Tibetan monks' houses: white walls, dark windows, earth roofs
Fort gate Rampart and bastion Monks' houses
A gilded dharma wheel and a victory banner
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. That is how Tawang sits on its spur, Kailasa is cut out of a basalt escarpment, Ajanta sits in its horseshoe gorge, and Amber stands on its ridge with Jaigarh's wall above.

Kailasa Temple cut out of a basalt escarpment, procedural terrain in three.js


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. And around it all lies the charbagh, the four-quarter garden that the first version forgot.

Konark. The Sun Temple is a stone chariot, so the 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.

Konark Sun Temple: the chariot plinth and the Jagamohana

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. Behind the screen there is now a court, with ranges and ramps, so the building is no longer a facade with nothing behind it.

Hawa Mahal's street front, a screen of stacked jharokhas

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 trees: neem, mango, jamun, tamarind, teak and sal Wide trees: banyan, peepal, chinar and willow Dry trees: ber, babul, khejri and dhok, low and wide
Broadleaf Wide Dry
Column and conifer trees: ashoka, poplar, cypress, casuarina, fir and deodar Coconut palms Clipped, flowering and dry shrubs
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. Six 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 }
));
Enter fullscreen mode Exit fullscreen mode

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.

Jaisalmer Fort in three.js, with the small dry trees of the Thar in place of a mango orchard

Lamps, flags, boats, towns

Heritage lamp posts, a bench, and bollards with a chain The blue Archaeological Survey of India board on two posts Fountain jets rising and falling into a ring of foam
Lamps, bench, bollards ASI board Fountains
Tibetan prayer flags in five colours The tricolour and a saffron temple pennant Marigold garlands hung between two points
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, one with a curved hood Ferry launches A cargo ship with crane posts and a funnel
Country boats Ferry launches Cargo ship
Houseboats Market stalls under coloured awnings Town houses of two to five storeys
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.

Towns are the change I like most. Kedarnath has its pilgrim town along the stone path. Tirumala has the temple town round its Mada streets. Varanasi's old city wraps the Kashi Vishwanath corridor, and Ujjain's old town sits beyond Mahakaleshwar. The Gateway of India has Elephanta ferry launches and ships at anchor.

Gateway of India with ferry launches and ships on the water, in the browser

Cars, taxis and buses on a bridge roadway
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, the seated bull Elephants, one plain and one with cloth and a howdah A horse and a seated lion
Nandi Elephants Horse and lion
Deer Yaks and camels in saddle cloths
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.

Amber Fort reflected in Maota Lake, rendered in three.js

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;
Enter fullscreen mode Exit fullscreen mode

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.

At night, floodlights and festival lights come on and stars fill the sky. Press the Night button on any monument to see it.

The Taj Mahal switching between day and night


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.

Taj Mahal marble in three.js Agra Fort red sandstone in three.js Brihadeeswara Temple granite in three.js
Marble Red sandstone Granite

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.

On top of the stone comes the ornament. The Taj gets black calligraphy bands round its pishtaqs. Itmad-ud-Daulah gets pietra dura over its walls, towers and pavilion. CSMT gets buff stone coursed with blue-grey basalt. Rani ki Vav gets sculpted panels on its side walls. All of it lives in the shader, so the mesh stays a wall.

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 full sharpness and step down for that visit if the frame rate cannot hold.

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.


How I checked the work, and what went wrong

Procedural models fail in a particular way: they look fine from the home view and wrong from two metres away. So most of the second pass was checking.

  • Photograph reviews. I went through the monuments in batches and compared each model with photographs of the real building. That is how the Taj, Charminar, the Lotus Temple, Mahabodhi and the Hawa Mahal got their fixes, and how a handful of near-misses were found and closed.
  • Full audits. After the photo reviews I went round the whole set again, in groups: views and land forms, mountains and towns, then framing, missing pieces and flicker.
  • Headless checks. The scripts in tools/ run in Node and Playwright and check triangle budgets, heights, filters, flicker and detail counts. They stop a regression from hiding in monument number 87.

Things that went wrong along the way, as I found them:

  • Flicker. Two faces on the same plane fight over the pixel. Windows, glazing and sea were all guilty. check-flicker.mjs finds them, and the rule is now that an overlay sits 2 to 5 cm off its wall.
  • A dark seam at the foot of sculpted hills, where the land met the ground.
  • A black reflection pixel on the Bandra–Worli Sea Link.
  • Eleven models that were too tall for their real heights, brought back within them.
  • Water and trees that were simply invented. The first scenery was generous. The mango orchard in the desert was the most photogenic of the lot.

I also made some deliberate cuts. The jali holes are not modelled, because 101 buildings of them would never fit the triangle budget. People are not in the scenes, and motor vehicles appear only on the two bridges that have traffic, so the monument stays the subject.


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.

If you know a building well and see something wrong, I would like to hear it. Open an issue with a photograph or a source, and it becomes the next fix.


Add your own monument

Adding another monument is still one module and one card in js/data.js.

  1. Create js/monuments/<name>.js with the masses: plinth, walls, towers, roofs.
  2. Add its card to js/data.js: name, place, dates, style, material and a few facts.
  3. Optionally add js/details/<name>.js with addDetails(f, g) for the finer architecture and the setting. Reuse the shared parts first.
  4. Run the checks in tools/ (budgets, heights, flicker). See tools/README.md.

The code is MIT licensed, so you are free to use it, learn from it and build on it.


Try it

Which building still looks least like itself? Tell me in the comments. That is the next detail file.


The component pictures are rendered from the repo by tools/shoot-components.mjs. The project's code is MIT licensed, and so is three.js. Photo textures are CC0 from Poly Haven. Map data is public domain from Natural Earth.

Top comments (2)

Collapse
 
amananandrai profile image
amananandrai • • Edited
  • ⭐ Star the repo on GitHub — it really helps more people discover it: github.com/amananandrai/monuments-...
  • 💡 Share suggestions — which monuments or details should I add next? Drop it in the comments!
  • 🐛 Raise issues — spotted an inaccurate monument design, a wrong architecture style, or a bug? Please open an issue.
  • 🔧 Raise PRs — contributions are very welcome, whether it's a new monument, a better feature like monuments according to timeline (eg- Qutub Minar in 1980s), a component fix, or a UI improvement.
Collapse
 
star123 profile image
Sᴛᴀʀ⭐ •

🥰🥰🥰🥰🥰🥰