Created
February 7, 2026 20:36
-
-
Save celsowm/9345cb404dc08a321e1cb92405cddae8 to your computer and use it in GitHub Desktop.
procedural rio janeiro metal gear vr style background
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Holographic City</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: #000; cursor: crosshair; } canvas { display: block; } #scanlines { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 11; background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.04) 2px, rgba(0,0,0,0.04) 4px); } .hud-base { position: fixed; color: rgba(60,60,60,0.7); font-family: 'Courier New', monospace; pointer-events: none; z-index: 12; } #hud { top: 20px; left: 20px; font-size: 11px; line-height: 1.8; } #hud-right { top: 20px; right: 20px; font-size: 10px; text-align: right; line-height: 1.8; } #hud-bottom { bottom: 20px; left: 50%; transform: translateX(-50%); color: rgba(80,80,80,0.5); font-size: 9px; letter-spacing: 2px; } </style> </head> <body> <div id="scanlines"></div> <div id="hud" class="hud-base"> <div>SECTOR: <span id="sector">7G-NORTH</span></div> <div>FREQ: <span id="freq">441.2</span> MHz</div> <div>NODES: <span id="nodes">2847</span></div> <div>LATENCY: <span id="latency">12</span>ms</div> <div>──────────────</div> <div>STREAM: <span id="datastream">ACTIVE</span></div> <div>BLOCKS: <span id="blocks">0</span></div> <div>TRAFFIC: <span id="traffic">0</span></div> </div> <div id="hud-right" class="hud-base"> <div><span id="clock">00:00:00</span></div> <div>FRM: <span id="frame">0</span></div> <div id="dataflow">████░░██░░</div> </div> <div id="hud-bottom" class="hud-base">[ DRAG TO ROTATE · SCROLL TO ZOOM · HOLOGRAPHIC URBAN MATRIX v4.2 ]</div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| (function() { | |
| var renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setClearColor(0xd0d0d0, 1); | |
| renderer.shadowMap.enabled = false; | |
| document.body.appendChild(renderer.domElement); | |
| var scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0xcccccc, 0.005); | |
| var camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.5, 600); | |
| var angleX = 0, angleY = 0.6, radius = 90, targetRadius = 90; | |
| var dragging = false, lastMX = 0, lastMY = 0; | |
| renderer.domElement.addEventListener('mousedown', function(e) { | |
| dragging = true; lastMX = e.clientX; lastMY = e.clientY; | |
| }); | |
| window.addEventListener('mouseup', function() { dragging = false; }); | |
| window.addEventListener('mousemove', function(e) { | |
| if (!dragging) return; | |
| angleX -= (e.clientX - lastMX) * 0.005; | |
| angleY = Math.max(0.1, Math.min(1.4, angleY + (e.clientY - lastMY) * 0.005)); | |
| lastMX = e.clientX; lastMY = e.clientY; | |
| }); | |
| window.addEventListener('wheel', function(e) { | |
| targetRadius = Math.max(20, Math.min(180, targetRadius + e.deltaY * 0.08)); | |
| }); | |
| window.addEventListener('resize', function() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| function rand(a, b) { return a + Math.random() * (b - a); } | |
| function randInt(a, b) { return Math.floor(rand(a, b)); } | |
| function createMat(color, opacity, wireframe, side) { | |
| var opts = { color: color, transparent: true, opacity: opacity }; | |
| if (wireframe) opts.wireframe = true; | |
| if (side) opts.side = side; | |
| return new THREE.MeshBasicMaterial(opts); | |
| } | |
| function createLineMat(color, opacity) { | |
| return new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: opacity }); | |
| } | |
| var edgeMat = createLineMat(0x222222, 0.6); | |
| var edgeMatDim = createLineMat(0x444444, 0.3); | |
| function makeBuildingMat(opacity) { return createMat(0x888888, opacity); } | |
| function makeWireMat(opacity) { return createMat(0x666666, opacity, true); } | |
| var blinkLights = [], windowPoints = [], cityGroup = new THREE.Group(); | |
| var cityExtent = 80; | |
| function generatePositions(start, extent, min, max) { | |
| var positions = []; | |
| var pos = start; | |
| while (pos < extent) { positions.push(pos); pos += rand(min, max); } | |
| positions.push(extent); | |
| return positions; | |
| } | |
| var avenuePositions = generatePositions(-cityExtent, cityExtent, 12, 26); | |
| var streetPositions = generatePositions(-cityExtent, cityExtent, 10, 22); | |
| var majorStreetWidth = 3.5, minorStreetWidth = 2.0, sidewalkWidth = 0.8; | |
| var streetMat = createMat(0x999999, 0.12); | |
| var sidewalkMat = createMat(0x8a8a8a, 0.08); | |
| var roadLineMat = createLineMat(0x666666, 0.35); | |
| var curbMat = createLineMat(0x555555, 0.4); | |
| function createPlane(width, height, material, rotation, position) { | |
| var mesh = new THREE.Mesh(new THREE.PlaneGeometry(width, height), material); | |
| mesh.rotation.x = rotation; | |
| mesh.position.set(position[0], position[1], position[2]); | |
| return mesh; | |
| } | |
| function createLine(points, material) { | |
| var geo = new THREE.BufferGeometry().setFromPoints(points); | |
| return new THREE.Line(geo, material); | |
| } | |
| function drawStreets(positions, isAvenue, majorInterval) { | |
| for (var pi = 0; pi < positions.length; pi++) { | |
| var pos = positions[pi]; | |
| var isMajor = pi % majorInterval === 0; | |
| var sw = isMajor ? majorStreetWidth : minorStreetWidth; | |
| var length = cityExtent * 2; | |
| var w, h, x, z; | |
| if (isAvenue) { w = sw; h = length; x = pos; z = 0; } | |
| else { w = length; h = sw; x = 0; z = pos; } | |
| cityGroup.add(createPlane(w, h, streetMat, -Math.PI/2, [x, 0.02, z])); | |
| for (var side = -1; side <= 1; side += 2) { | |
| var offset = side * (sw/2 + sidewalkWidth/2); | |
| var swW, swH, swX, swZ; | |
| if (isAvenue) { swW = sidewalkWidth; swH = length; swX = x + offset; swZ = 0; } | |
| else { swW = length; swH = sidewalkWidth; swX = 0; swZ = z + offset; } | |
| cityGroup.add(createPlane(swW, swH, sidewalkMat, -Math.PI/2, [swX, 0.04, swZ])); | |
| } | |
| if (isMajor) { | |
| for (var d = -cityExtent; d < cityExtent; d += 3) { | |
| var pts; | |
| if (isAvenue) pts = [new THREE.Vector3(pos, 0.06, d), new THREE.Vector3(pos, 0.06, d + 1.5)]; | |
| else pts = [new THREE.Vector3(d, 0.06, pos), new THREE.Vector3(d + 1.5, 0.06, pos)]; | |
| cityGroup.add(createLine(pts, roadLineMat)); | |
| } | |
| if (isAvenue) { | |
| for (var oi = 0; oi < 2; oi++) { | |
| var off = oi === 0 ? -0.15 : 0.15; | |
| cityGroup.add(createLine([ | |
| new THREE.Vector3(pos + off, 0.06, -cityExtent), | |
| new THREE.Vector3(pos + off, 0.06, cityExtent) | |
| ], createLineMat(0x777755, 0.2))); | |
| } | |
| } | |
| } | |
| for (var side2 = -1; side2 <= 1; side2 += 2) { | |
| var curbPos = pos + side2 * (sw/2); | |
| var cpts; | |
| if (isAvenue) cpts = [new THREE.Vector3(curbPos, 0.05, -cityExtent), new THREE.Vector3(curbPos, 0.05, cityExtent)]; | |
| else cpts = [new THREE.Vector3(-cityExtent, 0.05, curbPos), new THREE.Vector3(cityExtent, 0.05, curbPos)]; | |
| cityGroup.add(createLine(cpts, curbMat)); | |
| } | |
| } | |
| } | |
| drawStreets(avenuePositions, true, 3); | |
| drawStreets(streetPositions, false, 4); | |
| for (var cci = 0; cci < avenuePositions.length; cci++) { | |
| for (var ccj = 0; ccj < streetPositions.length; ccj++) { | |
| if (cci % 3 === 0 || ccj % 4 === 0) { | |
| var csw = (cci % 3 === 0) ? majorStreetWidth : minorStreetWidth; | |
| var csh = (ccj % 4 === 0) ? majorStreetWidth : minorStreetWidth; | |
| var crosswalkMat = createLineMat(0x777777, 0.25); | |
| for (var cs = -csw/2 + 0.3; cs < csw/2; cs += 0.6) { | |
| cityGroup.add(createLine([ | |
| new THREE.Vector3(avenuePositions[cci] + cs, 0.07, streetPositions[ccj] - csh/2), | |
| new THREE.Vector3(avenuePositions[cci] + cs, 0.07, streetPositions[ccj] + csh/2) | |
| ], crosswalkMat)); | |
| } | |
| for (var cs2 = -csh/2 + 0.3; cs2 < csh/2; cs2 += 0.6) { | |
| cityGroup.add(createLine([ | |
| new THREE.Vector3(avenuePositions[cci] - csw/2, 0.07, streetPositions[ccj] + cs2), | |
| new THREE.Vector3(avenuePositions[cci] + csw/2, 0.07, streetPositions[ccj] + cs2) | |
| ], crosswalkMat)); | |
| } | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // BEACH AND SEA - Lowpoly | |
| // ========================================== | |
| var oceanGroup = new THREE.Group(); | |
| var waveVertices = []; | |
| var beachCenterX = 85; | |
| var beachStartZ = -80; | |
| var beachEndZ = 80; | |
| var beachWidth = 18; | |
| function createBeach() { | |
| var beachVerts = []; | |
| var beachIdx = []; | |
| var segZ = 40; | |
| var segX = 6; | |
| for (var iz = 0; iz <= segZ; iz++) { | |
| var zt = iz / segZ; | |
| var z = beachStartZ + (beachEndZ - beachStartZ) * zt; | |
| for (var ix = 0; ix <= segX; ix++) { | |
| var xt = ix / segX; | |
| var curveOffset = Math.sin(zt * Math.PI) * 8 + Math.sin(zt * Math.PI * 3) * 2; | |
| var x = cityExtent + xt * beachWidth + curveOffset; | |
| var y = 0.3 - xt * 0.4 + rand(-0.15, 0.15); | |
| y += Math.sin(zt * 12) * 0.08 + Math.cos(xt * 8) * 0.05; | |
| if (y < -0.3) y = -0.3; | |
| beachVerts.push(x, y, z); | |
| } | |
| } | |
| for (var iz2 = 0; iz2 < segZ; iz2++) { | |
| for (var ix2 = 0; ix2 < segX; ix2++) { | |
| var a = iz2 * (segX + 1) + ix2; | |
| var b = a + 1; | |
| var c = a + (segX + 1); | |
| var d = c + 1; | |
| beachIdx.push(a, c, b); | |
| beachIdx.push(b, c, d); | |
| } | |
| } | |
| var beachGeo = new THREE.BufferGeometry(); | |
| beachGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(beachVerts), 3)); | |
| beachGeo.setIndex(beachIdx); | |
| beachGeo.computeVertexNormals(); | |
| var beachFill = new THREE.Mesh(beachGeo, createMat(0xc2b280, 0.15)); | |
| oceanGroup.add(beachFill); | |
| var beachWire = new THREE.Mesh(beachGeo, createMat(0xa09060, 0.1, true)); | |
| oceanGroup.add(beachWire); | |
| var beachEdges = new THREE.LineSegments(new THREE.EdgesGeometry(beachGeo, 15), createLineMat(0x8a7a50, 0.25)); | |
| oceanGroup.add(beachEdges); | |
| for (var smi = 0; smi < 25; smi++) { | |
| var smz = rand(beachStartZ + 5, beachEndZ - 5); | |
| var smzt = (smz - beachStartZ) / (beachEndZ - beachStartZ); | |
| var smCurve = Math.sin(smzt * Math.PI) * 8 + Math.sin(smzt * Math.PI * 3) * 2; | |
| var smx = cityExtent + rand(1, beachWidth * 0.5) + smCurve; | |
| var moundGeo = new THREE.DodecahedronGeometry(rand(0.3, 0.8), 0); | |
| var mound = new THREE.Mesh(moundGeo, createMat(0xb8a870, 0.1)); | |
| mound.position.set(smx, rand(-0.1, 0.15), smz); | |
| mound.scale.y = rand(0.2, 0.5); | |
| mound.rotation.y = rand(0, Math.PI); | |
| oceanGroup.add(mound); | |
| var moundEdge = new THREE.LineSegments(new THREE.EdgesGeometry(moundGeo), createLineMat(0x9a8a60, 0.15)); | |
| moundEdge.position.copy(mound.position); | |
| moundEdge.scale.copy(mound.scale); | |
| moundEdge.rotation.copy(mound.rotation); | |
| oceanGroup.add(moundEdge); | |
| } | |
| for (var fli = 0; fli < 3; fli++) { | |
| var foamPts = []; | |
| var foamOffset = fli * 1.5 + beachWidth * 0.4; | |
| for (var fpi = 0; fpi <= 60; fpi++) { | |
| var fpt = fpi / 60; | |
| var fpz = beachStartZ + (beachEndZ - beachStartZ) * fpt; | |
| var fpCurve = Math.sin(fpt * Math.PI) * 8 + Math.sin(fpt * Math.PI * 3) * 2; | |
| var fpx = cityExtent + foamOffset + fpCurve + Math.sin(fpt * 20 + fli * 2) * 0.5; | |
| foamPts.push(new THREE.Vector3(fpx, -0.05 + fli * 0.02, fpz)); | |
| } | |
| oceanGroup.add(createLine(foamPts, createLineMat(0xaabbcc, 0.15 - fli * 0.03))); | |
| } | |
| } | |
| createBeach(); | |
| function createOcean() { | |
| var oceanSegX = 30; | |
| var oceanSegZ = 50; | |
| var oceanWidth = 120; | |
| var oceanStartX = cityExtent + beachWidth - 5; | |
| for (var layer = 0; layer < 3; layer++) { | |
| var oceanVerts = []; | |
| var oceanIdx = []; | |
| var layerY = -0.5 - layer * 0.15; | |
| for (var oz = 0; oz <= oceanSegZ; oz++) { | |
| var ozt = oz / oceanSegZ; | |
| var ozPos = beachStartZ - 20 + (beachEndZ - beachStartZ + 40) * ozt; | |
| for (var ox = 0; ox <= oceanSegX; ox++) { | |
| var oxt = ox / oceanSegX; | |
| var beachCurveAtZ = Math.sin(ozt * Math.PI) * 8 + Math.sin(ozt * Math.PI * 3) * 2; | |
| var oxPos = oceanStartX + beachCurveAtZ + oxt * oceanWidth; | |
| var waveY = layerY; | |
| waveY += Math.sin(oxt * 4 + ozt * 3) * 0.3; | |
| waveY += Math.cos(ozt * 5 + oxt * 2) * 0.2; | |
| waveY += rand(-0.1, 0.1); | |
| oceanVerts.push(oxPos, waveY, ozPos); | |
| } | |
| } | |
| for (var oz2 = 0; oz2 < oceanSegZ; oz2++) { | |
| for (var ox2 = 0; ox2 < oceanSegX; ox2++) { | |
| var oa = oz2 * (oceanSegX + 1) + ox2; | |
| var ob = oa + 1; | |
| var oc = oa + (oceanSegX + 1); | |
| var od = oc + 1; | |
| oceanIdx.push(oa, oc, ob); | |
| oceanIdx.push(ob, oc, od); | |
| } | |
| } | |
| var oceanGeo = new THREE.BufferGeometry(); | |
| oceanGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(oceanVerts), 3)); | |
| oceanGeo.setIndex(oceanIdx); | |
| oceanGeo.computeVertexNormals(); | |
| var oceanColors = [0x5588aa, 0x447799, 0x336688]; | |
| var oceanOpacities = [0.12, 0.08, 0.05]; | |
| var oceanFill = new THREE.Mesh(oceanGeo, createMat(oceanColors[layer], oceanOpacities[layer])); | |
| oceanGroup.add(oceanFill); | |
| if (layer === 0) { | |
| var oceanWire = new THREE.Mesh(oceanGeo, createMat(0x4488aa, 0.06, true)); | |
| oceanGroup.add(oceanWire); | |
| } | |
| var edgeOpacity = [0.2, 0.12, 0.06]; | |
| var oceanEdges = new THREE.LineSegments( | |
| new THREE.EdgesGeometry(oceanGeo, 12), | |
| createLineMat(0x4477aa, edgeOpacity[layer]) | |
| ); | |
| oceanGroup.add(oceanEdges); | |
| waveVertices.push({ | |
| geo: oceanGeo, | |
| baseVerts: new Float32Array(oceanVerts), | |
| segX: oceanSegX, | |
| segZ: oceanSegZ, | |
| layer: layer | |
| }); | |
| } | |
| var deepGeo = new THREE.PlaneGeometry(200, 200, 8, 8); | |
| var deep = new THREE.Mesh(deepGeo, createMat(0x336688, 0.06)); | |
| deep.rotation.x = -Math.PI / 2; | |
| deep.position.set(cityExtent + beachWidth - 5 + 80, -1.2, 0); | |
| oceanGroup.add(deep); | |
| var deepEdge = new THREE.LineSegments(new THREE.EdgesGeometry(deepGeo), createLineMat(0x335577, 0.08)); | |
| deepEdge.rotation.x = -Math.PI / 2; | |
| deepEdge.position.copy(deep.position); | |
| oceanGroup.add(deepEdge); | |
| for (var hli = 0; hli < 5; hli++) { | |
| var hx = cityExtent + beachWidth - 5 + 80 + rand(20, 60); | |
| var hz1 = rand(-100, -30); | |
| var hz2 = rand(30, 100); | |
| oceanGroup.add(createLine([ | |
| new THREE.Vector3(hx, -0.8 + hli * 0.1, hz1), | |
| new THREE.Vector3(hx + rand(-10, 10), -0.7 + hli * 0.1, (hz1 + hz2) / 2), | |
| new THREE.Vector3(hx + rand(-5, 15), -0.8 + hli * 0.1, hz2) | |
| ], createLineMat(0x6699bb, 0.06))); | |
| } | |
| } | |
| createOcean(); | |
| var waveLines = []; | |
| for (var wli = 0; wli < 8; wli++) { | |
| var wlPts = []; | |
| var wlBaseOffset = beachWidth * 0.35 + wli * 2.5; | |
| for (var wlp = 0; wlp <= 40; wlp++) { | |
| var wlpt = wlp / 40; | |
| var wlz = beachStartZ + (beachEndZ - beachStartZ) * wlpt; | |
| var wlCurve = Math.sin(wlpt * Math.PI) * 8 + Math.sin(wlpt * Math.PI * 3) * 2; | |
| var wlx = cityExtent + wlBaseOffset + wlCurve; | |
| wlPts.push(new THREE.Vector3(wlx, -0.1, wlz)); | |
| } | |
| var wlGeo = new THREE.BufferGeometry().setFromPoints(wlPts); | |
| var wl = new THREE.Line(wlGeo, createLineMat(0x88aacc, 0.15 - wli * 0.015)); | |
| wl.userData = { | |
| baseOffset: wlBaseOffset, | |
| speed: 0.4 + wli * 0.08, | |
| phase: wli * 0.8 | |
| }; | |
| oceanGroup.add(wl); | |
| waveLines.push(wl); | |
| } | |
| for (var bri = 0; bri < 20; bri++) { | |
| var brz = rand(beachStartZ + 3, beachEndZ - 3); | |
| var brzt = (brz - beachStartZ) / (beachEndZ - beachStartZ); | |
| var brCurve = Math.sin(brzt * Math.PI) * 8 + Math.sin(brzt * Math.PI * 3) * 2; | |
| var brx = cityExtent + rand(beachWidth * 0.2, beachWidth * 0.7) + brCurve; | |
| var rockSize = rand(0.4, 1.8); | |
| var brGeo = new THREE.DodecahedronGeometry(rockSize, 0); | |
| var br = new THREE.Mesh(brGeo, createMat(0x777777, 0.1)); | |
| br.position.set(brx, rand(-0.2, 0.3), brz); | |
| br.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1)); | |
| br.scale.y = rand(0.3, 0.8); | |
| oceanGroup.add(br); | |
| var brEdge = new THREE.LineSegments(new THREE.EdgesGeometry(brGeo), createLineMat(0x555555, 0.2)); | |
| brEdge.position.copy(br.position); | |
| brEdge.rotation.copy(br.rotation); | |
| brEdge.scale.copy(br.scale); | |
| oceanGroup.add(brEdge); | |
| } | |
| for (var pti = 0; pti < 15; pti++) { | |
| var ptz = rand(beachStartZ + 8, beachEndZ - 8); | |
| var ptzt = (ptz - beachStartZ) / (beachEndZ - beachStartZ); | |
| var ptCurve = Math.sin(ptzt * Math.PI) * 8 + Math.sin(ptzt * Math.PI * 3) * 2; | |
| var ptx = cityExtent + rand(0.5, 4) + ptCurve; | |
| var ptHeight = rand(3, 6); | |
| var ptLean = rand(-0.3, 0.3); | |
| var trunkPts = []; | |
| for (var tsi = 0; tsi <= 8; tsi++) { | |
| var tst = tsi / 8; | |
| trunkPts.push(new THREE.Vector3( | |
| ptx + Math.sin(tst * 1.5) * ptLean * ptHeight, | |
| tst * ptHeight, | |
| ptz + Math.cos(tst * 2) * ptLean * 0.5 | |
| )); | |
| } | |
| oceanGroup.add(createLine(trunkPts, createLineMat(0x887755, 0.3))); | |
| var topX = ptx + Math.sin(1.5) * ptLean * ptHeight; | |
| var topZ = ptz + Math.cos(2) * ptLean * 0.5; | |
| for (var fri = 0; fri < 6; fri++) { | |
| var frAngle = (fri / 6) * Math.PI * 2 + rand(-0.2, 0.2); | |
| var frLen = rand(2, 3.5); | |
| var frPts = []; | |
| for (var frj = 0; frj <= 5; frj++) { | |
| var frt = frj / 5; | |
| frPts.push(new THREE.Vector3( | |
| topX + Math.cos(frAngle) * frt * frLen, | |
| ptHeight - frt * frt * 1.5, | |
| topZ + Math.sin(frAngle) * frt * frLen | |
| )); | |
| } | |
| oceanGroup.add(createLine(frPts, createLineMat(0x668855, 0.2))); | |
| } | |
| var trunkGeo = new THREE.CylinderGeometry(0.08, 0.15, ptHeight, 5); | |
| var trunk = new THREE.Mesh(trunkGeo, createMat(0x887755, 0.15)); | |
| trunk.position.set(ptx + ptLean * ptHeight * 0.3, ptHeight / 2, ptz); | |
| trunk.rotation.z = ptLean * 0.3; | |
| oceanGroup.add(trunk); | |
| } | |
| for (var bti = 0; bti < 6; bti++) { | |
| var btGroup = new THREE.Group(); | |
| var btz = rand(beachStartZ + 20, beachEndZ - 20); | |
| var btzt = (btz - beachStartZ) / (beachEndZ - beachStartZ); | |
| var btCurve = Math.sin(btzt * Math.PI) * 8 + Math.sin(btzt * Math.PI * 3) * 2; | |
| var btx = cityExtent + beachWidth + btCurve + rand(10, 50); | |
| var hullVerts = [ | |
| -0.8, 0, -0.3, 0.8, 0, -0.3, 0.8, 0, 0.3, -0.8, 0, 0.3, | |
| -1.0, 0.3, -0.4, 1.0, 0.3, -0.4, 1.0, 0.3, 0.4, -1.0, 0.3, 0.4, | |
| 1.4, 0.2, 0, | |
| -1.0, 0.2, 0 | |
| ]; | |
| var hullIdx = [ | |
| 0,1,5, 0,5,4, 1,2,6, 1,6,5, 2,3,7, 2,7,6, 3,0,4, 3,4,7, | |
| 4,5,8, 5,6,8, 6,7,9, 7,4,9 | |
| ]; | |
| var hullGeo = new THREE.BufferGeometry(); | |
| hullGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(hullVerts), 3)); | |
| hullGeo.setIndex(hullIdx); | |
| hullGeo.computeVertexNormals(); | |
| var hull = new THREE.Mesh(hullGeo, createMat(0x666666, 0.2)); | |
| btGroup.add(hull); | |
| var hullEdge = new THREE.LineSegments(new THREE.EdgesGeometry(hullGeo), createLineMat(0x444444, 0.35)); | |
| btGroup.add(hullEdge); | |
| if (Math.random() > 0.4) { | |
| var mastH = rand(1.5, 2.5); | |
| var mast = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, mastH, 4), createMat(0x555555, 0.3)); | |
| mast.position.y = 0.3 + mastH / 2; | |
| btGroup.add(mast); | |
| var sailVerts = [0, 0.3 + mastH, 0, 0, 0.5, 0, 0, 0.8, 1.0]; | |
| var sailIdx = [0, 1, 2]; | |
| var sailGeo = new THREE.BufferGeometry(); | |
| sailGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(sailVerts), 3)); | |
| sailGeo.setIndex(sailIdx); | |
| var sail = new THREE.Mesh(sailGeo, createMat(0xaaaaaa, 0.1, false, THREE.DoubleSide)); | |
| btGroup.add(sail); | |
| var sailEdge = new THREE.LineSegments(new THREE.EdgesGeometry(sailGeo), createLineMat(0x777777, 0.2)); | |
| btGroup.add(sailEdge); | |
| } | |
| var boatScale = rand(0.8, 1.8); | |
| btGroup.scale.setScalar(boatScale); | |
| btGroup.position.set(btx, -0.3, btz); | |
| btGroup.rotation.y = rand(0, Math.PI * 2); | |
| btGroup.userData = { | |
| baseY: -0.3, | |
| bobSpeed: rand(0.5, 1.5), | |
| bobAmp: rand(0.1, 0.25), | |
| bobPhase: rand(0, 6.28), | |
| rockSpeed: rand(0.3, 0.8), | |
| rockAmp: rand(0.02, 0.06) | |
| }; | |
| oceanGroup.add(btGroup); | |
| waveLines.push(btGroup); | |
| } | |
| for (var wsri = 0; wsri < 4; wsri++) { | |
| var wsrz = rand(beachStartZ + 10, beachEndZ - 10); | |
| var wsrzt = (wsrz - beachStartZ) / (beachEndZ - beachStartZ); | |
| var wsrCurve = Math.sin(wsrzt * Math.PI) * 8 + Math.sin(wsrzt * Math.PI * 3) * 2; | |
| var wsrx = cityExtent + beachWidth + wsrCurve + rand(15, 40); | |
| var wsrR = rand(2, 6); | |
| var wsring = new THREE.Mesh( | |
| new THREE.RingGeometry(wsrR, wsrR + 0.1, 8), | |
| createMat(0x5588aa, 0.06, false, THREE.DoubleSide) | |
| ); | |
| wsring.rotation.x = -Math.PI / 2; | |
| wsring.position.set(wsrx, -0.2, wsrz); | |
| oceanGroup.add(wsring); | |
| } | |
| oceanGroup.add(createLine([ | |
| new THREE.Vector3(cityExtent + beachWidth + 20, 0, 0), | |
| new THREE.Vector3(cityExtent + beachWidth + 25, 5, 0), | |
| new THREE.Vector3(cityExtent + beachWidth + 35, 5, 0) | |
| ], createLineMat(0x5588aa, 0.2))); | |
| var seaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.15, 4, 4), createMat(0x5588aa, 0.4)); | |
| seaLabelDot.position.set(cityExtent + beachWidth + 20, 0, 0); | |
| oceanGroup.add(seaLabelDot); | |
| scene.add(oceanGroup); | |
| var southBeachGroup = new THREE.Group(); | |
| function createSouthBeach() { | |
| var sbVerts = []; | |
| var sbIdx = []; | |
| var sbSegX = 40; | |
| var sbSegZ = 6; | |
| for (var sz = 0; sz <= sbSegZ; sz++) { | |
| var szt = sz / sbSegZ; | |
| for (var sx = 0; sx <= sbSegX; sx++) { | |
| var sxt = sx / sbSegX; | |
| var sxPos = -cityExtent + (cityExtent * 2) * sxt; | |
| var curveOff = Math.sin(sxt * Math.PI) * 6 + Math.sin(sxt * Math.PI * 4) * 1.5; | |
| var szPos = -cityExtent - szt * beachWidth - curveOff; | |
| var sy = 0.3 - szt * 0.4 + rand(-0.12, 0.12); | |
| sy += Math.sin(sxt * 15) * 0.06; | |
| if (sy < -0.3) sy = -0.3; | |
| sbVerts.push(sxPos, sy, szPos); | |
| } | |
| } | |
| for (var sz2 = 0; sz2 < sbSegZ; sz2++) { | |
| for (var sx2 = 0; sx2 < sbSegX; sx2++) { | |
| var sa = sz2 * (sbSegX + 1) + sx2; | |
| var sb = sa + 1; | |
| var sc = sa + (sbSegX + 1); | |
| var sd = sc + 1; | |
| sbIdx.push(sa, sc, sb); | |
| sbIdx.push(sb, sc, sd); | |
| } | |
| } | |
| var sbGeo = new THREE.BufferGeometry(); | |
| sbGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(sbVerts), 3)); | |
| sbGeo.setIndex(sbIdx); | |
| sbGeo.computeVertexNormals(); | |
| southBeachGroup.add(new THREE.Mesh(sbGeo, createMat(0xc2b280, 0.12))); | |
| southBeachGroup.add(new THREE.Mesh(sbGeo, createMat(0xa09060, 0.08, true))); | |
| southBeachGroup.add(new THREE.LineSegments(new THREE.EdgesGeometry(sbGeo, 15), createLineMat(0x8a7a50, 0.2))); | |
| var soVerts = []; | |
| var soIdx = []; | |
| var soSegX = 25; | |
| var soSegZ = 15; | |
| for (var soz = 0; soz <= soSegZ; soz++) { | |
| var sozt = soz / soSegZ; | |
| for (var sox = 0; sox <= soSegX; sox++) { | |
| var soxt = sox / soSegX; | |
| var soxPos = -cityExtent - 10 + (cityExtent * 2 + 20) * soxt; | |
| var soCurve = Math.sin(soxt * Math.PI) * 6 + Math.sin(soxt * Math.PI * 4) * 1.5; | |
| var sozPos = -cityExtent - beachWidth - soCurve - sozt * 60; | |
| var soy = -0.5 + Math.sin(soxt * 3 + sozt * 4) * 0.25 + rand(-0.1, 0.1); | |
| soVerts.push(soxPos, soy, sozPos); | |
| } | |
| } | |
| for (var soz2 = 0; soz2 < soSegZ; soz2++) { | |
| for (var sox2 = 0; sox2 < soSegX; sox2++) { | |
| var soa = soz2 * (soSegX + 1) + sox2; | |
| var sob = soa + 1; | |
| var soc = soa + (soSegX + 1); | |
| var sod = soc + 1; | |
| soIdx.push(soa, soc, sob); | |
| soIdx.push(sob, soc, sod); | |
| } | |
| } | |
| var soGeo = new THREE.BufferGeometry(); | |
| soGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(soVerts), 3)); | |
| soGeo.setIndex(soIdx); | |
| soGeo.computeVertexNormals(); | |
| southBeachGroup.add(new THREE.Mesh(soGeo, createMat(0x5588aa, 0.1))); | |
| southBeachGroup.add(new THREE.Mesh(soGeo, createMat(0x4488aa, 0.05, true))); | |
| southBeachGroup.add(new THREE.LineSegments(new THREE.EdgesGeometry(soGeo, 12), createLineMat(0x4477aa, 0.15))); | |
| waveVertices.push({ | |
| geo: soGeo, | |
| baseVerts: new Float32Array(soVerts), | |
| segX: soSegX, | |
| segZ: soSegZ, | |
| layer: 0 | |
| }); | |
| for (var sfli = 0; sfli < 4; sfli++) { | |
| var sflPts = []; | |
| var sflOff = beachWidth * 0.4 + sfli * 2; | |
| for (var sflp = 0; sflp <= 50; sflp++) { | |
| var sflpt = sflp / 50; | |
| var sflx = -cityExtent + (cityExtent * 2) * sflpt; | |
| var sflCurve = Math.sin(sflpt * Math.PI) * 6 + Math.sin(sflpt * Math.PI * 4) * 1.5; | |
| var sflz = -cityExtent - sflOff - sflCurve + Math.sin(sflpt * 18 + sfli) * 0.4; | |
| sflPts.push(new THREE.Vector3(sflx, -0.08, sflz)); | |
| } | |
| southBeachGroup.add(createLine(sflPts, createLineMat(0x88aacc, 0.12 - sfli * 0.02))); | |
| } | |
| for (var spti = 0; spti < 12; spti++) { | |
| var sptx = rand(-cityExtent + 5, cityExtent - 5); | |
| var sptxt = (sptx + cityExtent) / (cityExtent * 2); | |
| var sptCurve = Math.sin(sptxt * Math.PI) * 6 + Math.sin(sptxt * Math.PI * 4) * 1.5; | |
| var sptz = -cityExtent - rand(0.5, 3) - sptCurve; | |
| var sptH = rand(2.5, 5); | |
| var sptLean = rand(-0.3, 0.3); | |
| var stPts = []; | |
| for (var stsi = 0; stsi <= 6; stsi++) { | |
| var stst = stsi / 6; | |
| stPts.push(new THREE.Vector3( | |
| sptx + Math.sin(stst * 1.5) * sptLean * sptH * 0.3, | |
| stst * sptH, | |
| sptz + Math.cos(stst * 1.2) * sptLean * 0.4 | |
| )); | |
| } | |
| southBeachGroup.add(createLine(stPts, createLineMat(0x887755, 0.25))); | |
| for (var sfri = 0; sfri < 5; sfri++) { | |
| var sfrAngle = (sfri / 5) * Math.PI * 2 + rand(-0.3, 0.3); | |
| var sfrLen = rand(1.5, 3); | |
| var sfrPts = []; | |
| for (var sfrj = 0; sfrj <= 4; sfrj++) { | |
| var sfrt = sfrj / 4; | |
| sfrPts.push(new THREE.Vector3( | |
| sptx + Math.cos(sfrAngle) * sfrt * sfrLen, | |
| sptH - sfrt * sfrt * 1.2, | |
| sptz + Math.sin(sfrAngle) * sfrt * sfrLen | |
| )); | |
| } | |
| southBeachGroup.add(createLine(sfrPts, createLineMat(0x668855, 0.18))); | |
| } | |
| var stGeo = new THREE.CylinderGeometry(0.06, 0.12, sptH, 5); | |
| var stTrunk = new THREE.Mesh(stGeo, createMat(0x887755, 0.12)); | |
| stTrunk.position.set(sptx, sptH / 2, sptz); | |
| southBeachGroup.add(stTrunk); | |
| } | |
| for (var sbri = 0; sbri < 15; sbri++) { | |
| var sbrx = rand(-cityExtent + 3, cityExtent - 3); | |
| var sbrxt = (sbrx + cityExtent) / (cityExtent * 2); | |
| var sbrCurve = Math.sin(sbrxt * Math.PI) * 6 + Math.sin(sbrxt * Math.PI * 4) * 1.5; | |
| var sbrz = -cityExtent - rand(beachWidth * 0.2, beachWidth * 0.6) - sbrCurve; | |
| var sbrSize = rand(0.3, 1.5); | |
| var sbrGeo = new THREE.DodecahedronGeometry(sbrSize, 0); | |
| var sbr = new THREE.Mesh(sbrGeo, createMat(0x777777, 0.08)); | |
| sbr.position.set(sbrx, rand(-0.2, 0.2), sbrz); | |
| sbr.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1)); | |
| sbr.scale.y = rand(0.3, 0.7); | |
| southBeachGroup.add(sbr); | |
| var sbrEdge = new THREE.LineSegments(new THREE.EdgesGeometry(sbrGeo), createLineMat(0x555555, 0.18)); | |
| sbrEdge.position.copy(sbr.position); | |
| sbrEdge.rotation.copy(sbr.rotation); | |
| sbrEdge.scale.copy(sbr.scale); | |
| southBeachGroup.add(sbrEdge); | |
| } | |
| } | |
| createSouthBeach(); | |
| scene.add(southBeachGroup); | |
| // ========================================== | |
| // SUGAR LOAF - Reduced peak height | |
| // ========================================== | |
| function createSugarLoaf() { | |
| var sugarLoafGroup = new THREE.Group(); | |
| var pdaX = 62, pdaZ = -62; | |
| var urcaX = pdaX - 16, urcaZ = pdaZ + 12; | |
| var stationX = urcaX - 8, stationZ = urcaZ + 6; | |
| var urcaPeakY = 26.4; | |
| var pdaPeakY = 39; // reduced from 47.5 | |
| var waterGeo = new THREE.CircleGeometry(35, 10); | |
| var water = new THREE.Mesh(waterGeo, createMat(0x6688aa, 0.08)); | |
| water.rotation.x = -Math.PI / 2; | |
| water.position.set((pdaX + urcaX) / 2, 0.01, (pdaZ + urcaZ) / 2); | |
| sugarLoafGroup.add(water); | |
| var waterEdge = new THREE.LineSegments(new THREE.EdgesGeometry(waterGeo), createLineMat(0x557799, 0.15)); | |
| waterEdge.rotation.x = -Math.PI / 2; | |
| waterEdge.position.copy(water.position); | |
| sugarLoafGroup.add(waterEdge); | |
| var slVertices = []; | |
| var slIndices = []; | |
| // Reduced height rings for Pão de Açúcar (scaled from 47.5 to 39) | |
| var rings = [ | |
| { y: 0, r: 6.5 }, | |
| { y: 1.6, r: 6.3 }, | |
| { y: 4.1, r: 6.0 }, | |
| { y: 7.4, r: 5.5 }, | |
| { y: 10.7, r: 5.0 }, | |
| { y: 13.9, r: 4.5 }, | |
| { y: 17.2, r: 4.1 }, | |
| { y: 20.5, r: 3.7 }, | |
| { y: 23.8, r: 3.4 }, | |
| { y: 27.1, r: 3.1 }, | |
| { y: 29.5, r: 2.8 }, | |
| { y: 32.0, r: 2.5 }, | |
| { y: 34.0, r: 2.2 }, | |
| { y: 35.7, r: 1.9 }, | |
| { y: 36.9, r: 1.5 }, | |
| { y: 37.9, r: 1.1 }, | |
| { y: 38.6, r: 0.5 }, | |
| { y: 39.0, r: 0.0 } | |
| ]; | |
| var segments = 10; | |
| var slOffsets = []; | |
| for (var osi = 0; osi < segments; osi++) { | |
| slOffsets.push(rand(-1, 1)); | |
| } | |
| for (var ri = 0; ri < rings.length; ri++) { | |
| var ring = rings[ri]; | |
| if (ring.r <= 0.01) { | |
| slVertices.push(0, ring.y, 0); | |
| } else { | |
| for (var sj = 0; sj < segments; sj++) { | |
| var angle = (sj / segments) * Math.PI * 2; | |
| var rx = ring.r * (1.0 + 0.15 * Math.cos(angle * 2)); | |
| var rz = ring.r * (1.0 - 0.08 * Math.cos(angle * 2)); | |
| var irregFactor = Math.max(0, 1.0 - ring.y / 42) * 0.25; | |
| var irregOffset = slOffsets[sj] * irregFactor; | |
| var vx = Math.cos(angle) * (rx + irregOffset); | |
| var vz = Math.sin(angle) * (rz + irregOffset * 0.7); | |
| slVertices.push(vx, ring.y, vz); | |
| } | |
| } | |
| } | |
| var normalRingCount = rings.length - 1; | |
| var apexIdx = normalRingCount * segments; | |
| for (var ri2 = 0; ri2 < normalRingCount - 1; ri2++) { | |
| for (var sk = 0; sk < segments; sk++) { | |
| var s1 = sk; | |
| var s2 = (sk + 1) % segments; | |
| var i0 = ri2 * segments + s1; | |
| var i1 = ri2 * segments + s2; | |
| var i2 = (ri2 + 1) * segments + s2; | |
| var i3 = (ri2 + 1) * segments + s1; | |
| slIndices.push(i0, i1, i2); | |
| slIndices.push(i0, i2, i3); | |
| } | |
| } | |
| var lastFullRing = (normalRingCount - 1) * segments; | |
| for (var sa = 0; sa < segments; sa++) { | |
| var sa2 = (sa + 1) % segments; | |
| slIndices.push(lastFullRing + sa, lastFullRing + sa2, apexIdx); | |
| } | |
| for (var sb2 = 0; sb2 < segments - 2; sb2++) { | |
| slIndices.push(0, sb2 + 1, sb2 + 2); | |
| } | |
| var slGeo = new THREE.BufferGeometry(); | |
| slGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(slVertices), 3)); | |
| slGeo.setIndex(slIndices); | |
| slGeo.computeVertexNormals(); | |
| var slFill = new THREE.Mesh(slGeo, createMat(0x888888, 0.1)); | |
| slFill.position.set(pdaX, 0, pdaZ); | |
| sugarLoafGroup.add(slFill); | |
| var slWire = new THREE.Mesh(slGeo, createMat(0x666666, 0.15, true)); | |
| slWire.position.copy(slFill.position); | |
| sugarLoafGroup.add(slWire); | |
| var slEdges = new THREE.LineSegments(new THREE.EdgesGeometry(slGeo, 20), createLineMat(0x222222, 0.5)); | |
| slEdges.position.copy(slFill.position); | |
| sugarLoafGroup.add(slEdges); | |
| var urcaRings = [ | |
| { y: 0, r: 5.5 }, | |
| { y: 2, r: 5.3 }, | |
| { y: 5, r: 5.0 }, | |
| { y: 9, r: 4.5 }, | |
| { y: 13, r: 4.0 }, | |
| { y: 17, r: 3.5 }, | |
| { y: 20, r: 3.0 }, | |
| { y: 23, r: 2.6 }, | |
| { y: 25, r: 2.3 }, | |
| { y: 26, r: 2.0 }, | |
| { y: 26.4, r: 1.6 } | |
| ]; | |
| var urcaSegments = 8; | |
| var urcaVerts = []; | |
| var urcaIdx = []; | |
| var urcaOffsets = []; | |
| for (var uoi = 0; uoi < urcaSegments; uoi++) { | |
| urcaOffsets.push(rand(-1, 1)); | |
| } | |
| for (var uri = 0; uri < urcaRings.length; uri++) { | |
| var uring = urcaRings[uri]; | |
| for (var usj = 0; usj < urcaSegments; usj++) { | |
| var uAngle = (usj / urcaSegments) * Math.PI * 2; | |
| var urx = uring.r * (1.0 + 0.12 * Math.cos(uAngle * 2 + 0.5)); | |
| var urz = uring.r * (1.0 - 0.1 * Math.sin(uAngle * 2)); | |
| var uIrreg = Math.max(0, 1.0 - uring.y / 30) * 0.35; | |
| var uOff = urcaOffsets[usj] * uIrreg; | |
| urcaVerts.push(Math.cos(uAngle) * (urx + uOff), uring.y, Math.sin(uAngle) * (urz + uOff * 0.6)); | |
| } | |
| } | |
| var urcaTopIdx = urcaRings.length * urcaSegments; | |
| urcaVerts.push(0, 26.4, 0); | |
| for (var uri2 = 0; uri2 < urcaRings.length - 1; uri2++) { | |
| for (var usk = 0; usk < urcaSegments; usk++) { | |
| var us1 = usk, us2 = (usk + 1) % urcaSegments; | |
| var ui0 = uri2 * urcaSegments + us1; | |
| var ui1 = uri2 * urcaSegments + us2; | |
| var ui2 = (uri2 + 1) * urcaSegments + us2; | |
| var ui3 = (uri2 + 1) * urcaSegments + us1; | |
| urcaIdx.push(ui0, ui1, ui2); | |
| urcaIdx.push(ui0, ui2, ui3); | |
| } | |
| } | |
| var uLastRing = (urcaRings.length - 1) * urcaSegments; | |
| for (var utc = 0; utc < urcaSegments; utc++) { | |
| var utc2 = (utc + 1) % urcaSegments; | |
| urcaIdx.push(uLastRing + utc, uLastRing + utc2, urcaTopIdx); | |
| } | |
| for (var ubc = 0; ubc < urcaSegments - 2; ubc++) { | |
| urcaIdx.push(0, ubc + 1, ubc + 2); | |
| } | |
| var urcaGeo = new THREE.BufferGeometry(); | |
| urcaGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(urcaVerts), 3)); | |
| urcaGeo.setIndex(urcaIdx); | |
| urcaGeo.computeVertexNormals(); | |
| var urcaFill = new THREE.Mesh(urcaGeo, createMat(0x777777, 0.12)); | |
| urcaFill.position.set(urcaX, 0, urcaZ); | |
| sugarLoafGroup.add(urcaFill); | |
| var urcaWireM = new THREE.Mesh(urcaGeo, createMat(0x666666, 0.15, true)); | |
| urcaWireM.position.copy(urcaFill.position); | |
| sugarLoafGroup.add(urcaWireM); | |
| var urcaEdgesM = new THREE.LineSegments(new THREE.EdgesGeometry(urcaGeo, 20), createLineMat(0x333333, 0.45)); | |
| urcaEdgesM.position.copy(urcaFill.position); | |
| sugarLoafGroup.add(urcaEdgesM); | |
| var stationY = 1.5; | |
| var stationGeo = new THREE.BoxGeometry(3, 3, 2.5); | |
| var station = new THREE.Mesh(stationGeo, createMat(0x777777, 0.15)); | |
| station.position.set(stationX, stationY, stationZ); | |
| sugarLoafGroup.add(station); | |
| var stationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(stationGeo), createLineMat(0x333333, 0.4)); | |
| stationEdge.position.copy(station.position); | |
| sugarLoafGroup.add(stationEdge); | |
| var stationTowerH = 6; | |
| var stationTowerGeo = new THREE.BoxGeometry(0.6, stationTowerH, 0.6); | |
| var stationTower = new THREE.Mesh(stationTowerGeo, createMat(0x666666, 0.2)); | |
| stationTower.position.set(stationX, stationTowerH / 2, stationZ); | |
| sugarLoafGroup.add(stationTower); | |
| var stationTowerEdge = new THREE.LineSegments(new THREE.EdgesGeometry(stationTowerGeo), createLineMat(0x333333, 0.4)); | |
| stationTowerEdge.position.copy(stationTower.position); | |
| sugarLoafGroup.add(stationTowerEdge); | |
| var urcaStationGeo = new THREE.BoxGeometry(2.5, 2, 2); | |
| var urcaStation = new THREE.Mesh(urcaStationGeo, createMat(0x777777, 0.15)); | |
| urcaStation.position.set(urcaX, urcaPeakY + 1, urcaZ); | |
| sugarLoafGroup.add(urcaStation); | |
| var urcaStationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(urcaStationGeo), createLineMat(0x333333, 0.4)); | |
| urcaStationEdge.position.copy(urcaStation.position); | |
| sugarLoafGroup.add(urcaStationEdge); | |
| var pdaStationGeo = new THREE.BoxGeometry(2, 1.5, 1.8); | |
| var pdaStationY = pdaPeakY - 3; | |
| var pdaStation = new THREE.Mesh(pdaStationGeo, createMat(0x777777, 0.15)); | |
| pdaStation.position.set(pdaX, pdaStationY, pdaZ); | |
| sugarLoafGroup.add(pdaStation); | |
| var pdaStationEdge = new THREE.LineSegments(new THREE.EdgesGeometry(pdaStationGeo), createLineMat(0x333333, 0.4)); | |
| pdaStationEdge.position.copy(pdaStation.position); | |
| sugarLoafGroup.add(pdaStationEdge); | |
| var cable1StartY = stationTowerH; | |
| var cable1EndY = urcaPeakY + 2; | |
| var cable2StartY = urcaPeakY + 2; | |
| var cable2EndY = pdaStationY + 0.75; | |
| var cable1Points = []; | |
| var c1Start = new THREE.Vector3(stationX, cable1StartY, stationZ); | |
| var c1End = new THREE.Vector3(urcaX, cable1EndY, urcaZ); | |
| for (var c1i = 0; c1i <= 30; c1i++) { | |
| var c1t = c1i / 30; | |
| var c1x = c1Start.x + (c1End.x - c1Start.x) * c1t; | |
| var c1z = c1Start.z + (c1End.z - c1Start.z) * c1t; | |
| var c1y = c1Start.y + (c1End.y - c1Start.y) * c1t; | |
| var c1sag = -Math.sin(c1t * Math.PI) * 3.0; | |
| cable1Points.push(new THREE.Vector3(c1x, c1y + c1sag, c1z)); | |
| } | |
| sugarLoafGroup.add(createLine(cable1Points, createLineMat(0x444444, 0.35))); | |
| var cable1bPoints = []; | |
| for (var c1bi = 0; c1bi <= 30; c1bi++) { | |
| var c1bt = c1bi / 30; | |
| var c1bx = c1Start.x + (c1End.x - c1Start.x) * c1bt + 0.3; | |
| var c1bz = c1Start.z + (c1End.z - c1Start.z) * c1bt + 0.3; | |
| var c1by = c1Start.y + (c1End.y - c1Start.y) * c1bt; | |
| var c1bsag = -Math.sin(c1bt * Math.PI) * 3.3; | |
| cable1bPoints.push(new THREE.Vector3(c1bx, c1by + c1bsag, c1bz)); | |
| } | |
| sugarLoafGroup.add(createLine(cable1bPoints, createLineMat(0x444444, 0.25))); | |
| var cable2Points = []; | |
| var c2Start = new THREE.Vector3(urcaX, cable2StartY, urcaZ); | |
| var c2End = new THREE.Vector3(pdaX, cable2EndY, pdaZ); | |
| for (var c2i = 0; c2i <= 35; c2i++) { | |
| var c2t = c2i / 35; | |
| var c2x = c2Start.x + (c2End.x - c2Start.x) * c2t; | |
| var c2z = c2Start.z + (c2End.z - c2Start.z) * c2t; | |
| var c2y = c2Start.y + (c2End.y - c2Start.y) * c2t; | |
| var c2sag = -Math.sin(c2t * Math.PI) * 2.5; | |
| cable2Points.push(new THREE.Vector3(c2x, c2y + c2sag, c2z)); | |
| } | |
| sugarLoafGroup.add(createLine(cable2Points, createLineMat(0x444444, 0.4))); | |
| var cable2bPoints = []; | |
| for (var c2bi = 0; c2bi <= 35; c2bi++) { | |
| var c2bt = c2bi / 35; | |
| var c2bx = c2Start.x + (c2End.x - c2Start.x) * c2bt + 0.3; | |
| var c2bz = c2Start.z + (c2End.z - c2Start.z) * c2bt + 0.3; | |
| var c2by = c2Start.y + (c2End.y - c2Start.y) * c2bt; | |
| var c2bsag = -Math.sin(c2bt * Math.PI) * 2.8; | |
| cable2bPoints.push(new THREE.Vector3(c2bx, c2by + c2bsag, c2bz)); | |
| } | |
| sugarLoafGroup.add(createLine(cable2bPoints, createLineMat(0x444444, 0.3))); | |
| var gondola1 = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.0, 0.9), createMat(0x555555, 0.5)); | |
| gondola1.userData = { cablePoints: cable1Points, t: 0, speed: 0.003, hangOffset: -1.2 }; | |
| sugarLoafGroup.add(gondola1); | |
| gondola1.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.4, 1.0, 0.9)), createLineMat(0x222222, 0.6))); | |
| var gondola2 = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.0, 0.9), createMat(0x555555, 0.5)); | |
| gondola2.userData = { cablePoints: cable2Points, t: 0.5, speed: 0.002, hangOffset: -1.2 }; | |
| sugarLoafGroup.add(gondola2); | |
| gondola2.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.4, 1.0, 0.9)), createLineMat(0x222222, 0.6))); | |
| var topBeacon = new THREE.Mesh(new THREE.SphereGeometry(0.35, 6, 6), createMat(0xffffff, 0.8)); | |
| topBeacon.position.set(pdaX, pdaPeakY + 1, pdaZ); | |
| topBeacon.userData = { blink: 1.5, offset: 0 }; | |
| sugarLoafGroup.add(topBeacon); | |
| blinkLights.push(topBeacon); | |
| var urcaBeacon = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), createMat(0xffffff, 0.7)); | |
| urcaBeacon.position.set(urcaX, urcaPeakY + 0.5, urcaZ); | |
| urcaBeacon.userData = { blink: 2.0, offset: 1.5 }; | |
| sugarLoafGroup.add(urcaBeacon); | |
| blinkLights.push(urcaBeacon); | |
| var pdaLightYs = [10, 18, 26, 33]; | |
| for (var pli = 0; pli < pdaLightYs.length; pli++) { | |
| var plY = pdaLightYs[pli]; | |
| var plight = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0xdddddd, 0.6)); | |
| var plAngle = rand(0, Math.PI * 2); | |
| var plR = 3; | |
| for (var plri = 0; plri < rings.length - 1; plri++) { | |
| if (rings[plri].y <= plY && rings[plri + 1].y >= plY) { | |
| var plFrac = (plY - rings[plri].y) / (rings[plri + 1].y - rings[plri].y); | |
| plR = rings[plri].r + (rings[plri + 1].r - rings[plri].r) * plFrac; | |
| break; | |
| } | |
| } | |
| plight.position.set(pdaX + Math.cos(plAngle) * (plR * 0.8), plY, pdaZ + Math.sin(plAngle) * (plR * 0.8)); | |
| plight.userData = { blink: rand(0.8, 2.5), offset: rand(0, 6.28) }; | |
| sugarLoafGroup.add(plight); | |
| blinkLights.push(plight); | |
| } | |
| var urcaLightYs = [8, 16, 22]; | |
| for (var uli = 0; uli < urcaLightYs.length; uli++) { | |
| var ulight = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0xdddddd, 0.5)); | |
| var ulAngle = rand(0, Math.PI * 2); | |
| ulight.position.set(urcaX + Math.cos(ulAngle) * 3.0, urcaLightYs[uli], urcaZ + Math.sin(ulAngle) * 3.0); | |
| ulight.userData = { blink: rand(0.8, 2.5), offset: rand(0, 6.28) }; | |
| sugarLoafGroup.add(ulight); | |
| blinkLights.push(ulight); | |
| } | |
| for (var rki = 0; rki < 16; rki++) { | |
| var rkAngle = (rki / 16) * Math.PI * 2 + rand(-0.2, 0.2); | |
| var rkCx = (pdaX + urcaX) / 2; | |
| var rkCz = (pdaZ + urcaZ) / 2; | |
| var rkDist = 16 + rand(0, 6); | |
| var rockGeo = new THREE.DodecahedronGeometry(rand(0.8, 2.5), 0); | |
| var rock = new THREE.Mesh(rockGeo, createMat(0x777777, 0.08)); | |
| rock.position.set(rkCx + Math.cos(rkAngle) * rkDist, rand(0.2, 1.2), rkCz + Math.sin(rkAngle) * rkDist); | |
| rock.rotation.set(rand(0, 1), rand(0, 1), rand(0, 1)); | |
| rock.scale.y = rand(0.4, 1.2); | |
| sugarLoafGroup.add(rock); | |
| var rockEdge = new THREE.LineSegments(new THREE.EdgesGeometry(rockGeo), createLineMat(0x444444, 0.25)); | |
| rockEdge.position.copy(rock.position); | |
| rockEdge.rotation.copy(rock.rotation); | |
| rockEdge.scale.copy(rock.scale); | |
| sugarLoafGroup.add(rockEdge); | |
| } | |
| for (var sri = 0; sri < 5; sri++) { | |
| var scanY = 4 + sri * 7; | |
| var scanR = 4; | |
| for (var srri = 0; srri < rings.length - 1; srri++) { | |
| if (rings[srri].y <= scanY && rings[srri + 1].y >= scanY) { | |
| var srFrac = (scanY - rings[srri].y) / (rings[srri + 1].y - rings[srri].y); | |
| scanR = rings[srri].r + (rings[srri + 1].r - rings[srri].r) * srFrac + 1.5; | |
| break; | |
| } | |
| } | |
| var scanRing = new THREE.Mesh( | |
| new THREE.RingGeometry(scanR, scanR + 0.08, 16), | |
| createMat(0x6688aa, 0.06, false, THREE.DoubleSide) | |
| ); | |
| scanRing.rotation.x = -Math.PI / 2; | |
| scanRing.position.set(pdaX, scanY, pdaZ); | |
| sugarLoafGroup.add(scanRing); | |
| } | |
| for (var usri = 0; usri < 3; usri++) { | |
| var uScanY = 5 + usri * 8; | |
| var uScanR = 4.5 - usri * 0.8; | |
| var uScanRing = new THREE.Mesh( | |
| new THREE.RingGeometry(uScanR, uScanR + 0.08, 16), | |
| createMat(0x6688aa, 0.05, false, THREE.DoubleSide) | |
| ); | |
| uScanRing.rotation.x = -Math.PI / 2; | |
| uScanRing.position.set(urcaX, uScanY, urcaZ); | |
| sugarLoafGroup.add(uScanRing); | |
| } | |
| sugarLoafGroup.add(createLine([ | |
| new THREE.Vector3(pdaX + 3, pdaPeakY + 2, pdaZ), | |
| new THREE.Vector3(pdaX + 14, pdaPeakY + 8, pdaZ + 5), | |
| new THREE.Vector3(pdaX + 22, pdaPeakY + 8, pdaZ + 5) | |
| ], createLineMat(0x555555, 0.35))); | |
| var pdaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.15, 4, 4), createMat(0x666666, 0.5)); | |
| pdaLabelDot.position.set(pdaX + 3, pdaPeakY + 2, pdaZ); | |
| sugarLoafGroup.add(pdaLabelDot); | |
| sugarLoafGroup.add(createLine([ | |
| new THREE.Vector3(urcaX - 2, urcaPeakY + 2, urcaZ), | |
| new THREE.Vector3(urcaX - 12, urcaPeakY + 8, urcaZ + 6), | |
| new THREE.Vector3(urcaX - 20, urcaPeakY + 8, urcaZ + 6) | |
| ], createLineMat(0x555555, 0.3))); | |
| var urcaLabelDot = new THREE.Mesh(new THREE.SphereGeometry(0.12, 4, 4), createMat(0x666666, 0.45)); | |
| urcaLabelDot.position.set(urcaX - 2, urcaPeakY + 2, urcaZ); | |
| sugarLoafGroup.add(urcaLabelDot); | |
| var trailPts = []; | |
| for (var tpi = 0; tpi <= 20; tpi++) { | |
| var tpt = tpi / 20; | |
| trailPts.push(new THREE.Vector3( | |
| urcaX + (pdaX - urcaX) * tpt, | |
| 0.08, | |
| urcaZ + (pdaZ - urcaZ) * tpt | |
| )); | |
| } | |
| sugarLoafGroup.add(createLine(trailPts, createLineMat(0x666666, 0.2))); | |
| return { | |
| group: sugarLoafGroup, | |
| gondolas: [gondola1, gondola2] | |
| }; | |
| } | |
| var sugarLoaf = createSugarLoaf(); | |
| cityGroup.add(sugarLoaf.group); | |
| var slExcludeX = 58, slExcludeZ = -56, slExcludeR = 28; | |
| function createBuilding(x, z, maxH, footW, footD) { | |
| var dx = x - slExcludeX, dz = z - slExcludeZ; | |
| if (Math.sqrt(dx * dx + dz * dz) < slExcludeR) return new THREE.Group(); | |
| if (x > cityExtent - 2) return new THREE.Group(); | |
| if (z < -cityExtent + 2) return new THREE.Group(); | |
| var group = new THREE.Group(); | |
| group.position.set(x, 0, z); | |
| var type = randInt(0, 8); | |
| var w = footW || rand(1.2, 3); | |
| var d = footD || rand(1.2, 3); | |
| var h, geo; | |
| switch(type) { | |
| case 0: h = rand(5, maxH); geo = new THREE.BoxGeometry(w, h, d); break; | |
| case 1: h = rand(10, maxH * 1.3); geo = new THREE.BoxGeometry(w * 0.7, h, d * 0.7); break; | |
| case 2: h = rand(2, 8); geo = new THREE.BoxGeometry(w, h, d); break; | |
| case 3: h = rand(6, maxH); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.4, Math.min(w,d)*0.45, h, 8); break; | |
| case 4: h = rand(12, maxH * 1.2); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.12, Math.min(w,d)*0.45, h, 4); break; | |
| case 5: h = rand(8, maxH); geo = new THREE.CylinderGeometry(Math.min(w,d)*0.45, Math.min(w,d)*0.45, h, 8); break; | |
| case 6: h = rand(5, maxH); geo = new THREE.BoxGeometry(w, h, d); break; | |
| default: h = rand(4, maxH); geo = new THREE.BoxGeometry(w, h, d); break; | |
| } | |
| function addMeshes(geometry, yPos) { | |
| var fill = new THREE.Mesh(geometry, makeBuildingMat(rand(0.06, 0.18))); | |
| fill.position.y = yPos; group.add(fill); | |
| var wire = new THREE.Mesh(geometry, makeWireMat(rand(0.08, 0.2))); | |
| wire.position.y = yPos; group.add(wire); | |
| var edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), Math.random() > 0.5 ? edgeMat : edgeMatDim); | |
| edges.position.y = yPos; group.add(edges); | |
| } | |
| addMeshes(geo, h/2); | |
| if (Math.random() > 0.5 && h > 8) { | |
| var curH = h, curW = w, curD = d; | |
| var levels = randInt(1, 3); | |
| for (var lv = 0; lv < levels; lv++) { | |
| curW *= rand(0.5, 0.8); curD *= rand(0.5, 0.8); | |
| var lh = rand(2, 8); | |
| addMeshes(new THREE.BoxGeometry(curW, lh, curD), curH + lh/2); | |
| curH += lh; | |
| } | |
| h = curH; | |
| } | |
| if (type === 6 && Math.random() > 0.3) { | |
| var ew = w * rand(0.3, 0.6), ed = d * rand(0.5, 0.9), eh = h * rand(0.5, 1.0); | |
| var eGeo = new THREE.BoxGeometry(ew, eh, ed); | |
| var eFill = new THREE.Mesh(eGeo, makeBuildingMat(rand(0.06, 0.15))); | |
| eFill.position.set(w/2 + ew/2, eh/2, 0); group.add(eFill); | |
| var eEdge = new THREE.LineSegments(new THREE.EdgesGeometry(eGeo), edgeMatDim); | |
| eEdge.position.copy(eFill.position); group.add(eEdge); | |
| } | |
| if (Math.random() > 0.6) { | |
| var aH = rand(2, 7); | |
| var antenna = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.06, aH, 4), createMat(0x444444, 1)); | |
| antenna.position.y = h + aH/2; group.add(antenna); | |
| var beacon = new THREE.Mesh(new THREE.SphereGeometry(0.12, 6, 6), createMat(0xffffff, 0.9)); | |
| beacon.position.y = h + aH; | |
| beacon.userData = { blink: rand(1, 4), offset: rand(0, 6.28) }; | |
| group.add(beacon); blinkLights.push(beacon); | |
| } | |
| if (h > 5 && Math.random() > 0.2) { | |
| var wPos = []; | |
| var floors = Math.floor(h / 1.5); | |
| var wPerFloor = Math.max(2, Math.floor(w / 0.8)); | |
| var ww = w * 0.5, wd = d * 0.5; | |
| for (var f = 1; f < floors; f++) { | |
| var wy = f * 1.5 + rand(-0.1, 0.1); | |
| for (var wi = 0; wi < wPerFloor && wPos.length < randInt(12, 60) * 3; wi++) { | |
| var wside = randInt(0, 4); | |
| var frac = (wi + 0.5) / wPerFloor; | |
| var wx, wz; | |
| if (wside === 0) { wx = ww + 0.05; wz = -wd + frac * 2 * wd; } | |
| else if (wside === 1) { wx = -ww - 0.05; wz = -wd + frac * 2 * wd; } | |
| else if (wside === 2) { wx = -ww + frac * 2 * ww; wz = wd + 0.05; } | |
| else { wx = -ww + frac * 2 * ww; wz = -wd - 0.05; } | |
| wPos.push(wx, wy, wz); | |
| } | |
| } | |
| if (wPos.length > 0) { | |
| var wGeo = new THREE.BufferGeometry(); | |
| wGeo.setAttribute('position', new THREE.Float32BufferAttribute(wPos, 3)); | |
| var wPts = new THREE.Points(wGeo, new THREE.PointsMaterial({ | |
| color: 0x333333, size: rand(0.15, 0.35), transparent: true, opacity: rand(0.3, 0.7) | |
| })); | |
| group.add(wPts); windowPoints.push(wPts); | |
| } | |
| } | |
| return group; | |
| } | |
| var totalBlocks = 0; | |
| var buildingSetback = 0.6; | |
| for (var ai = 0; ai < avenuePositions.length - 1; ai++) { | |
| for (var si2 = 0; si2 < streetPositions.length - 1; si2++) { | |
| var x0 = avenuePositions[ai], x1 = avenuePositions[ai + 1]; | |
| var z0 = streetPositions[si2], z1 = streetPositions[si2 + 1]; | |
| var sw0 = ((ai % 3 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback; | |
| var sw1 = (((ai+1) % 3 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback; | |
| var sw2 = ((si2 % 4 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback; | |
| var sw3 = (((si2+1) % 4 === 0) ? majorStreetWidth : minorStreetWidth) / 2 + sidewalkWidth + buildingSetback; | |
| var blockLeft = x0 + sw0, blockRight = x1 - sw1; | |
| var blockBottom = z0 + sw2, blockTop = z1 - sw3; | |
| var blockW = blockRight - blockLeft, blockD = blockTop - blockBottom; | |
| if (blockW < 2 || blockD < 2 || Math.random() < 0.2) continue; // increased empty block chance | |
| totalBlocks++; | |
| var blockCX = (blockLeft + blockRight) / 2, blockCZ = (blockBottom + blockTop) / 2; | |
| var bDist = Math.sqrt(blockCX * blockCX + blockCZ * blockCZ); | |
| var baseMaxH = 5 + (1 - Math.min(bDist / cityExtent, 1)) * 45 + rand(0, 10); | |
| var blockType = Math.random(); | |
| if (blockType < 0.3 && blockW > 5 && blockD > 5) { | |
| cityGroup.add(createBuilding(blockCX, blockCZ, baseMaxH, blockW * rand(0.7, 0.95), blockD * rand(0.7, 0.95))); | |
| } else if (blockType < 0.6) { | |
| var divisions = Math.max(1, Math.floor(blockW / rand(4, 6.5))); // fewer divisions | |
| var slotW = blockW / divisions; | |
| for (var di = 0; di < divisions; di++) { | |
| var bx = blockLeft + slotW * (di + 0.5); | |
| if (Math.random() > 0.2) { // increased skip chance | |
| var bd1 = Math.min(blockD * 0.4, rand(2, 4)); | |
| cityGroup.add(createBuilding(bx, blockBottom + bd1/2 + 0.2, baseMaxH * rand(0.5, 1), slotW * rand(0.75, 0.95), bd1)); | |
| } | |
| if (Math.random() > 0.35) { // increased skip chance | |
| var bd2 = Math.min(blockD * 0.4, rand(2, 4)); | |
| cityGroup.add(createBuilding(bx, blockTop - bd2/2 - 0.2, baseMaxH * rand(0.4, 0.9), slotW * rand(0.75, 0.95), bd2)); | |
| } | |
| } | |
| var sideDivisions = Math.max(1, Math.floor(blockD / rand(4, 6.5))); // fewer divisions | |
| var slotD = blockD / sideDivisions; | |
| for (var di2 = 0; di2 < sideDivisions; di2++) { | |
| var bz = blockBottom + slotD * (di2 + 0.5); | |
| if (Math.random() > 0.45) { // increased skip chance | |
| var bw1 = Math.min(blockW * 0.3, rand(1.5, 3)); | |
| cityGroup.add(createBuilding(blockLeft + bw1/2 + 0.2, bz, baseMaxH * rand(0.3, 0.8), bw1, slotD * rand(0.7, 0.9))); | |
| } | |
| if (Math.random() > 0.45) { // increased skip chance | |
| var bw2 = Math.min(blockW * 0.3, rand(1.5, 3)); | |
| cityGroup.add(createBuilding(blockRight - bw2/2 - 0.2, bz, baseMaxH * rand(0.3, 0.8), bw2, slotD * rand(0.7, 0.9))); | |
| } | |
| } | |
| } else { | |
| var cols = Math.max(1, Math.floor(blockW / rand(3.5, 5.5))); // fewer cols | |
| var rows = Math.max(1, Math.floor(blockD / rand(3.5, 5.5))); // fewer rows | |
| var cellW = blockW / cols, cellD = blockD / rows; | |
| for (var c = 0; c < cols; c++) { | |
| for (var r = 0; r < rows; r++) { | |
| if (Math.random() < 0.35) continue; // increased skip from 0.15 to 0.35 | |
| cityGroup.add(createBuilding(blockLeft + cellW * (c + 0.5), blockBottom + cellD * (r + 0.5), | |
| baseMaxH * rand(0.3, 1), cellW * rand(0.6, 0.9), cellD * rand(0.6, 0.9))); | |
| } | |
| } | |
| } | |
| if (Math.random() < 0.08) { // slightly more parks to fill gaps | |
| var treeCount = randInt(3, 10); | |
| for (var ti = 0; ti < treeCount; ti++) { | |
| var tx = rand(blockLeft + 1, blockRight - 1); | |
| var tz = rand(blockBottom + 1, blockTop - 1); | |
| var tH = rand(1.5, 4); | |
| var trunk2 = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.12, tH, 4), createMat(0x666655, 0.3)); | |
| trunk2.position.set(tx, tH/2, tz); cityGroup.add(trunk2); | |
| var canopyGeo = new THREE.SphereGeometry(rand(0.8, 1.8), 6, 6); | |
| var canopy = new THREE.Mesh(canopyGeo, createMat(0x667766, 0.15)); | |
| canopy.position.set(tx, tH + 0.5, tz); cityGroup.add(canopy); | |
| var canopyEdge = new THREE.LineSegments(new THREE.EdgesGeometry(canopyGeo), createLineMat(0x556655, 0.2)); | |
| canopyEdge.position.copy(canopy.position); cityGroup.add(canopyEdge); | |
| } | |
| } | |
| } | |
| } | |
| // Street lights | |
| for (var sli2 = 0; sli2 < avenuePositions.length; sli2++) { | |
| var slsw = (sli2 % 3 === 0) ? majorStreetWidth : minorStreetWidth; | |
| for (var slz = -cityExtent; slz < cityExtent; slz += rand(6, 12)) { | |
| for (var slside = -1; slside <= 1; slside += 2) { | |
| var lx = avenuePositions[sli2] + slside * (slsw/2 + sidewalkWidth * 0.5); | |
| var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.06, 3, 4), createMat(0x555555, 0.3)); | |
| pole.position.set(lx, 1.5, slz); cityGroup.add(pole); | |
| var slight = new THREE.Mesh(new THREE.SphereGeometry(0.1, 4, 4), createMat(0xeeeecc, 0.6)); | |
| slight.position.set(lx, 3.1, slz); | |
| slight.userData = { blink: rand(0.2, 0.8), offset: rand(0, 6.28) }; | |
| cityGroup.add(slight); blinkLights.push(slight); | |
| } | |
| } | |
| } | |
| // Traffic - reduced count | |
| var trafficParticles = []; | |
| var trafficCount = 140; // reduced from 200 | |
| for (var tri = 0; tri < trafficCount; tri++) { | |
| var trIsAvenue = Math.random() > 0.5; | |
| var trStreetIdx = randInt(0, trIsAvenue ? avenuePositions.length : streetPositions.length); | |
| var trPos, trDir; | |
| if (trIsAvenue) { | |
| trPos = new THREE.Vector3(avenuePositions[trStreetIdx] + rand(-0.8, 0.8), 0.15, rand(-cityExtent, cityExtent)); | |
| trDir = new THREE.Vector3(0, 0, Math.random() > 0.5 ? 1 : -1); | |
| } else { | |
| trPos = new THREE.Vector3(rand(-cityExtent, cityExtent), 0.15, streetPositions[trStreetIdx] + rand(-0.8, 0.8)); | |
| trDir = new THREE.Vector3(Math.random() > 0.5 ? 1 : -1, 0, 0); | |
| } | |
| var tMesh = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.2, 0.3), createMat(Math.random() > 0.5 ? 0xffffee : 0xffeecc, rand(0.3, 0.6))); | |
| tMesh.position.copy(trPos); | |
| if (trIsAvenue) tMesh.rotation.y = Math.PI / 2; | |
| cityGroup.add(tMesh); | |
| trafficParticles.push({ mesh: tMesh, dir: trDir, speed: rand(0.05, 0.2) }); | |
| } | |
| scene.add(cityGroup); | |
| var gridHelper = new THREE.GridHelper(250, 125, 0x999999, 0xbbbbbb); | |
| gridHelper.material.transparent = true; | |
| gridHelper.material.opacity = 0.12; | |
| gridHelper.position.y = 0.005; | |
| scene.add(gridHelper); | |
| var pulseRings = []; | |
| for (var pri = 0; pri < 4; pri++) { | |
| var baseR = 3 + pri * 15; | |
| var pring = new THREE.Mesh(new THREE.RingGeometry(baseR, baseR + 0.15, 64), createMat(0x888888, 0.1, false, THREE.DoubleSide)); | |
| pring.rotation.x = -Math.PI / 2; | |
| pring.position.y = 0.05; | |
| pring.userData = { baseRadius: baseR, speed: 0.3 + pri * 0.1 }; | |
| scene.add(pring); pulseRings.push(pring); | |
| } | |
| var pCount = 3000; | |
| var pGeo = new THREE.BufferGeometry(); | |
| var pPos = new Float32Array(pCount * 3); | |
| var pVel = new Float32Array(pCount); | |
| for (var ppi = 0; ppi < pCount; ppi++) { | |
| pPos[ppi * 3] = rand(-90, 90); | |
| pPos[ppi * 3 + 1] = rand(0, 60); | |
| pPos[ppi * 3 + 2] = rand(-90, 90); | |
| pVel[ppi] = rand(0.03, 0.18); | |
| } | |
| pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3)); | |
| var pMat = new THREE.PointsMaterial({ color: 0x444444, size: 0.2, transparent: true, opacity: 0.35, sizeAttenuation: true }); | |
| var particleSystem = new THREE.Points(pGeo, pMat); | |
| scene.add(particleSystem); | |
| var streams = []; | |
| for (var sti = 0; sti < 40; sti++) { | |
| var stx = rand(-80, 80), stz = rand(-80, 80), sth = rand(15, 50); | |
| var stPoints = []; | |
| for (var stj = 0; stj <= 80; stj++) stPoints.push(new THREE.Vector3(stx, stj * sth / 80, stz)); | |
| var sLine = createLine(stPoints, createLineMat(0x555555, 0.08)); | |
| sLine.userData = { baseOpacity: rand(0.04, 0.15), speed: rand(0.5, 3), offset: rand(0, 6.28) }; | |
| scene.add(sLine); streams.push(sLine); | |
| } | |
| var connections = []; | |
| for (var cni = 0; cni < 30; cni++) { | |
| var cnx1 = rand(-70, 70), cnz1 = rand(-70, 70); | |
| var cnx2 = cnx1 + rand(-25, 25), cnz2 = cnz1 + rand(-25, 25), cny = rand(3, 35); | |
| var cnPts = []; | |
| for (var cnj = 0; cnj <= 30; cnj++) { | |
| var cnt = cnj / 30; | |
| cnPts.push(new THREE.Vector3(cnx1 + (cnx2 - cnx1) * cnt, cny + Math.sin(cnt * Math.PI) * rand(1, 5), cnz1 + (cnz2 - cnz1) * cnt)); | |
| } | |
| var cLine = createLine(cnPts, createLineMat(0x666666, 0.06)); | |
| cLine.userData = { speed: rand(0.5, 2.5), offset: rand(0, 6.28) }; | |
| scene.add(cLine); connections.push(cLine); | |
| } | |
| var packets = []; | |
| for (var pki = 0; pki < 40; pki++) { | |
| var packet = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, 0.3), createMat(0x333333, 0.5)); | |
| packet.userData = { pathIndex: randInt(0, connections.length), t: Math.random(), speed: rand(0.002, 0.01) }; | |
| scene.add(packet); packets.push(packet); | |
| } | |
| var holoCircles = []; | |
| for (var hci = 0; hci < 6; hci++) { | |
| var hcr = rand(1, 4); | |
| var hc = new THREE.Mesh(new THREE.RingGeometry(hcr, hcr + 0.08, 32), createMat(0x777777, rand(0.04, 0.12), false, THREE.DoubleSide)); | |
| hc.position.set(rand(-40, 40), rand(10, 40), rand(-40, 40)); | |
| hc.userData = { rotSpeed: rand(0.2, 1), bobSpeed: rand(0.3, 1), bobAmp: rand(0.5, 2), baseY: hc.position.y }; | |
| scene.add(hc); holoCircles.push(hc); | |
| } | |
| var frame = 0; | |
| var clockObj = new THREE.Clock(); | |
| function updateElement(id, value) { document.getElementById(id).textContent = value; } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| var t = clockObj.getElapsedTime(); | |
| frame++; | |
| radius += (targetRadius - radius) * 0.06; | |
| if (!dragging) angleX -= 0.0012; | |
| camera.position.set(Math.sin(angleX) * radius, 8 + angleY * radius * 0.55, Math.cos(angleX) * radius); | |
| camera.lookAt(0, 10, 0); | |
| for (var bli = 0; bli < blinkLights.length; bli++) { | |
| var bl = blinkLights[bli]; | |
| var val = Math.abs(Math.sin(t * bl.userData.blink + bl.userData.offset)); | |
| bl.material.opacity = 0.2 + 0.8 * val; | |
| bl.scale.setScalar(0.8 + 0.4 * val); | |
| } | |
| for (var wpi = 0; wpi < windowPoints.length; wpi++) { | |
| windowPoints[wpi].material.opacity = 0.2 + 0.4 * Math.abs(Math.sin(t * 1.5 + windowPoints[wpi].id)); | |
| } | |
| var posArr = particleSystem.geometry.attributes.position.array; | |
| for (var pai = 0; pai < pCount; pai++) { | |
| posArr[pai * 3 + 1] += pVel[pai]; | |
| if (posArr[pai * 3 + 1] > 65) { | |
| posArr[pai * 3 + 1] = 0; | |
| posArr[pai * 3] = rand(-90, 90); | |
| posArr[pai * 3 + 2] = rand(-90, 90); | |
| } | |
| } | |
| particleSystem.geometry.attributes.position.needsUpdate = true; | |
| pMat.opacity = 0.2 + 0.15 * Math.sin(t * 0.5); | |
| for (var sti2 = 0; sti2 < streams.length; sti2++) { | |
| var st = streams[sti2]; | |
| st.material.opacity = st.userData.baseOpacity * (0.5 + 0.5 * Math.sin(t * st.userData.speed + st.userData.offset)); | |
| } | |
| for (var cni2 = 0; cni2 < connections.length; cni2++) { | |
| var cn = connections[cni2]; | |
| cn.material.opacity = 0.03 + 0.1 * Math.abs(Math.sin(t * cn.userData.speed + cn.userData.offset)); | |
| } | |
| for (var pki2 = 0; pki2 < packets.length; pki2++) { | |
| var pk = packets[pki2]; | |
| pk.userData.t += pk.userData.speed; | |
| if (pk.userData.t > 1) pk.userData.t = 0; | |
| var conn = connections[pk.userData.pathIndex]; | |
| if (conn) { | |
| var connPos = conn.geometry.attributes.position.array; | |
| var totalPts = connPos.length / 3; | |
| var idx = Math.floor(pk.userData.t * (totalPts - 1)); | |
| var nextIdx = Math.min(idx + 1, totalPts - 1); | |
| var frac2 = (pk.userData.t * (totalPts - 1)) - idx; | |
| pk.position.x = connPos[idx * 3] + (connPos[nextIdx * 3] - connPos[idx * 3]) * frac2; | |
| pk.position.y = connPos[idx * 3 + 1] + (connPos[nextIdx * 3 + 1] - connPos[idx * 3 + 1]) * frac2; | |
| pk.position.z = connPos[idx * 3 + 2] + (connPos[nextIdx * 3 + 2] - connPos[idx * 3 + 2]) * frac2; | |
| } | |
| pk.rotation.x += 0.05; pk.rotation.y += 0.03; | |
| } | |
| for (var pri2 = 0; pri2 < pulseRings.length; pri2++) { | |
| var pr = pulseRings[pri2]; | |
| var ps = 1 + 0.3 * Math.sin(t * pr.userData.speed); | |
| pr.scale.set(ps, ps, 1); | |
| pr.material.opacity = 0.04 + 0.08 * Math.abs(Math.sin(t * pr.userData.speed)); | |
| } | |
| for (var hci2 = 0; hci2 < holoCircles.length; hci2++) { | |
| var hc2 = holoCircles[hci2]; | |
| hc2.rotation.x += hc2.userData.rotSpeed * 0.01; | |
| hc2.rotation.y += hc2.userData.rotSpeed * 0.015; | |
| hc2.position.y = hc2.userData.baseY + Math.sin(t * hc2.userData.bobSpeed) * hc2.userData.bobAmp; | |
| } | |
| for (var tri2 = 0; tri2 < trafficParticles.length; tri2++) { | |
| var tp = trafficParticles[tri2]; | |
| tp.mesh.position.x += tp.dir.x * tp.speed; | |
| tp.mesh.position.z += tp.dir.z * tp.speed; | |
| if (tp.mesh.position.x > cityExtent + 5) tp.mesh.position.x = -cityExtent - 5; | |
| if (tp.mesh.position.x < -cityExtent - 5) tp.mesh.position.x = cityExtent + 5; | |
| if (tp.mesh.position.z > cityExtent + 5) tp.mesh.position.z = -cityExtent - 5; | |
| if (tp.mesh.position.z < -cityExtent - 5) tp.mesh.position.z = cityExtent + 5; | |
| } | |
| // Animate gondolas | |
| var gondolas = sugarLoaf.gondolas; | |
| for (var gi = 0; gi < gondolas.length; gi++) { | |
| var gond = gondolas[gi]; | |
| gond.userData.t += gond.userData.speed; | |
| if (gond.userData.t > 1) gond.userData.t = 0; | |
| var gCable = gond.userData.cablePoints; | |
| var gIdx = Math.floor(gond.userData.t * (gCable.length - 1)); | |
| var gNext = Math.min(gIdx + 1, gCable.length - 1); | |
| var gFrac = (gond.userData.t * (gCable.length - 1)) - gIdx; | |
| gond.position.lerpVectors(gCable[gIdx], gCable[gNext], gFrac); | |
| gond.position.y += gond.userData.hangOffset; | |
| } | |
| // Animate ocean waves | |
| for (var wvi = 0; wvi < waveVertices.length; wvi++) { | |
| var wv = waveVertices[wvi]; | |
| var wvPos = wv.geo.attributes.position.array; | |
| var wvBase = wv.baseVerts; | |
| var wvTotal = wvPos.length / 3; | |
| for (var wvj = 0; wvj < wvTotal; wvj++) { | |
| var bx2 = wvBase[wvj * 3]; | |
| var by2 = wvBase[wvj * 3 + 1]; | |
| var bz2 = wvBase[wvj * 3 + 2]; | |
| var waveOffset = Math.sin(bx2 * 0.08 + t * 1.2 + wv.layer) * 0.3 | |
| + Math.cos(bz2 * 0.06 + t * 0.8) * 0.2 | |
| + Math.sin((bx2 + bz2) * 0.04 + t * 0.5) * 0.15; | |
| wvPos[wvj * 3 + 1] = by2 + waveOffset; | |
| } | |
| wv.geo.attributes.position.needsUpdate = true; | |
| wv.geo.computeVertexNormals(); | |
| } | |
| // Animate wave shore lines | |
| for (var wli2 = 0; wli2 < waveLines.length; wli2++) { | |
| var wlObj = waveLines[wli2]; | |
| if (wlObj.userData && wlObj.userData.baseY !== undefined) { | |
| wlObj.position.y = wlObj.userData.baseY + Math.sin(t * wlObj.userData.bobSpeed + wlObj.userData.bobPhase) * wlObj.userData.bobAmp; | |
| wlObj.rotation.z = Math.sin(t * wlObj.userData.rockSpeed + wlObj.userData.bobPhase) * wlObj.userData.rockAmp; | |
| wlObj.rotation.x = Math.cos(t * wlObj.userData.rockSpeed * 0.7 + wlObj.userData.bobPhase) * wlObj.userData.rockAmp * 0.5; | |
| continue; | |
| } | |
| if (wlObj.geometry && wlObj.userData && wlObj.userData.baseOffset !== undefined) { | |
| var wlPositions = wlObj.geometry.attributes.position.array; | |
| var wlCount = wlPositions.length / 3; | |
| var wlPulse = Math.sin(t * wlObj.userData.speed + wlObj.userData.phase) * 1.5; | |
| for (var wlvi = 0; wlvi < wlCount; wlvi++) { | |
| wlPositions[wlvi * 3 + 1] = -0.1 + Math.sin(t * 2 + wlvi * 0.3 + wlObj.userData.phase) * 0.08; | |
| } | |
| wlObj.geometry.attributes.position.needsUpdate = true; | |
| wlObj.material.opacity = 0.05 + 0.1 * Math.abs(Math.sin(t * wlObj.userData.speed + wlObj.userData.phase)); | |
| } | |
| } | |
| if (frame % 20 === 0) { | |
| updateElement('freq', (400 + Math.random() * 100).toFixed(1)); | |
| updateElement('nodes', Math.floor(2500 + Math.random() * 500)); | |
| updateElement('latency', Math.floor(8 + Math.random() * 15)); | |
| updateElement('traffic', Math.floor(trafficCount * (0.8 + Math.random() * 0.4))); | |
| } | |
| if (frame % 120 === 0) { | |
| var sectors = ['7G-NORTH','4A-EAST','9C-SOUTH','2F-WEST','5B-CENTRAL','3D-UPTOWN','8E-HARBOR','PÃO-AÇÚCAR','COPACABANA','IPANEMA']; | |
| updateElement('sector', sectors[randInt(0, sectors.length)]); | |
| var dstreams = ['ACTIVE','SYNC','BURST','PEAK','IDLE','TIDAL']; | |
| updateElement('datastream', dstreams[randInt(0, dstreams.length)]); | |
| } | |
| updateElement('frame', frame); | |
| updateElement('blocks', totalBlocks); | |
| if (frame % 4 === 0) { | |
| var df = ''; | |
| for (var dfi = 0; dfi < 14; dfi++) df += Math.random() > 0.5 ? '█' : '░'; | |
| updateElement('dataflow', df); | |
| } | |
| var now = new Date(); | |
| var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds(); | |
| updateElement('clock', (hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| })(); | |
| </script> | |
| </body></html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment