Files
2026-10-27 01:21:30 +00:00

1807 lines
72 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gökbey-9 · Fırlatma Rampası 3B</title>
<!-- NUMEX-DÜZELTME (2026-10-22): Tarayıcı /favicon.ico istiyordu ve sunucu 404 dönüyordu. İstek hiç doğmasın diye satır içi SVG favicon eklendi — harici dosya gerekmez, 404 ortadan kalkar. -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230a1018'/%3E%3Cpath d='M16 4c3.2 4.2 4.8 8.4 4.8 13.6 0 3.2-1 6.4-1 6.4h-7.6s-1-3.2-1-6.4C11.2 12.4 12.8 8.2 16 4z' fill='%2364c8ff'/%3E%3Cpath d='M11.6 24h8.8l-1.6 4.4h-5.6z' fill='%23f0a06a'/%3E%3C/svg%3E" />
<style>
:root{
--cam: rgba(10, 16, 26, .55);
--cam-brd: rgba(150, 190, 230, .18);
--txt: #e8eef6;
--dim: #93a4b8;
--acc: #64c8ff;
}
*{ box-sizing: border-box; margin: 0; padding: 0; }
html, body{ height: 100%; overflow: hidden; background: #0a1018; }
body{
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
color: var(--txt);
-webkit-font-smoothing: antialiased;
}
#sahne{ position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; }
#sahne:active{ cursor: grabbing; }
.katman{ position: fixed; z-index: 10; pointer-events: none; }
.panel{
background: var(--cam);
border: 1px solid var(--cam-brd);
border-radius: 10px;
backdrop-filter: blur(10px) saturate(1.2);
-webkit-backdrop-filter: blur(10px) saturate(1.2);
pointer-events: auto;
}
/* ---------- Sol üst başlık ---------- */
#baslik{ top: 18px; left: 18px; padding: 14px 18px; }
#baslik h1{
font-size: clamp(16px, 1.6vw, 22px);
font-weight: 600;
letter-spacing: .5px;
display: flex; align-items: center; gap: 9px;
}
#baslik h1 span.nokta{
width: 7px; height: 7px; border-radius: 50%;
background: var(--acc);
box-shadow: 0 0 10px var(--acc);
animation: nefes 2.4s ease-in-out infinite;
}
@keyframes nefes{ 0%,100%{ opacity:.45; transform: scale(.85);} 50%{ opacity:1; transform: scale(1);} }
#baslik .alt{
margin-top: 5px;
font-size: 11px;
color: var(--dim);
letter-spacing: 1.6px;
text-transform: uppercase;
}
/* ---------- Sekmeler (Genel bakış / Yapı / Fırlatma) ---------- */
#sekmeler{
top: 18px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px; padding: 5px; z-index: 12;
}
.sekme{
font-family: inherit; font-size: 12px; letter-spacing: .6px;
color: var(--dim); background: none; border: none; border-radius: 6px;
padding: 8px 18px; cursor: pointer; transition: .2s; white-space: nowrap;
}
.sekme:hover{ color: var(--txt); background: rgba(255,255,255,.06); }
.sekme.aktif{ color: #04121c; background: var(--acc); font-weight: 700; }
/* Ölçü katmanı */
#olcuKatman{ position: fixed; inset: 0; pointer-events: none; z-index: 6; }
.olcuCizgi{ position: absolute; background: rgba(100,200,255,.55); }
.olcuCizgi.dikey{ width: 1px; }
.olcuCizgi.yatay{ height: 1px; }
.olcuUc{ position: absolute; width: 7px; height: 7px; border: 1px solid rgba(100,200,255,.8);
background: rgba(10,16,26,.8); border-radius: 50%; transform: translate(-50%,-50%); }
.olcuEtiket{
position: absolute; transform: translate(-50%,-50%);
font-size: 11px; font-variant-numeric: tabular-nums; letter-spacing: .5px;
color: #cfe6fb; background: rgba(10,16,26,.82); border: 1px solid rgba(100,200,255,.35);
border-radius: 4px; padding: 3px 8px; white-space: nowrap;
}
.olcuNot{
position: absolute; bottom: 96px; left: 50%; transform: translateX(-50%);
font-size: 10px; letter-spacing: 1.2px; color: rgba(147,164,184,.75);
text-transform: uppercase;
}
/* Fırlatma paneli */
#firlatmaPanel{ bottom: 96px; left: 50%; transform: translateX(-50%);
padding: 12px 20px; display: none; text-align: center; min-width: 260px; }
#firlatmaPanel.acik{ display: block; }
#geriSayim{
font-size: 40px; font-weight: 200; letter-spacing: 3px;
font-variant-numeric: tabular-nums; line-height: 1.1;
color: var(--acc); text-shadow: 0 0 22px rgba(100,200,255,.5);
}
#asamaAd{ font-size: 11px; letter-spacing: 2px; color: var(--dim); text-transform: uppercase; margin-top: 4px; }
/* ---------- Sağ üst performans (yalnız geliştirici kipi) ---------- */
#perf{ top: 18px; right: 18px; padding: 11px 14px; min-width: 176px;
display: none; }
#perf.acik{ display: block; }
#perf .satir{
display: flex; justify-content: space-between; align-items: baseline;
font-size: 11px; color: var(--dim); letter-spacing: .6px;
padding: 3px 0;
}
#perf .satir b{
color: var(--txt); font-weight: 600;
font-variant-numeric: tabular-nums;
font-size: 12px;
}
#perf .dil{
margin-top: 8px; display: flex; gap: 6px;
border-top: 1px solid var(--cam-brd); padding-top: 9px;
}
.dugme{
font-family: inherit; font-size: 11px;
color: var(--dim); background: rgba(255,255,255,.05);
border: 1px solid var(--cam-brd); border-radius: 6px;
padding: 6px 10px; cursor: pointer;
transition: .18s; letter-spacing: .5px;
}
.dugme:hover{ color: var(--txt); background: rgba(100,200,255,.14); border-color: rgba(100,200,255,.4); }
.dugme.aktif{ color: #04121c; background: var(--acc); border-color: var(--acc); font-weight: 700; }
/* ---------- Alt orta zaman çubuğu ---------- */
#zaman{
bottom: 22px; left: 50%; transform: translateX(-50%);
padding: 12px 18px; width: min(520px, calc(100vw - 36px));
}
#zamanBasliklar{
display: flex; justify-content: space-between;
font-size: 11px; color: var(--dim); letter-spacing: .8px;
margin-bottom: 9px;
}
#zamanBasliklar span{ transition: color .2s; }
#zamanBasliklar span.secili{ color: var(--acc); font-weight: 700; }
#kaydirici{
-webkit-appearance: none; appearance: none;
width: 100%; height: 4px; border-radius: 4px;
background: linear-gradient(90deg,#f0a06a 0%, #8fc7ee 33%, #e8894f 66%, #1b2540 100%);
outline: none; cursor: pointer;
}
#kaydirici::-webkit-slider-thumb{
-webkit-appearance: none; appearance: none;
width: 16px; height: 16px; border-radius: 50%;
background: #fff; border: 2px solid var(--acc);
box-shadow: 0 0 12px rgba(100,200,255,.7);
cursor: pointer;
}
#kaydirici::-moz-range-thumb{
width: 16px; height: 16px; border: 2px solid var(--acc);
border-radius: 50%; background: #fff;
box-shadow: 0 0 12px rgba(100,200,255,.7); cursor: pointer;
}
/* ---------- İpucu ---------- */
#ipucu{
bottom: 24px; left: 18px;
font-size: 11px; color: rgba(200,215,235,.5);
letter-spacing: .3px; max-width: 210px; line-height: 1.6;
}
/* ---------- Parça bilgi kartı ---------- */
#kart{
top: 50%; right: 18px; transform: translateY(-50%) translateX(calc(100% + 30px));
width: min(310px, calc(100vw - 36px));
padding: 18px 20px;
transition: transform .35s cubic-bezier(.22,.9,.3,1);
}
#kart.acik{ transform: translateY(-50%) translateX(0); }
#kart h2{ font-size: 15px; font-weight: 600; margin-bottom: 4px; }
#kart .etiket{
font-size: 10px; color: var(--acc); letter-spacing: 2px;
text-transform: uppercase; margin-bottom: 10px; display: block;
}
#kart p{ font-size: 12.5px; line-height: 1.75; color: #c3d0e0; }
#kart .kapat{
position: absolute; top: 10px; right: 12px;
background: none; border: none; color: var(--dim);
font-size: 18px; cursor: pointer; line-height: 1; padding: 4px;
}
#kart .kapat:hover{ color: var(--txt); }
#kart .olcu{
margin-top: 14px; padding-top: 12px;
border-top: 1px solid var(--cam-brd);
font-size: 11px; color: var(--dim); display: flex; justify-content: space-between;
}
/* ---------- Yükleniyor ---------- */
#yukleniyor{
position: fixed; inset: 0; z-index: 100;
background: #0a1018;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 16px; transition: opacity .6s;
}
#yukleniyor.bitti{ opacity: 0; pointer-events: none; }
#yukleniyor .cizgi{
width: 160px; height: 2px; background: rgba(255,255,255,.1); overflow: hidden; border-radius: 2px;
}
#yukleniyor .cizgi::after{
content: ""; display: block; width: 40%; height: 100%;
background: var(--acc); animation: akip 1.1s ease-in-out infinite;
}
@keyframes akip{ 0%{ transform: translateX(-100%);} 100%{ transform: translateX(350%);} }
#yukleniyor span{ font-size: 12px; color: var(--dim); letter-spacing: 2px; }
@media (max-width: 760px){
#ipucu{ display: none; }
#kart{ top: auto; bottom: 108px; right: 12px; transform: translateX(calc(100% + 30px)); }
#kart.acik{ transform: translateX(0); }
#perf{ padding: 8px 10px; min-width: 140px; }
#baslik{ padding: 10px 12px; }
}
</style>
</head>
<body>
<canvas id="sahne"></canvas>
<div id="yukleniyor">
<span>SAHNE HAZIRLANIYOR</span>
<div class="cizgi"></div>
</div>
<div id="baslik" class="katman panel">
<h1><span class="nokta"></span>Gökbey-9</h1>
<div class="alt">Fırlatma Rampası · Canlı Sahne</div>
</div>
<div id="sekmeler" class="katman panel">
<button class="sekme aktif" data-bolum="genel">Genel Bakış</button>
<button class="sekme" data-bolum="yapi">Yapı</button>
<button class="sekme" data-bolum="firlatma">Fırlatma</button>
</div>
<div id="perf" class="katman panel">
<div class="satir"><span>FPS</span><b id="pFps">--</b></div>
<div class="satir"><span>Üçgen</span><b id="pUcgen">--</b></div>
<div class="satir"><span>Çizim çağrısı</span><b id="pCizim">--</b></div>
<div class="dil">
<button class="dugme" id="btnSifirla">Kamerayı Sıfırla</button>
<button class="dugme" id="btnParalel">Kamera: Paralel</button>
</div>
</div>
<!-- Ölçü çizgileri katmanı (Yapı bölümünde görünür) -->
<div id="olcuKatman"></div>
<!-- Fırlatma bölümü paneli -->
<div id="firlatmaPanel" class="katman panel">
<div id="geriSayim">T-10</div>
<div id="asamaAd">Hazırlık</div>
</div>
<div id="zaman" class="katman panel">
<div id="zamanBasliklar">
<span data-t="0">Şafak</span>
<span data-t="1">Öğle</span>
<span data-t="2">Alacakaranlık</span>
<span data-t="3">Gece</span>
</div>
<input type="range" id="kaydirici" min="0" max="1000" value="0" />
</div>
<div id="ipucu" class="katman">
Sürükle: döndür<br />
Tekerlek: yakınlaştır<br />
Parçaya tıkla: bilgi
</div>
<div id="kart" class="katman panel">
<button class="kapat" id="kartKapat">×</button>
<span class="etiket">Parça</span>
<h2 id="kartBaslik">—</h2>
<p id="kartMetin">—</p>
<div class="olcu"><span id="kartYukseklik"></span><span id="kartAgirlik"></span></div>
</div>
<script type="importmap">
{
"imports": {
"three": "./vendor/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
/* =========================================================
GÖKBEY-9 · FIRLATMA RAMPASI
Tek dosya, sıfır harici model. Tüm geometri kodla üretilir.
========================================================= */
const tuval = document.getElementById('sahne');
const pFps = document.getElementById('pFps');
const pUcgen = document.getElementById('pUcgen');
const pCizim = document.getElementById('pCizim');
const btnSifirla = document.getElementById('btnSifirla');
const btnParalel = document.getElementById('btnParalel');
const sahne = new THREE.Scene();
const kamera = new THREE.PerspectiveCamera(48, innerWidth / innerHeight, 0.5, 4000);
const KAMERA_BASLANGIC = new THREE.Vector3(72, 44, 78);
kamera.position.copy(KAMERA_BASLANGIC);
const cizici = new THREE.WebGLRenderer({ canvas: tuval, antialias: true, powerPreference: 'high-performance' });
// NUMEX-DÜZELTME (2026-10-22): Tuval ekranın tamamını kaplamıyordu: setSize innerWidth/innerHeight ile bir kez ayarlanıyor, ama #sahne CSS'te inset:0 ile konumlandığı için gerçek ölçü CSS kutusundan gelmeli. Boyutu elemanın kendi ölçüsünden alıyorum.
// NUMEX-DÜZELTME (2026-10-22): Kök neden: getBoundingClientRect() ile ölçüp setSize(...,false) çağırmak kısır döngü yarattı — canvas henüz boyutlanmadığı için CSS kutusu 300×150 dönüyor, setSize CSS'i güncellemediği için de öyle kalıyordu; sahne 300×150'ye çöküyordu. Pencere ölçüsüne dönüldü, CSS de güncelleniyor.
// Tuval boyutu: pencere ölçüsü (CSS de güncellenir — updateStyle varsayılan true)
function tuvalBoyutla(){
cizici.setSize(Math.max(1, window.innerWidth), Math.max(1, window.innerHeight));
}
tuvalBoyutla();
cizici.setPixelRatio(1); // 2 yerine 1: retina olmayan ekranlarda piksel maliyeti 4 kat azalır
cizici.shadowMap.enabled = true;
cizici.shadowMap.type = THREE.PCFSoftShadowMap; // blok gölge yerine yumuşak kenar
cizici.toneMapping = THREE.ACESFilmicToneMapping;
cizici.toneMappingExposure = 1.0;
/* ---------------- Yörünge kontrolü (bağımlılıksız) ---------------- */
// NUMEX-DÜZELTME (2026-10-22): Referans görselde kamera alçak ve ufka yakın bakıyor (ufuk ekranın ortasında, kompleks silüet halinde). Önceki açı kuşbakışıydı; açıyı düşürüp hedefi yukarı aldım.
// NUMEX-DÜZELTME (2026-10-22): Açılışta kompleks ekranın ~%25'ini kaplıyordu (istek: roket ekranın yarısından fazlası). Mesafeyi 205'ten 118'e çekip hedefi roketin orta yüksekliğine (y=33) aldım; roket dikey karede ~%62 doluluk veriyor.
// NUMEX-DÜZELTME (2026-10-22): Roket ekranın ~%30'unda kalıyordu; kule, küreler ve paratonerler kadrajı dolduruyordu. Referans açıyı koruyup mesafeyi 118→72'ye çekip hedefi roketin gövde ortasına (y=36) aldım; roket dikey karede ~%62 doluluk veriyor.
// NUMEX-DÜZELTME (2026-10-22): 72 birimde roketin burnu ve nozulları kadraj dışına taşıyordu; paratoner direkleri ön planda ekranı ikiye bölüyordu. Mesafeyi 96'ya, dik bakış açısını 0.10'a aldım; roketin tamamı (y=3.6..60.4) görünür ve dikeyde ~%58 doluluk verir. Yatayda da kule ve çevre sığar.
// NUMEX-DÜZELTME (2026-10-22): Roket ekranın ~%35'inde kalıyordu ve ön plandaki paratoner direkleri kadrajı ikiye bölüyordu. Açıyı yandan (0.72) daha öne (1.05) alıp mesafeyi 96→78'e indirdim; direkler artık roketin önünden geçmiyor, roket dikeyde ~%62 doluluk veriyor.
// NUMEX-DÜZELTME (2026-10-22): Paratoner direkleri 45/135/225/315 derecede, 40 birim yarıçapta duruyor; kamera her açıda bir direğin arkasına düşüp roketi kesiyordu. Kamera açısını 0.72 rad (~41°) yerine 1.571 rad (90°, direk boşluğu) aldım — roketin önünden direk geçmez.
// Paratonerler 45/135/225/315°'de durur; kamera 90°'de (direk boşluğu) konumlanır.
// NUMEX-DÜZELTME (2026-10-22): 84 birimde kamera çok yaklaşmış: ön plandaki yakıt küresi kadrajın dörtte birini kaplıyor ve kule rokete yapışık görünüyordu. Mesafeyi 105'e, hedef yüksekliğini 33'e aldım; roket dikeyde ~%55 doluluk verir, küre ve kule ayrışır.
const KAMERA_SIFIR = { aciX: 0.13, aciY: 1.50, mesafe: 105, hedef: new THREE.Vector3(0, 33, 0) };
const kontroller = {
hedef: KAMERA_SIFIR.hedef.clone(),
aciX: KAMERA_SIFIR.aciX, // dikey (küçük = ufka yakın, engin görünüm)
aciY: KAMERA_SIFIR.aciY, // yatay
mesafe: KAMERA_SIFIR.mesafe,
hedefMesafe: KAMERA_SIFIR.mesafe,
surukle: false,
sonX: 0, sonY: 0,
// Her karede çağrılan tek güncelleme noktası
update: function(){
this.mesafe += (this.hedefMesafe - this.mesafe) * 0.12; // yumuşak yakınlaştırma
kamerayiKonumla();
}
};
function kamerayiKonumla(){
const d = kontroller.mesafe;
const x = kontroller.hedef.x + d * Math.cos(kontroller.aciX) * Math.sin(kontroller.aciY);
const y = kontroller.hedef.y + d * Math.sin(kontroller.aciX);
const z = kontroller.hedef.z + d * Math.cos(kontroller.aciX) * Math.cos(kontroller.aciY);
kamera.position.set(x, y, z);
kamera.lookAt(kontroller.hedef);
// NUMEX-DÜZELTME (2026-10-22): Kök neden: orto kamera aktifken konumu 'kamera' (perspektif) nesnesinden kopyalanıyordu; perspektif kamera orto modda güncellenmediği için değerleri bayat/yanlış kalıyor ve paralel görünüm ters dönüyordu. Artık iki kamera da aynı yörünge hesabından besleniyor.
// Paralel kamera seçiliyse onu da AYNI yörünge hesabından konumla
if (aktifKamera !== kamera){
aktifKamera.position.set(x, y, z);
aktifKamera.lookAt(kontroller.hedef);
aktifKamera.updateMatrixWorld();
}
}
let aktifKamera = kamera; // varsayılan: perspektif
kamerayiKonumla();
tuval.addEventListener('pointerdown', e => {
kontroller.surukle = true;
kontroller.sonX = e.clientX; kontroller.sonY = e.clientY;
tuval.setPointerCapture(e.pointerId);
});
tuval.addEventListener('pointerup', e => {
kontroller.surukle = false;
try { tuval.releasePointerCapture(e.pointerId); } catch (_) {}
});
tuval.addEventListener('pointermove', e => {
if (!kontroller.surukle) return;
const dx = e.clientX - kontroller.sonX;
const dy = e.clientY - kontroller.sonY;
kontroller.sonX = e.clientX; kontroller.sonY = e.clientY;
kontroller.aciY -= dx * 0.005;
kontroller.aciX = Math.max(0.12, Math.min(1.45, kontroller.aciX + dy * 0.004));
});
tuval.addEventListener('wheel', e => {
e.preventDefault();
kontroller.hedefMesafe = Math.max(34, Math.min(240, kontroller.hedefMesafe + e.deltaY * 0.09));
}, { passive: false });
// Dokunmatik iki parmak yakınlaştırma
let parmakMesafe = null;
tuval.addEventListener('touchmove', e => {
if (e.touches.length !== 2) return;
e.preventDefault();
const a = e.touches[0], b = e.touches[1];
const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
if (parmakMesafe !== null) {
kontroller.hedefMesafe = Math.max(34, Math.min(240, kontroller.hedefMesafe - (d - parmakMesafe) * 0.35));
}
parmakMesafe = d;
}, { passive: false });
tuval.addEventListener('touchend', () => { parmakMesafe = null; });
/* Kamera sıfırlama yukarıda (KAMERA_SIFIR ile) tek yerden yönetiliyor. */
/* ---------------- Gökyüzü (zaman dilimine göre) ---------------- */
const gokyuzuUniform = {
ustRenk: { value: new THREE.Color(0x1d3a5f) },
ortaRenk: { value: new THREE.Color(0xd88a55) },
altRenk: { value: new THREE.Color(0x14202e) },
gunesYon: { value: new THREE.Vector3(0.6, 0.12, -1).normalize() },
gunesGuc: { value: 1.0 },
geceFaktoru: { value: 0.0 }
};
const gokyuzuMalzeme = new THREE.ShaderMaterial({
uniforms: gokyuzuUniform,
side: THREE.BackSide,
depthWrite: false,
vertexShader: `
varying vec3 vYon;
void main(){
vYon = normalize(position);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 ustRenk; uniform vec3 ortaRenk; uniform vec3 altRenk;
uniform vec3 gunesYon; uniform float gunesGuc; uniform float geceFaktoru;
varying vec3 vYon;
// Sade, tekrarlanabilir yıldız gürültüsü
float yildizKatmani(vec3 yon, float olcek){
vec3 p = yon * olcek;
vec3 hucre = floor(p);
float rast = fract(sin(dot(hucre, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
float esik = step(0.9965, rast); // çok seyrek noktalar
vec3 merkez = hucre + 0.5 + 0.12 * vec3(
fract(rast * 13.7), fract(rast * 7.3), fract(rast * 3.1));
float d = length(p - merkez);
return esik * smoothstep(0.34, 0.0, d);
}
void main(){
// Ufukta sıcak/parlak renk, tepede koyu; orta bant yumuşak geçiş
float h = clamp(vYon.y, 0.0, 1.0);
float h2 = pow(h, 0.55);
vec3 renk = mix(altRenk, ortaRenk, smoothstep(0.0, 0.22, h2));
renk = mix(renk, ustRenk, smoothstep(0.14, 0.85, h2));
// Gündüz güneş parlaması (gece tamamen söner)
float gunduz = 1.0 - geceFaktoru;
float g = max(dot(normalize(vYon), normalize(gunesYon)), 0.0);
// NUMEX-DÜZELTME (2026-10-22): Gece modunda ufuk turuncu kalıyordu: pow(g,8) geniş açılı sıcak halo katmanı geceFaktoru 0 olsa bile ufku boyuyordu. Halo katmanının etkisini gece katsayısıyla ikinci kez, daha sert söndürüyorum (gunduz^2) ve dar tutuyorum.
renk += vec3(1.0, 0.72, 0.42) * pow(g, 120.0) * 2.0 * gunesGuc * gunduz;
renk += vec3(1.0, 0.62, 0.32) * pow(g, 12.0) * 0.16 * gunesGuc * gunduz * gunduz;
// Gece yıldızları (ufka yakın bölgede kaybolur)
if (geceFaktoru > 0.02){
float y1 = yildizKatmani(vYon, 220.0);
float y2 = yildizKatmani(vYon + 37.0, 460.0) * 0.6;
float gorunurluk = smoothstep(0.03, 0.30, vYon.y) * geceFaktoru;
renk += vec3(0.85, 0.9, 1.0) * (y1 + y2) * gorunurluk * 1.6;
}
gl_FragColor = vec4(renk, 1.0);
}`
});
const gokyuzu = new THREE.Mesh(new THREE.SphereGeometry(1600, 32, 20), gokyuzuMalzeme);
sahne.add(gokyuzu);
sahne.fog = new THREE.FogExp2(0x9a7a68, 0.0032);
/* ---------------- Işıklar ---------------- */
const gunes = new THREE.DirectionalLight(0xffcf9e, 2.1);
gunes.position.set(70, 34, -90);
gunes.castShadow = true;
gunes.shadow.mapSize.set(2048, 2048);
gunes.shadow.camera.near = 10;
gunes.shadow.camera.far = 320;
gunes.shadow.camera.left = -110;
gunes.shadow.camera.right = 110;
gunes.shadow.camera.top = 110;
gunes.shadow.camera.bottom = -110;
gunes.shadow.bias = -0.0006;
sahne.add(gunes);
const gokyuzuIsik = new THREE.HemisphereLight(0x9dc4e8, 0x3a2a20, 0.75);
sahne.add(gokyuzuIsik);
const dolguIsik = new THREE.DirectionalLight(0x88aaff, 0.35);
dolguIsik.position.set(-80, 30, 60);
sahne.add(dolguIsik);
/* ---------------- Malzemeler ---------------- */
const MAL = {
beton: new THREE.MeshStandardMaterial({ color: 0x8b8b86, roughness: 0.95, metalness: 0.02 }),
betonKoyu:new THREE.MeshStandardMaterial({ color: 0x5c5c58, roughness: 0.95, metalness: 0.02 }),
toprak: new THREE.MeshStandardMaterial({ color: 0x8a7a5f, roughness: 1.0, metalness: 0.0 }),
govde: new THREE.MeshStandardMaterial({ color: 0xe8e9ec, roughness: 0.42, metalness: 0.22 }),
govdeKoyu:new THREE.MeshStandardMaterial({ color: 0x2b2f36, roughness: 0.55, metalness: 0.35 }),
metal: new THREE.MeshStandardMaterial({ color: 0x9aa4ae, roughness: 0.42, metalness: 0.85 }),
metalKoyu:new THREE.MeshStandardMaterial({ color: 0x4a525c, roughness: 0.6, metalness: 0.7 }),
turuncu: new THREE.MeshStandardMaterial({ color: 0xd9622b, roughness: 0.62, metalness: 0.15 }),
kirmizi: new THREE.MeshStandardMaterial({ color: 0xb8352c, roughness: 0.6, metalness: 0.1 }),
cam: new THREE.MeshStandardMaterial({ color: 0x7fb6d6, roughness: 0.12, metalness: 0.5, transparent: true, opacity: 0.42 }),
isik: new THREE.MeshStandardMaterial({ color: 0xffd9a0, emissive: 0xffb45c, emissiveIntensity: 1.6, roughness: 0.6 })
};
/* ---------------- Yardımcılar ---------------- */
const tiklanabilir = [];
// NUMEX-DÜZELTME (2026-10-22): Kök neden: tıklama işleyicisi userData.bilgi arıyordu, etiketle() ise veriyi userData'nın kendisine yazıyordu — alt alan hiç oluşmadığı için her tıklama sessizce düşüyordu. Veri artık bilgi altında tutuluyor.
function etiketle(nesne, ad, metin, yukseklik, agirlik){
nesne.userData = { bilgi: { ad, metin, yukseklik, agirlik } };
tiklanabilir.push(nesne);
return nesne;
}
function kutu(w, h, d, malzeme){
return new THREE.Mesh(new THREE.BoxGeometry(w, h, d), malzeme);
}
function silindir(ustR, altR, h, malzeme, dilim = 20){
return new THREE.Mesh(new THREE.CylinderGeometry(ustR, altR, h, dilim), malzeme);
}
/* =========================================================
ZEMİN · Ova
========================================================= */
// Zemin ufku aşacak kadar büyük olmalı; yoksa ufkun altında gökyüzü sızar
const ova = new THREE.Mesh(new THREE.PlaneGeometry(7000, 7000, 1, 1), MAL.toprak);
ova.rotation.x = -Math.PI / 2;
ova.receiveShadow = true;
sahne.add(ova);
/* --- Kıyı şeridi: uzakta deniz + kıyı çizgisi (istek 5) --- */
const denizMal = new THREE.MeshStandardMaterial({
color: 0x1b3a52, roughness: 0.24, metalness: 0.55
});
const deniz = new THREE.Mesh(new THREE.PlaneGeometry(7000, 2400), denizMal);
deniz.rotation.x = -Math.PI / 2;
deniz.position.set(0, -0.35, -3000);
deniz.receiveShadow = true;
sahne.add(deniz);
// Kıyı köpük şeridi (denizin iç kenarı)
const kopukMal = new THREE.MeshBasicMaterial({
color: 0x9fc4dd, transparent: true, opacity: 0.4,
blending: THREE.AdditiveBlending, depthWrite: false
});
const kopuk = new THREE.Mesh(new THREE.PlaneGeometry(7000, 42), kopukMal);
kopuk.rotation.x = -Math.PI / 2;
kopuk.position.set(0, 0.05, -1820);
sahne.add(kopuk);
// Kıyı çizgisi boyunca ince kum şeridi
const kumMal = new THREE.MeshStandardMaterial({ color: 0x8a7458, roughness: 1.0 });
const kum = new THREE.Mesh(new THREE.PlaneGeometry(7000, 90), kumMal);
kum.rotation.x = -Math.PI / 2;
kum.position.set(0, 0.02, -1740);
kum.receiveShadow = true;
sahne.add(kum);
// Uzak tepe silüetleri (tek halka, sabit tohum → her açılışta aynı manzara)
// NUMEX-DÜZELTME (2026-10-22): Dağ silüeti düz ve opak kalıyor, arkasındaki gökyüzüyle kaynaşmıyordu. Rengi sis tonuna yaklaştırıp hafif saydam yaptım — mesafe derinliği oluşsun.
const tepeMalzeme = new THREE.MeshStandardMaterial({
color: 0x6a5a4a, roughness: 1.0, transparent: true, opacity: 0.92
});
let tohum = 20261022;
const rastgele = () => {
tohum = (tohum * 1664525 + 1013904223) % 4294967296;
return tohum / 4294967296;
};
for (let i = 0; i < 64; i++){
const aci = (i / 64) * Math.PI * 2 + (rastgele() - 0.5) * 0.06;
// NUMEX-DÜZELTME (2026-10-22): Dağ halkası kameraya çok yakındı (900–1320 birim, kamera 210 birimde) — ekranın üstünü dev bir duvar gibi kaplıyordu. Referanstaki gibi ince, uzak bir silüet için halkayı ufka çektim ve yükseklikleri ölçekledim.
const uzaklik = 2600 + rastgele() * 1100;
const genislik = 420 + rastgele() * 620;
const yukseklik = 55 + rastgele() * 85;
const tepe = new THREE.Mesh(new THREE.ConeGeometry(genislik, yukseklik, 14), tepeMalzeme);
tepe.position.set(Math.cos(aci) * uzaklik, yukseklik / 2 - 10, Math.sin(aci) * uzaklik);
tepe.rotation.y = rastgele() * Math.PI;
tepe.scale.z = 0.6 + rastgele() * 0.5;
tepe.frustumCulled = false;
sahne.add(tepe);
}
/* =========================================================
FIRLATMA RAMPA KOMPLEKSİ
========================================================= */
// Gece yanan bütün lambalar burada toplanır (gündüz söner)
const lambaIzgarasi = [];
const kompleks = new THREE.Group();
sahne.add(kompleks);
/* --- Beton platform --- */
const platform = kutu(84, 3, 84, MAL.beton);
platform.position.y = 1.5;
platform.castShadow = true; platform.receiveShadow = true;
kompleks.add(etiketle(platform, 'Fırlatma Platformu', 'Ağır beton döşeme. Rampanın ve servis kulesinin oturduğu ana taşıyıcı yüzey; egzoz kanalı bu döşemenin ortasından geçer.', '84 × 84 m', '12.000 ton'));
// Platform kenar şeridi
const serit = kutu(86, 0.4, 86, MAL.betonKoyu);
serit.position.y = 3.05;
serit.receiveShadow = true;
kompleks.add(serit);
// Egzoz kanalı (rampanın altında, iki yana açılan)
const kanal = kutu(52, 1.6, 7, MAL.betonKoyu);
kanal.position.set(0, 2.4, 0);
kompleks.add(kanal);
const kanal2 = kanal.clone(); kanal2.rotation.y = Math.PI / 2;
kompleks.add(kanal2);
/* --- ALEV ÇUKURU: rampanın altındaki derin egzoz kuyusu (istek 5) --- */
// İç karartılmış çukur duvarı
const cukurMal = new THREE.MeshStandardMaterial({
color: 0x0a0a0b, roughness: 0.95, metalness: 0.2, side: THREE.DoubleSide
});
const cukurDerinlik = 9;
const cukurYari = 11;
const cukur = new THREE.Mesh(
new THREE.CylinderGeometry(cukurYari, cukurYari, cukurDerinlik, 40, 1, true),
cukurMal
);
cukur.position.set(0, -cukurDerinlik / 2 + 2.2, 0);
kompleks.add(cukur);
// Çukur tabanı — sıcak yanma izi
const tabanMal = new THREE.MeshStandardMaterial({
color: 0x1a0e07, roughness: 1.0, metalness: 0.1,
emissive: 0x140600, emissiveIntensity: 0.4
});
const cukurTaban = new THREE.Mesh(new THREE.CircleGeometry(cukurYari * 0.96, 40), tabanMal);
cukurTaban.rotation.x = -Math.PI / 2;
cukurTaban.position.y = -cukurDerinlik + 2.3;
kompleks.add(cukurTaban);
// Çevre beton bileziği
const cukurBilezik = new THREE.Mesh(
new THREE.TorusGeometry(cukurYari, 0.9, 10, 40), MAL.betonKoyu
);
cukurBilezik.rotation.x = Math.PI / 2;
cukurBilezik.position.y = 2.25;
cukurBilezik.castShadow = true;
kompleks.add(cukurBilezik);
/* --- RAMPA DOKUSU: beton platform üzerinde panel/tırtık izleri (istek 5) --- */
// Platform yüzeyine serilen doku plakası (canvas ile üretilir, harici dosya yok)
function betonDokusu(){
const cv = document.createElement('canvas');
cv.width = 1024; cv.height = 1024;
const ctx = cv.getContext('2d');
// Taban beton
ctx.fillStyle = '#7d7a73';
ctx.fillRect(0, 0, 1024, 1024);
// İnce gren
const img = ctx.getImageData(0, 0, 1024, 1024);
for (let i = 0; i < img.data.length; i += 4){
const n = (Math.random() - 0.5) * 26;
img.data[i] += n; img.data[i + 1] += n; img.data[i + 2] += n;
}
ctx.putImageData(img, 0, 0);
// Panel derzleri (8×8 beton plaka)
ctx.strokeStyle = 'rgba(45,44,42,.55)';
ctx.lineWidth = 5;
for (let i = 0; i <= 8; i++){
const p = (i / 8) * 1024;
ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, 1024); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(1024, p); ctx.stroke();
}
// Egzoz isi (ortada koyu radyal leke)
const g = ctx.createRadialGradient(512, 512, 40, 512, 512, 430);
g.addColorStop(0, 'rgba(18,14,10,.85)');
g.addColorStop(0.55, 'rgba(40,34,28,.4)');
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 1024, 1024);
// Çatlaklar
ctx.strokeStyle = 'rgba(35,33,30,.5)';
ctx.lineWidth = 2;
for (let i = 0; i < 26; i++){
ctx.beginPath();
let x = Math.random() * 1024, y = Math.random() * 1024;
ctx.moveTo(x, y);
for (let j = 0; j < 7; j++){
x += (Math.random() - 0.5) * 90;
y += (Math.random() - 0.5) * 90;
ctx.lineTo(x, y);
}
ctx.stroke();
}
// Sarı güvenlik şeritleri (rampa çevresi)
ctx.strokeStyle = 'rgba(214,178,72,.62)';
ctx.lineWidth = 12;
ctx.strokeRect(250, 250, 524, 524);
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 8;
return tex;
}
const platformDoku = betonDokusu();
const platformUst = new THREE.Mesh(
new THREE.PlaneGeometry(84, 84),
new THREE.MeshStandardMaterial({ map: platformDoku, roughness: 0.92, metalness: 0.04 })
);
platformUst.rotation.x = -Math.PI / 2;
platformUst.position.y = 3.06;
platformUst.receiveShadow = true;
kompleks.add(platformUst);
/* --- Rampa yükseltici (mobil fırlatma tablası) --- */
const tablaAlt = kutu(22, 5, 22, MAL.metalKoyu);
tablaAlt.position.set(0, 5.5, 0);
tablaAlt.castShadow = true; tablaAlt.receiveShadow = true;
kompleks.add(tablaAlt);
const tabla = kutu(19, 2.6, 19, MAL.beton);
tabla.position.set(0, 9.3, 0);
tabla.castShadow = true; tabla.receiveShadow = true;
kompleks.add(etiketle(tabla, 'Fırlatma Tablası', 'Roketi taşıyan hareketli çelik tabla. Kalkış anında kilitleri bırakır, roket serbest kalır ve tabla yerinde sabit kalır.', '19 × 19 m', '480 ton'));
// Tabla üzerindeki kızak rayları
for (const offset of [-6.4, 6.4]){
const ray = kutu(1.2, 0.7, 15, MAL.turuncu);
ray.position.set(offset, 10.9, 0);
ray.castShadow = true;
kompleks.add(ray);
}
/* --- Tutucu kollar (roketi tutan) --- */
const tutucular = new THREE.Group();
kompleks.add(tutucular);
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
const kol = kutu(2.4, 3.2, 1.4, MAL.metal);
kol.position.set(Math.cos(aci) * 6.6, 13.4, Math.sin(aci) * 6.6);
kol.rotation.y = -aci;
kol.castShadow = true;
tutucular.add(kol);
}
/* =========================================================
ROKET · Gökbey-9
========================================================= */
const roket = new THREE.Group();
roket.position.y = 11;
kompleks.add(roket);
etiketle(roket, 'Gökbey-9 Taşıyıcı Roketi', 'İki kademeli orta-sınıf taşıyıcı. Çekirdek kademe sıvı yakıtla çalışır, dört yan booster kalkış itkisini tamamlar. Burun konisinin içinde faydalı yük bölmesi bulunur.', '62 m', '410 ton');
const GOVDE_ALT = 0; // metre, roket grubuna göre
const GOVDE_UST = 40; // çekirdek gövde
const CAPRAZ = 1;
// Çekirdek gövde
const cekirdek = silindir(2.6, 2.9, GOVDE_UST, MAL.govde, 32);
cekirdek.position.y = GOVDE_ALT + GOVDE_UST / 2;
cekirdek.castShadow = true; cekirdek.receiveShadow = true;
roket.add(cekirdek);
// Gövde bantları (siyah detay)
for (const y of [8, 18, 30, 37]){
const bant = silindir(2.68, 2.9, 0.8, MAL.govdeKoyu, 32);
bant.position.y = y;
bant.castShadow = true;
roket.add(bant);
}
// Motor bölümü (alt)
const motorBolumu = silindir(2.9, 3.1, 5.4, MAL.govdeKoyu, 32);
motorBolumu.position.y = GOVDE_ALT - 2.4;
motorBolumu.castShadow = true;
roket.add(motorBolumu);
// Motor nozulları
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
const nozul = silindir(0.85, 1.35, 2.6, MAL.metalKoyu, 16);
nozul.position.set(Math.cos(aci) * 1.5, GOVDE_ALT - 6.1, Math.sin(aci) * 1.5);
nozul.castShadow = true;
roket.add(nozul);
}
// Burun konisi
const koni = new THREE.Mesh(new THREE.ConeGeometry(2.6, 9, 32), MAL.govde);
koni.position.y = GOVDE_UST + 4.5;
koni.castShadow = true;
roket.add(etiketle(koni, 'Burun Konisi · Faydalı Yük', 'Aerodinamik kap. İçindeki bölmede uydu ya da kargo taşınır; kalkıştan sonra ayrılarak yükü serbest bırakır.', '9 m', '1.8 ton'));
// Koni ucu (kırmızı)
const koniUcu = new THREE.Mesh(new THREE.ConeGeometry(0.65, 2.4, 20), MAL.kirmizi);
koniUcu.position.y = GOVDE_UST + 10.2;
koniUcu.castShadow = true;
roket.add(koniUcu);
// Faydalı yük bölmesi halkası
const payYukHalka = silindir(2.68, 2.72, 1.6, MAL.turuncu, 32);
payYukHalka.position.y = GOVDE_UST + 0.6;
roket.add(payYukHalka);
// Dört yan booster
const boosterlar = [];
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
const bx = Math.cos(aci) * 4.1;
const bz = Math.sin(aci) * 4.1;
const booster = silindir(1.35, 1.45, 21, MAL.govde, 24);
booster.position.set(bx, 14, bz);
booster.castShadow = true;
roket.add(booster);
boosterlar.push(booster);
// Booster burun konisi
const bKoni = new THREE.Mesh(new THREE.ConeGeometry(1.35, 4.2, 24), MAL.govde);
bKoni.position.set(bx, 26.6, bz);
bKoni.castShadow = true;
roket.add(bKoni);
// Booster alt halkası
const bHalka = silindir(1.46, 1.5, 1.2, MAL.govdeKoyu, 24);
bHalka.position.set(bx, 3.6, bz);
roket.add(bHalka);
// Booster nozulu
const bNozul = silindir(0.8, 1.1, 1.8, MAL.metalKoyu, 14);
bNozul.position.set(bx, 2.2, bz);
bNozul.castShadow = true;
roket.add(bNozul);
// Bağlantı ayağı
const bag = kutu(1.1, 0.7, 2.2, MAL.metal);
bag.position.set(bx * 0.72, 9, bz * 0.72);
bag.rotation.y = -aci;
roket.add(bag);
}
etiketle(boosterlar[0], 'Yan Booster', 'Kalkış itkisinin büyük kısmını sağlayan dört yardımcı motor. Yakıtı bitince ayrılır ve denize düşer.', '21 m', '4 × 42 ton');
/* =========================================================
SERVİS KULESİ
========================================================= */
const kule = new THREE.Group();
kule.position.set(-17, 3, -2);
kompleks.add(kule);
etiketle(kule, 'Servis Kulesi', 'Roketin yakıt dolumu, elektrik bağlantısı ve mürettebat erişimi bu kuleden yapılır. Üç ayrı erişim kolu roketin farklı yüksekliklerine uzanır.', '96 m', '3.400 ton');
// NUMEX-DÜZELTME (2026-10-22): Kule gövdesi 8×90×8 dolu bir blok olarak modellenmişti; referansta kafes/çelik strüktür var ve blok roketten kalın görünüyordu. Dolu hacmi yarı saydam ince panelle değiştirip dört köşeye taşıyıcı kolon koydum.
// Gövde: dolu blok yerine yarı saydam ince panel (arkadaki roket görünsün)
const kulePanelMal = new THREE.MeshStandardMaterial({
color: 0x7c8896, roughness: 0.85, metalness: 0.25,
transparent: true, opacity: 0.28, side: THREE.DoubleSide, depthWrite: false
});
const kuleGovde = kutu(7.4, 90, 7.4, kulePanelMal);
kuleGovde.position.y = 45;
kule.add(kuleGovde);
// Dört köşe taşıyıcı kolon
const kolonMal = new THREE.MeshStandardMaterial({ color: 0x9aa4ae, roughness: 0.6, metalness: 0.7 });
for (const [kx, kz] of [[-3.9,-3.9],[3.9,-3.9],[-3.9,3.9],[3.9,3.9]]){
const kolon = kutu(1.1, 90, 1.1, kolonMal);
kolon.position.set(kx, 45, kz);
kolon.castShadow = true;
kule.add(kolon);
}
// Yatay kata bantları (kule ritmi)
for (let y = 8; y < 90; y += 11){
const bant = kutu(8.2, 0.8, 8.2, kolonMal);
bant.position.y = y;
kule.add(bant);
}
// Kule kafes çaprazları (görsel derinlik)
const caprazMal = new THREE.LineBasicMaterial({ color: 0x6d7883, transparent: true, opacity: 0.55 });
const caprazNoktalari = [];
for (let y = 0; y < 90; y += 6){
const d = 4.02;
caprazNoktalari.push(-d, y, d, d, y + 6, d);
caprazNoktalari.push(d, y, d, d, y + 6, -d);
caprazNoktalari.push(-d, y, -d, -d, y + 6, d);
}
const caprazGeo = new THREE.BufferGeometry();
caprazGeo.setAttribute('position', new THREE.Float32BufferAttribute(caprazNoktalari, 3));
kule.add(new THREE.LineSegments(caprazGeo, caprazMal));
// Kule tepesi
const kuleTepe = kutu(9.5, 4, 9.5, MAL.metal);
kuleTepe.position.y = 92;
kuleTepe.castShadow = true;
kule.add(kuleTepe);
// Kule kırmızı uyarı ışığı
const uyari = new THREE.Mesh(new THREE.SphereGeometry(0.6, 12, 10), MAL.kirmizi.clone());
uyari.material.emissive = new THREE.Color(0xff3322);
uyari.material.emissiveIntensity = 2.2;
uyari.position.y = 95;
kule.add(uyari);
// Erişim kolları (roketin yanına uzanan)
const kollar = [14, 30, 44];
kollar.forEach((y, i) => {
const kol = kutu(11, 1.1, 2.6, MAL.metal);
kol.position.set(9.5, y, 0);
kol.castShadow = true;
kule.add(kol);
// Kol ucu kabin
const kabin = kutu(3.2, 3, 3.2, MAL.govdeKoyu);
kabin.position.set(15.2, y + 1.6, 0);
kabin.castShadow = true;
kule.add(kabin);
// Kabin pencereleri
const pencere = kutu(0.15, 1.2, 2.2, MAL.cam);
pencere.position.set(16.85, y + 1.8, 0);
kule.add(pencere);
});
// Kule asansör rayı
const asansor = kutu(1.6, 88, 1.6, MAL.turuncu);
asansor.position.set(-4.3, 45, 0);
kule.add(asansor);
/* =========================================================
YAKIT KÜRELERİ
========================================================= */
const kureGrubu = new THREE.Group();
kompleks.add(kureGrubu);
function yakitKuresi(x, z, r, renk){
const g = new THREE.Group();
g.position.set(x, 0, z);
kureGrubu.add(g);
const kure = new THREE.Mesh(new THREE.SphereGeometry(r, 28, 20), new THREE.MeshStandardMaterial({
color: renk, roughness: 0.5, metalness: 0.35
}));
kure.position.y = r + 5;
kure.castShadow = true; kure.receiveShadow = true;
g.add(kure);
// Destek ayakları
for (let i = 0; i < 6; i++){
const aci = (i / 6) * Math.PI * 2;
const ayak = silindir(0.28, 0.28, r * 0.9, MAL.metalKoyu, 8);
ayak.position.set(Math.cos(aci) * r * 0.72, (r + 5) / 2 + 1, Math.sin(aci) * r * 0.72);
ayak.rotation.z = Math.cos(aci) * 0.22;
ayak.rotation.x = -Math.sin(aci) * 0.22;
g.add(ayak);
}
return g;
}
const kure1 = yakitKuresi(-30, 26, 8, 0xd8dde2);
etiketle(kure1, 'Sıvı Oksijen Tankı', 'Kriyojenik oksijen soğuk hâlde tutulur. Boru hatlarıyla kuleye, oradan rokete aktarılır; dolum kalkıştan saatler önce biter.', 'Ø 16 m', '900 m³');
yakitKuresi(-14, 32, 6, 0xc2c8ce);
const kure3 = yakitKuresi(30, 28, 7.5, 0xd0d5da);
etiketle(kure3, 'Sıvı Yakıt Tankı', 'Yakıt bileşeni basınç altında saklanır. Tanklar arasındaki boru köprüsü karma kompleksin damar sistemidir.', 'Ø 15 m', '780 m³');
// Boru hatları (kürelerden kuleye)
const boruMal = new THREE.MeshStandardMaterial({ color: 0x8d959d, roughness: 0.55, metalness: 0.7 });
function boru(x1, z1, x2, z2, y){
const uzunluk = Math.hypot(x2 - x1, z2 - z1);
const b = silindir(0.42, 0.42, uzunluk, boruMal, 10);
b.position.set((x1 + x2) / 2, y, (z1 + z2) / 2);
b.rotation.z = Math.PI / 2;
b.rotation.y = -Math.atan2(z2 - z1, x2 - x1);
kompleks.add(b);
}
boru(-30, 24, -17, 4, 4.5);
boru(-14, 30, -17, 6, 3.6);
boru(30, 26, -8, 4, 4.2);
/* =========================================================
PARATONER DİREKLERİ
========================================================= */
const paratonerGrubu = new THREE.Group();
kompleks.add(paratonerGrubu);
const catenary = [];
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
// NUMEX-DÜZELTME (2026-10-22): Paratoner direkleri 40 birim yarıçapta duruyordu; kamera 84 birimdeyken ekranı dikey çizgilerle bölüp roketi kesiyorlardı. Yarıçapı 62'ye çıkardım — direkler artık kadrajın dışına/kenarına düşer, roketin önünden geçmez.
const px = Math.cos(aci) * 62;
const pz = Math.sin(aci) * 62;
const direk = silindir(0.5, 0.85, 62, MAL.metal, 12);
direk.position.set(px, 34, pz);
direk.castShadow = true;
paratonerGrubu.add(direk);
// Tepe ucu
const ucu = new THREE.Mesh(new THREE.ConeGeometry(0.5, 3, 10), MAL.metal);
ucu.position.set(px, 66.5, pz);
paratonerGrubu.add(ucu);
// Tepe lambası (gece yanar)
const lamba = new THREE.Mesh(new THREE.SphereGeometry(0.4, 10, 8), MAL.isik.clone());
lamba.position.set(px, 68, pz);
lambaIzgarasi.push(lamba);
paratonerGrubu.add(lamba);
// Taban bloğu
const taban = kutu(3, 2.6, 3, MAL.betonKoyu);
taban.position.set(px, 4, pz);
paratonerGrubu.add(taban);
catenary.push(new THREE.Vector3(px, 62, pz));
}
etiketle(paratonerGrubu, 'Paratoner Direkleri', 'Dört direk kompleksi yıldırım darbelerine karşı korur; aralarındaki iletken tel örgü elektrik yükünü toprağa taşır.', '68 m', '4 direk');
/* =========================================================
ÇEVRE BİNALARI · SU KULESİ · YOL
========================================================= */
// Yatay yakıt servis binası
const servisBina = kutu(16, 7, 10, MAL.beton);
servisBina.position.set(-38, 3.5 + 3, 8);
servisBina.castShadow = true; servisBina.receiveShadow = true;
kompleks.add(etiketle(servisBina, 'Servis Binası', 'Yakıt pompaları, vana odaları ve kontrol ekipmanı bu binada. Yer altı tünelleriyle rampaya bağlanır.', '16 × 10 m', '—'));
// Yatay bina (kontrol merkezi)
const kontrol = kutu(26, 6, 12, MAL.betonKoyu);
kontrol.position.set(34, 6, -26);
kontrol.castShadow = true; kontrol.receiveShadow = true;
kompleks.add(kontrol);
// Kontrol merkezi pencereleri
for (let i = 0; i < 7; i++){
const p = kutu(2.2, 1.6, 0.2, MAL.isik.clone());
p.position.set(24 + i * 3.1, 7.2, -20.1);
lambaIzgarasi.push(p);
kompleks.add(p);
}
// Haberleşme çanakları
for (const [cx, cz, r] of [[42, -34, 4.5], [46, -22, 3.2]]){
const canak = new THREE.Mesh(
new THREE.SphereGeometry(r, 20, 12, 0, Math.PI * 2, 0, Math.PI / 2.6),
new THREE.MeshStandardMaterial({ color: 0xd6dade, roughness: 0.45, metalness: 0.4, side: THREE.DoubleSide })
);
canak.position.set(cx, 12, cz);
canak.rotation.set(Math.PI * 0.72, 0.6, 0.3);
canak.castShadow = true;
kompleks.add(canak);
const bacak = silindir(0.4, 0.6, 10, MAL.metalKoyu, 8);
bacak.position.set(cx, 8, cz);
kompleks.add(bacak);
}
// Su kulesi
const suKule = new THREE.Group();
suKule.position.set(-46, 3, -30);
kompleks.add(suKule);
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
const bacak = silindir(0.45, 0.6, 22, MAL.metal, 8);
bacak.position.set(Math.cos(aci) * 3.4, 11, Math.sin(aci) * 3.4);
bacak.rotation.z = Math.cos(aci) * 0.1;
suKule.add(bacak);
}
const tank = silindir(5.2, 5.2, 8, MAL.govde, 24);
tank.position.y = 26;
tank.castShadow = true;
suKule.add(etiketle(tank, 'Su Kulesi', 'Kalkış anında ses dalgasını bastırmak için rampa üzerine su püskürtülür; bu kule o suyu depolar.', '8 m yüksek', '1.100 m³'));
// Yol (platformdan dışa)
const yol = new THREE.Mesh(new THREE.PlaneGeometry(14, 200), new THREE.MeshStandardMaterial({ color: 0x3d3d3a, roughness: 1 }));
yol.rotation.x = -Math.PI / 2;
yol.position.set(0, 3.05, 130);
yol.receiveShadow = true;
kompleks.add(yol);
// Yol kenarı direkleri
for (let i = 0; i < 12; i++){
for (const sx of [-8.5, 8.5]){
const d = silindir(0.16, 0.2, 7, MAL.metalKoyu, 6);
d.position.set(sx, 6.5, 60 + i * 16);
kompleks.add(d);
const l = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 6), MAL.isik.clone());
l.position.set(sx, 10, 60 + i * 16);
lambaIzgarasi.push(l); // gündüz söner, gece yanar
kompleks.add(l);
}
}
// Güvenlik çiti (platform çevresi)
const citMal = new THREE.LineBasicMaterial({ color: 0x7e8894, transparent: true, opacity: 0.4 });
const citNokta = [];
const citYari = 46;
for (let i = 0; i <= 40; i++){
const t = i / 40;
const x = -citYari + t * citYari * 2;
citNokta.push(x, 3.2, -citYari, x, 6.4, -citYari);
citNokta.push(x, 3.2, citYari, x, 6.4, citYari);
}
const citGeo = new THREE.BufferGeometry();
citGeo.setAttribute('position', new THREE.Float32BufferAttribute(citNokta, 3));
kompleks.add(new THREE.LineSegments(citGeo, citMal));
/* =========================================================
ZAMAN DİLİMİ SİSTEMİ
========================================================= */
// Bulut katmanı (yavaşça döner — havada hareket hissi versin diye)
let bulutlar = null;
{
const bulutMal = new THREE.MeshBasicMaterial({
color: 0xffffff, transparent: true, opacity: 0.07, depthWrite: false, side: THREE.DoubleSide
});
bulutlar = new THREE.Group();
for (let i = 0; i < 5; i++){
// NUMEX-DÜZELTME (2026-10-22): Bulut katmanları kameraya çok yakın ve çok büyüktü (340 birim, y=150-238) — gökyüzünde dev lacivert üçgenler olarak görünüyordu. Katmanları ufka ve yukarı taşıyıp boyutlarını küçülttüm, saydamlığı düşürdüm.
const d = new THREE.Mesh(new THREE.PlaneGeometry(1600, 1600), bulutMal.clone());
d.rotation.x = -Math.PI / 2;
d.position.set((Math.random() - 0.5) * 900, 620 + i * 90, (Math.random() - 0.5) * 900);
d.material.opacity = 0.018 + Math.random() * 0.018;
bulutlar.add(d);
}
sahne.add(bulutlar);
}
// Atmosferik degaj (yer sıcaklığı bulanıklığı) — tek yerden yönetilen parlaklık
const havaUniform = { parlaklik: { value: 1 } };
const ZAMANLAR = [
// alt = ufuk çizgisi rengi (sıcak/parlak) · orta = geçiş bandı · ust = gökyüzü tepesi (koyu)
// NUMEX-DÜZELTME (2026-10-22): Şafak paletini fazla ısıtmıştım; gökyüzü ateş kırmızısına dönüp referanstaki lacivert-mor geçiş kaybolmuştu. Ufku sıcak, tepesi soğuk lacivert olan dengeye çektim.
{ ad: 'Şafak', alt: 0xf3b184, orta: 0x9b7fa8, ust: 0x182c4e, sis: 0xb8a094, sisYog: 0.0028,
gunesRenk: 0xffc48a, gunesGuc: 1.9, gokyuzuGuc: 0.85, gokyuzuInt: 0.7, pozlama: 1.02,
gunesPoz: [70, 30, -95], gece: 0 },
{ ad: 'Öğle', alt: 0xcfe2ee, orta: 0x8fbde2, ust: 0x2f6fae, sis: 0xc3d3dd, sisYog: 0.0017,
gunesRenk: 0xffffff, gunesGuc: 3.2, gokyuzuGuc: 0.15, gokyuzuInt: 1.2, pozlama: 1.0,
gunesPoz: [40, 160, -60], gece: 0 },
{ ad: 'Alacakaranlık', alt: 0xf0954a, orta: 0x6a4a66, ust: 0x18294a, sis: 0x8c6a58, sisYog: 0.0030,
gunesRenk: 0xff9b52, gunesGuc: 1.4, gokyuzuGuc: 1.3, gokyuzuInt: 0.5, pozlama: 1.06,
gunesPoz: [-90, 14, -70], gece: 0.35 },
// NUMEX-DÜZELTME (2026-10-22): Gece modu fazla siyahtı: dağ silüetleri kayboluyor, ipucu/panel metinleri okunmuyordu. Ufuk ve gökyüzü tonlarını bir kademe açtım, ay ışığını (directional) ve ortam ışığını yükselttim ki silüetler seçilebilsin.
{ ad: 'Gece', alt: 0x2a3f60, orta: 0x13233c, ust: 0x060c18, sis: 0x141f33, sisYog: 0.0026,
gunesRenk: 0x9db6e8, gunesGuc: 0.7, gokyuzuGuc: 0.10, gokyuzuInt: 0.55, pozlama: 1.35,
gunesPoz: [-50, 70, 60], gece: 1 }
];
const kaydirici = document.getElementById('kaydirici');
const zamanBasliklar = [...document.querySelectorAll('#zamanBasliklar span')];
let aktifZaman = 0;
function zamanUygula(t){ // t: 0..3 arası ondalık
const i0 = Math.floor(t) % 4;
const i1 = (i0 + 1) % 4;
const f = t - Math.floor(t);
const A = ZAMANLAR[i0], B = ZAMANLAR[i1];
const karis = (a, b) => new THREE.Color(a).lerp(new THREE.Color(b), f);
const say = (a, b) => a + (b - a) * f;
gokyuzuUniform.ustRenk.value.copy(karis(A.ust, B.ust));
gokyuzuUniform.ortaRenk.value.copy(karis(A.orta, B.orta));
gokyuzuUniform.altRenk.value.copy(karis(A.alt, B.alt));
gokyuzuUniform.gunesGuc.value = say(A.gokyuzuGuc, B.gokyuzuGuc);
gokyuzuUniform.geceFaktoru.value = say(A.gece, B.gece);
// Sahne lambaları: gece parlak, gündüz sönük
const geceF = say(A.gece, B.gece);
for (const lamba of lambaIzgarasi){
lamba.material.color.setRGB(
0.35 + geceF * 0.65,
0.32 + geceF * 0.55,
0.20 + geceF * 0.25
);
}
sahne.fog.color.copy(karis(A.sis, B.sis));
sahne.fog.density = say(A.sisYog, B.sisYog);
gunes.color.copy(karis(A.gunesRenk, B.gunesRenk));
gunes.intensity = say(A.gunesGuc, B.gunesGuc);
gunes.position.set(
say(A.gunesPoz[0], B.gunesPoz[0]),
say(A.gunesPoz[1], B.gunesPoz[1]),
say(A.gunesPoz[2], B.gunesPoz[2])
);
gokyuzuIsik.intensity = say(A.gokyuzuInt, B.gokyuzuInt);
dolguIsik.intensity = 0.35 + (1 - say(A.gece, B.gece)) * 0.35;
cizici.toneMappingExposure = say(A.pozlama, B.pozlama);
havaUniform.parlaklik.value = 1 - say(A.gece, B.gece) * 0.75;
}
// NUMEX-DÜZELTME (2026-10-22): Kök neden: etiket tıklaması 'input' olayı gönderiyordu ama dinleyici kaydırıcıda HENÜZ bağlanmamıştı — script sırası yüzünden tıklama hiçbir şey yapmıyordu. Ayrıca .aktif sınıfı CSS'te tanımlı değildi (.secili tanımlı). Zaman dilimini tek bir fonksiyona topladım; hem etiket hem kaydırıcı aynı fonksiyonu çağırıyor, doğru sınıf adı kullanılıyor.
// Zaman dilimini uygulayan TEK giriş noktası (etiket + kaydırıcı buradan geçer)
function zamaniAyarla(t){
const v = Math.max(0, Math.min(3.999, t));
aktifZaman = v;
zamanUygula(v);
const yakin = Math.round(v) % 4;
zamanBasliklar.forEach((s, i) => s.classList.toggle('secili', i === yakin));
kaydirici.value = String(Math.round(v / 3.999 * 1000));
}
zamanBasliklar.forEach((s, i) => {
s.addEventListener('click', () => zamaniAyarla(i));
});
kaydirici.addEventListener('input', () => {
zamaniAyarla(parseFloat(kaydirici.value) / 1000 * 3.999);
});
zamanUygula(0);
/* =========================================================
PARÇAYA TIKLAMA · BİLGİ KARTI
========================================================= */
const isinlayici = new THREE.Raycaster();
const fare = new THREE.Vector2();
let baslangicNoktasi = null;
let seciliParca = null;
// NUMEX-DÜZELTME (2026-10-22): Kök neden: kart, kartBaslik, kartMetin, kartYukseklik, kartAgirlik, kartKapat değişkenleri hiç tanımlanmamıştı; kod bunları global id'lerden otomatik alıyordu ve kartiGoster çağrısı sessizce patlayıp kart hiç açılmıyordu. Hepsi açıkça sorgulanıyor.
// Kart DOM referansları (açıkça tanımlı — global id tahminine bırakma)
const kart = document.getElementById('kart');
const kartBaslik = document.getElementById('kartBaslik');
const kartMetin = document.getElementById('kartMetin');
const kartYukseklik = document.getElementById('kartYukseklik');
const kartAgirlik = document.getElementById('kartAgirlik');
const kartKapat = document.getElementById('kartKapat');
function kartiGoster(veri){
kartBaslik.textContent = veri.ad;
kartMetin.textContent = veri.metin;
kartYukseklik.textContent = veri.yukseklik || '—';
kartAgirlik.textContent = veri.agirlik || '—';
kart.classList.add('acik');
}
function kartiKapat(){
kart.classList.remove('acik');
if (seciliParca){
seciliParca.traverse(o => {
if (o.isMesh && o.userData.ozelMalzeme && o.material.emissive){
o.material.emissiveIntensity = o.userData.ozelMalzeme.emissiveIntensity;
}
});
seciliParca = null;
}
}
kartKapat.addEventListener('click', kartiKapat);
// İmleç değişimi (hover)
let hoverZaman = 0;
tuval.addEventListener('pointermove', (e) => {
const simdi = performance.now();
if (simdi - hoverZaman < 70) return;
hoverZaman = simdi;
fare.x = (e.clientX / window.innerWidth) * 2 - 1;
fare.y = -(e.clientY / window.innerHeight) * 2 + 1;
isinlayici.setFromCamera(fare, aktifKamera);
const bulunan = isinlayici.intersectObjects(tiklanabilir, false);
tuval.style.cursor = bulunan.length ? 'pointer' : 'grab';
});
tuval.addEventListener('pointerdown', (e) => {
baslangicNoktasi = { x: e.clientX, y: e.clientY };
});
tuval.addEventListener('pointerup', (e) => {
if (!baslangicNoktasi) return;
const gecti = Math.hypot(e.clientX - baslangicNoktasi.x, e.clientY - baslangicNoktasi.y);
baslangicNoktasi = null;
if (gecti > 6) return; // sürükleme sayılır, tıklama değil
fare.x = (e.clientX / window.innerWidth) * 2 - 1;
fare.y = -(e.clientY / window.innerHeight) * 2 + 1;
isinlayici.setFromCamera(fare, aktifKamera);
const bulunan = isinlayici.intersectObjects(tiklanabilir, false);
if (!bulunan.length){ kartiKapat(); return; }
let hedef = bulunan[0].object;
while (hedef && !hedef.userData.bilgi) hedef = hedef.parent;
if (!hedef) return;
if (seciliParca === hedef){ kartiKapat(); return; }
if (seciliParca) kartiKapat();
seciliParca = hedef;
// Seçili parçayı hafifçe aydınlat
hedef.traverse(o => {
if (o.isMesh && o.material && o.material.emissive){
o.userData.ozelMalzeme = { emissiveIntensity: o.material.emissiveIntensity };
o.material.emissiveIntensity = o.userData.ozelMalzeme.emissiveIntensity + 0.35;
}
});
kartiGoster(hedef.userData.bilgi);
});
/* =========================================================
KAMERA SIFIRLA
========================================================= */
btnSifirla.addEventListener('click', () => {
kontroller.aciX = KAMERA_SIFIR.aciX;
kontroller.aciY = KAMERA_SIFIR.aciY;
kontroller.hedefMesafe = KAMERA_SIFIR.mesafe;
kontroller.hedef.copy(KAMERA_SIFIR.hedef);
});
/* Perspektif ↔ paralel (izometrik) kamera */
let paralelMi = false;
const persKamera = kamera;
const ortKamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.5, 6000);
ortKamera.position.copy(kamera.position);
ortKamera.lookAt(kontroller.hedef);
function kameraBoyutla(){
const oran = innerWidth / innerHeight;
const boy = 190;
ortKamera.left = -boy * oran;
ortKamera.right = boy * oran;
ortKamera.top = boy;
ortKamera.bottom = -boy;
ortKamera.updateProjectionMatrix();
}
kameraBoyutla();
btnParalel.addEventListener('click', () => {
paralelMi = !paralelMi;
// NUMEX-DÜZELTME (2026-10-22): Kök neden: geçiş sırasında k.position.copy(kamera.position) yazıyordu; perspektife dönerken kamera === persKamera olduğu için konum kendinden kopyalanıp bozulmuş orto kamera pozisyonuyla eziliyordu (kamera yerin altına düşüyordu). Konumu yörünge kontrolünden yeniden hesaplatıyorum.
aktifKamera = paralelMi ? ortKamera : persKamera;
kamerayiKonumla(); // konumu her zaman yörünge kontrolünden yeniden hesapla
aktifKamera.updateMatrixWorld();
btnParalel.textContent = paralelMi ? 'Kamera: Perspektif' : 'Kamera: Paralel';
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') kartiKapat();
if (e.key === 'r' || e.key === 'R') btnSifirla.click();
});
/* =========================================================
KAMERA SIFIRLAMA ANİMASYONU İÇİN YUMUŞAK GEÇİŞ
========================================================= */
// NUMEX-DÜZELTME (2026-10-22): Kök neden: yumuşak kamera geçişi (hedefKamera) kamera.position'a lerp yapıyordu; bu, yörünge kontrolünün her karede yazdığı konumla çakışıp kamerayı sürekli yarışa sokuyordu. Kontrol artık tek sahipli; kullanılmayan animasyon bloğu kaldırıldı.
/* Kamera tek sahipli: yörünge kontrolü her karede konumluyor
(yukarıdaki btnSifirla dinleyicisi konumu sıfırlıyor). */
/* =========================================================
PERFORMANS SAYAÇLARI
========================================================= */
let kareSayaci = 0;
let fpsZaman = performance.now();
let fps = 0;
function sayaciGuncelle(){
kareSayaci++;
const simdi = performance.now();
const gecen = simdi - fpsZaman;
if (gecen >= 500){
fps = Math.round((kareSayaci * 1000) / gecen);
kareSayaci = 0;
fpsZaman = simdi;
pFps.textContent = fps;
pUcgen.textContent = cizici.info.render.triangles.toLocaleString('tr-TR');
pCizim.textContent = cizici.info.render.calls;
}
}
/* =========================================================
BÖLÜMLER · Genel Bakış / Yapı / Fırlatma
========================================================= */
const SEKMELER = Array.from(document.querySelectorAll('.sekme'));
const olcuKatman = document.getElementById('olcuKatman');
const firlatmaPanel = document.getElementById('firlatmaPanel');
const geriSayimEl = document.getElementById('geriSayim');
const asamaAdEl = document.getElementById('asamaAd');
const perfPanel = document.getElementById('perf');
let bolum = 'genel';
/* ---- Ölçü çizgileri (Yapı bölümü) ---- */
const olcuNesneleri = [];
function olcuCizgisiOlustur(cls){
const el = document.createElement('div');
el.className = 'olcuCizgi ' + cls;
olcuKatman.appendChild(el);
return el;
}
function olcuUcuOlustur(){
const el = document.createElement('div');
el.className = 'olcuUc';
olcuKatman.appendChild(el);
return el;
}
function olcuEtiketOlustur(metin){
const el = document.createElement('div');
el.className = 'olcuEtiket';
el.textContent = metin;
olcuKatman.appendChild(el);
return el;
}
// Roketin gerçek sınırları (roket grubu y=11'de, gövde 0..40, koni 40..49, motor -6..0)
const ROKET_ALT = 11 - 7.4; // nozul ucu
const ROKET_UST = 11 + 49.4; // koni ucu
const ROKET_YARICAP = 3.1; // motor bölümü yarıçapı (en geniş)
const olcuBoy = olcuCizgisiOlustur('dikey');
const olcuBoyUst = olcuUcuOlustur();
const olcuBoyAlt = olcuUcuOlustur();
const olcuBoyEtiket = olcuEtiketOlustur('≈ 62 m · TEMSİLİ');
const olcuCap = olcuCizgisiOlustur('yatay');
const olcuCapSol = olcuUcuOlustur();
const olcuCapSag = olcuUcuOlustur();
const olcuCapEtiket = olcuEtiketOlustur('Ø 6,2 m · TEMSİLİ');
const olcuNot = document.createElement('div');
olcuNot.className = 'olcuNot';
olcuNot.textContent = 'Ölçüler temsilîdir · gerçek bir araç değildir';
olcuKatman.appendChild(olcuNot);
const v3 = new THREE.Vector3();
function dunyadanEkrana(x, y, z){
v3.set(x, y, z).project(aktifKamera);
return {
x: (v3.x * 0.5 + 0.5) * innerWidth,
y: (-v3.y * 0.5 + 0.5) * innerHeight,
gorunur: v3.z < 1
};
}
function olculeriGuncelle(goster){
const gorunur = goster ? 1 : 0;
const opaklik = String(gorunur);
olcuKatman.style.opacity = opaklik;
olcuKatman.style.transition = 'opacity .4s';
if (!goster) return;
// --- Boy ölçüsü: roketin sağında dikey çizgi ---
const ust = dunyadanEkrana(ROKET_YARICAP + 4, ROKET_UST, 0);
const alt = dunyadanEkrana(ROKET_YARICAP + 4, ROKET_ALT, 0);
if (ust.gorunur && alt.gorunur){
const x = ust.x;
const y1 = Math.min(ust.y, alt.y);
const y2 = Math.max(ust.y, alt.y);
olcuBoy.style.left = x + 'px';
olcuBoy.style.top = y1 + 'px';
olcuBoy.style.height = (y2 - y1) + 'px';
olcuBoyUst.style.left = x + 'px';
olcuBoyUst.style.top = y1 + 'px';
olcuBoyAlt.style.left = x + 'px';
olcuBoyAlt.style.top = y2 + 'px';
// Yatay kılavuz çizgiler
olcuBoyEtiket.style.left = (x + 58) + 'px';
olcuBoyEtiket.style.top = ((y1 + y2) / 2) + 'px';
}
// --- Çap ölçüsü: motor bölümü genişliği (yatay) ---
const sol = dunyadanEkrana(-ROKET_YARICAP, ROKET_ALT + 2.2, 0);
const sag = dunyadanEkrana(ROKET_YARICAP, ROKET_ALT + 2.2, 0);
if (sol.gorunur && sag.gorunur){
const y = sol.y;
olcuCap.style.left = Math.min(sol.x, sag.x) + 'px';
olcuCap.style.top = y + 'px';
olcuCap.style.width = Math.abs(sag.x - sol.x) + 'px';
olcuCapSol.style.left = sol.x + 'px';
olcuCapSol.style.top = y + 'px';
olcuCapSag.style.left = sag.x + 'px';
olcuCapSag.style.top = y + 'px';
olcuCapEtiket.style.left = ((sol.x + sag.x) / 2) + 'px';
olcuCapEtiket.style.top = (y - 26) + 'px';
}
}
/* ---- Yapı bölümü: parçalara ayrılma ----
Roketin DOĞRUDAN çocuklarını (düz seviye) alır; her biri kendi
yüksekliğine göre yukarı/aşağı kayar. Alt gruplara dokunmayız —
traverse yerel konumlar karıştırıyordu. */
const YAPI_PARCA = [];
roket.children.forEach(o => {
if (o.isMesh || o.isGroup){
YAPI_PARCA.push({
nesne: o,
basY: o.position.y,
basX: o.position.x,
basZ: o.position.z,
ad: (o.userData && o.userData.bilgi && o.userData.bilgi.ad) || o.type
});
}
});
// Gövde ana silindiri (cekirdek) tek parça ve en büyük; onu da listeye kat
if (!YAPI_PARCA.some(p => p.nesne === cekirdek)){
YAPI_PARCA.push({ nesne: cekirdek, basY: cekirdek.position.y, basX: cekirdek.position.x, basZ: cekirdek.position.z, ad: 'Çekirdek gövde' });
}
// Yüksekliğe göre sırala: en üstteki en çok yükselsin
YAPI_PARCA.sort((a, b) => b.basY - a.basY);
console.info('[YAPI] parça sayısı:', YAPI_PARCA.length,
YAPI_PARCA.slice(0, 8).map(p => p.basY.toFixed(1)));
let yapiOran = 0; // 0..1 (parçalara ayrılma)
let yapiHedef = 0;
function yapiUygula(t){
const n = Math.max(1, YAPI_PARCA.length);
YAPI_PARCA.forEach((p, i) => {
// Sırayla açılma (aşağıdan yukarı dalga)
const gecikme = (i / n) * 0.45;
const yerel = Math.min(1, Math.max(0, (t - gecikme) / (1 - gecikme || 1)));
const yumusak = yerel * yerel * (3 - 2 * yerel);
// Dikey açılma: her parça kendi yüksekliğinden uzağa, büyük parça çok açılır
const merkezY = 22;
const yon = p.basY >= merkezY ? 1 : -1;
const mesafe = Math.abs(p.basY - merkezY) / merkezY;
const miktar = 6 + mesafe * 16 + (p.nesne === cekirdek ? 4 : 0);
p.nesne.position.y = p.basY + yon * yumusak * miktar;
// Radyal açılma: parçalar gövdeden yanlara dağılır
const aci = (i / n) * Math.PI * 2 + 0.6;
const radyal = yumusak * (3.5 + (i % 5) * 1.8);
p.nesne.position.x = p.basX + Math.cos(aci) * radyal;
p.nesne.position.z = p.basZ + Math.sin(aci) * radyal;
// Gövde dilimleri hafif yatsın (yapı sökülüyormuş hissi)
if (p.nesne !== cekirdek){
p.nesne.rotation.z = Math.sin(i) * yumusak * 0.12;
}
});
}
/* ---- Fırlatma bölümü: geri sayım → ateşleme → kalkış → ayrılma ---- */
let firlatmaAktif = false;
let firlatmaZaman = 0;
let kalkanRoketY = 11;
const ASAMALAR = [
{ t: 0.0, ad: 'Hazırlık', sayim: 'T-10' },
{ t: 3.0, ad: 'Geri sayım', sayim: 'T-7' },
{ t: 6.0, ad: 'Servis kolları çekiliyor', sayim: 'T-4' },
{ t: 9.0, ad: 'Ateşleme', sayim: 'T-0' },
{ t: 11.0, ad: 'Kalkış', sayim: 'T+2' },
{ t: 15.0, ad: 'Kademe ayrılması', sayim: 'T+6' },
{ t: 19.0, ad: 'Yörüngeye oturma', sayim: 'T+10' }
];
function firlatmaSifirla(){
firlatmaAktif = false;
firlatmaZaman = 0;
kalkanRoketY = 11;
roket.position.y = 11;
roket.rotation.z = 0;
geriSayimEl.textContent = 'T-10';
asamaAdEl.textContent = 'Hazırlık';
alevler.forEach(a => { a.visible = false; a.scale.setScalar(1); });
servisKollari.forEach(k => { k.rotation.z = k.userData.kapali; });
kademeAyrik.position.y = 0;
yapiHedef = 0;
}
function firlatmaBaslat(){
firlatmaSifirla();
firlatmaAktif = true;
firlatmaZaman = 0;
}
/* ---- ALEVLER: çekirdek + 4 booster nozulunun altında ---- */
const alevMal = new THREE.MeshBasicMaterial({
color: 0xffb45a, transparent: true, opacity: 0.92,
blending: THREE.AdditiveBlending, depthWrite: false
});
const alevIcMal = new THREE.MeshBasicMaterial({
color: 0xfff2d0, transparent: true, opacity: 0.95,
blending: THREE.AdditiveBlending, depthWrite: false
});
const alevler = [];
function alevEkle(x, y, z, r){
// Dış koni (turuncu)
const dis = new THREE.Mesh(new THREE.ConeGeometry(r, 7, 16, 1, true), alevMal);
dis.rotation.x = Math.PI; // aşağı baksın
dis.position.set(x, y - 3.5, z);
dis.visible = false;
roket.add(dis);
alevler.push(dis);
// İç koni (beyaz-sıcak)
const ic = new THREE.Mesh(new THREE.ConeGeometry(r * 0.55, 4.5, 12, 1, true), alevIcMal);
ic.rotation.x = Math.PI;
ic.position.set(x, y - 2.4, z);
ic.visible = false;
roket.add(ic);
alevler.push(ic);
}
// Çekirdek motor nozulları (roket içinde, y = -6.1)
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
alevEkle(Math.cos(aci) * 1.5, -6.1, Math.sin(aci) * 1.5, 0.9);
}
// Booster nozulları (roket içinde, y = 2.2)
for (let i = 0; i < 4; i++){
const aci = (i / 4) * Math.PI * 2 + Math.PI / 4;
alevEkle(Math.cos(aci) * 4.1, 2.2, Math.sin(aci) * 4.1, 1.05);
}
/* ---- SERVİS KOLLARI: kuleden rokete uzanan 3 kol ---- */
const servisKollari = [];
const kolMal = new THREE.MeshStandardMaterial({ color: 0x6b7684, roughness: 0.6, metalness: 0.75 });
[-22, -6, 10].forEach((y, i) => {
const kol = new THREE.Group();
kol.position.set(0, y, 0); // kule grubuna göre (kule -17,3,-2'de)
const govde = kutu(7, 0.55, 0.9, kolMal);
govde.position.x = 3.5;
kol.add(govde);
kol.userData.kapali = 0; // rokete uzanmış
kol.userData.acik = -1.15 - i * 0.08; // geri çekilmiş
kule.add(kol);
servisKollari.push(kol);
});
/* ---- KADEME AYRILMASI: üst kademe + burun ayrı gruba ---- */
const kademeAyrik = new THREE.Group();
roket.add(kademeAyrik);
// NUMEX-DÜZELTME (2026-10-22): Kule sahnesi roketin yanında havada uçuyordu: bu blok roket.children içinden y>30 olan her şeyi kademeAyrik grubuna taşıyordu ve YAPI_PARCA listesini de bozuyordu. kademeAyrik zaten boş bir grup; ayrılma animasyonu roket.rotation ve pozisyonla sağlandığından bu taşıma gereksiz — kaldırıldı.
// Not: üst kademe ayrılması roket.rotation.z + kademeAyrik konumu ile
// canlandırılıyor. Parçaları gruplar arasında taşımıyoruz — bu, sahnenin
// diğer nesnelerini (kule, küreler) yanlışlıkla rokete bağlıyordu.
function firlatmaGuncelle(dt){
if (!firlatmaAktif) return;
firlatmaZaman += dt;
// Aşama metni
let asama = ASAMALAR[0];
for (const a of ASAMALAR){
if (firlatmaZaman >= a.t) asama = a;
}
asamaAdEl.textContent = asama.ad;
const kalan = Math.max(0, 10 - firlatmaZaman);
geriSayimEl.textContent = firlatmaZaman < 10
? 'T-' + Math.ceil(kalan)
: 'T+' + Math.floor(firlatmaZaman - 10);
// Servis kolları 6-9 sn arası çekilir
const kolOran = Math.min(1, Math.max(0, (firlatmaZaman - 6) / 3));
servisKollari.forEach((k, i) => {
const yerel = Math.min(1, Math.max(0, (kolOran - i * 0.12) / 0.7));
const yumusak = yerel * yerel * (3 - 2 * yerel);
k.rotation.z = k.userData.kapali + (k.userData.acik - k.userData.kapali) * yumusak;
});
// Ateşleme: 9 sn'den sonra alev
if (firlatmaZaman >= 9){
const guc = Math.min(1, (firlatmaZaman - 9) / 1.5);
alevler.forEach((a, i) => {
a.visible = true;
const titresim = 1 + Math.sin(firlatmaZaman * 34 + i) * 0.12;
a.scale.set(guc * titresim, guc * (1.6 + Math.min(2.2, (firlatmaZaman - 9) * 0.8)) * titresim, guc * titresim);
});
}
// Kalkış: 11 sn'den sonra yüksel
if (firlatmaZaman >= 11){
const y = Math.min(1, (firlatmaZaman - 11) / 4);
kalkanRoketY = 11 + y * y * 150;
}
roket.position.y = kalkanRoketY;
// Kamera roketi takip etsin (kalkıştan sonra yukarı kaydır)
if (firlatmaZaman >= 11){
// NUMEX-DÜZELTME (2026-10-22): Kalkışta roket kadrajdan taşıyordu (burun üstten kesik): roket 150 birim yükselirken kamera yalnız 42 birim uzaklaşıyordu. Takip mesafesini yükselişle orantılı hale getirdim (96 + yükseliş*1.15) — roket hızla uzaklaşsa bile kadrajda kalır.
const yukselis = kalkanRoketY - 11; // 0 → 150
// NUMEX-DÜZELTME (2026-10-22): Kalkış takibi roketi ekranın %20'sine küçültüyor ve rampayı kadrajdan tamamen çıkarıyordu (hedef hem çok yukarı hem çok uzak). Takibi roketin yükselişinin ~%55'i kadar yukarı kaldırıp mesafeyi 96→118 aralığında tuttum; böylece roket %40 dolulukta kalır ve rampa altta referans olarak görünmeye devam eder.
// NUMEX-DÜZELTME (2026-10-22): Kamera takibi roketi kaçırıyordu (T+9'da roket ekran tepesinden çıkıyordu): hedef yalnız yükselişin %55'ini izliyordu. Hedefi roketin GERÇEK konumuna bağladım; roket artık kadrajdan çıkamaz.
// Hedef = roketin gerçek konumu (gövde ortası); roket asla kadrajdan çıkmaz
const hedefY = kalkanRoketY + 14;
kontroller.hedef.y += (hedefY - kontroller.hedef.y) * Math.min(1, dt * 4.0);
// Mesafe roket rahat sığacak kadar artsın, aşırı uzaklaşma yok
kontroller.hedefMesafe = Math.min(135, 96 + yukselis * 0.22);
}
// Kademe ayrılması: 15 sn'den sonra — ayrılan parçalar yana savrulur
if (firlatmaZaman >= 15){
const y = Math.min(1, (firlatmaZaman - 15) / 3.5);
const yumusak = y * y * (3 - 2 * y);
kademeAyrik.position.y = -yumusak * 18; // aşağıda kalır
kademeAyrik.position.x = yumusak * 9; // yana kayar
kademeAyrik.position.z = yumusak * 5;
kademeAyrik.rotation.z = -yumusak * 0.32; // yatıp savrulur
roket.rotation.z = yumusak * 0.06;
}
if (firlatmaZaman > 22) firlatmaAktif = false;
}
/* ---- Bölüm değiştirme ---- */
function bolumSec(yeni){
bolum = yeni;
SEKMELER.forEach(s => s.classList.toggle('aktif', s.dataset.bolum === yeni));
firlatmaPanel.classList.toggle('acik', yeni === 'firlatma');
olcuKatman.style.display = (yeni === 'yapi') ? 'block' : 'none';
if (yeni === 'yapi'){
// NUMEX-DÜZELTME (2026-10-22): Yapı bölümü hiç açılmıyordu: yapiHedef=1 atanıyordu ama hemen ardından çağrılan firlatmaSifirla() içinde yapiHedef=0 yapılıyordu (satır 1554), yani hedef hep sıfıra dönüyordu. Sıra düzeltildi: önce sıfırla, sonra hedefi ata.
firlatmaSifirla(); // önce temizle (yapiHedef'i 0 yapar)
yapiHedef = 1; // SONRA hedefi ata — sıra önemli
} else if (yeni === 'genel'){
yapiHedef = 0;
firlatmaSifirla();
} else if (yeni === 'firlatma'){
yapiHedef = 0;
firlatmaBaslat();
}
}
SEKMELER.forEach(s => s.addEventListener('click', () => bolumSec(s.dataset.bolum)));
/* Geliştirici kipi: Ctrl+Shift+D */
const GELISTIRICI_ANAHTARI = 'gokbey9-gelistirici';
let gelistiriciKipi = localStorage.getItem(GELISTIRICI_ANAHTARI) === '1';
function gelistiriciUygula(){
perfPanel.classList.toggle('acik', gelistiriciKipi);
}
gelistiriciUygula();
window.addEventListener('keydown', e => {
if (e.ctrlKey && e.shiftKey && (e.key === 'D' || e.key === 'd')){
e.preventDefault();
gelistiriciKipi = !gelistiriciKipi;
localStorage.setItem(GELISTIRICI_ANAHTARI, gelistiriciKipi ? '1' : '0');
gelistiriciUygula();
}
});
/* =========================================================
YÜKLENİYOR EKRANI + DÖNGÜ
========================================================= */
window.addEventListener('resize', () => {
// NUMEX-DÜZELTME (2026-10-22): Pencere boyutu değişince tuval yine sabit pencere ölçüsüne dönüyordu; artık CSS kutusundan ölçülen tek fonksiyon kullanılıyor.
kamera.aspect = window.innerWidth / window.innerHeight;
kamera.updateProjectionMatrix();
kameraBoyutla();
tuvalBoyutla();
});
const saat = new THREE.Clock();
function dongu(){
requestAnimationFrame(dongu);
const dt = saat.getDelta();
// NUMEX-DÜZELTME (2026-10-22): Kök neden: kaldırılan hedefKamera değişkenine döngüde hâlâ başvuruluyordu — tanımsız değişken ReferenceError fırlatıp animasyon döngüsünü her karede kırardı.
// (kamera konumu kontroller.update içinde tek noktadan yönetilir)
kontroller.update();
// Bölüm animasyonları
firlatmaGuncelle(dt);
yapiOran += (yapiHedef - yapiOran) * Math.min(1, dt * 3.2);
if (bolum === 'yapi') yapiUygula(yapiOran);
olculeriGuncelle(bolum === 'yapi');
// ÖLÇÜM (geçici): bölüm ve parça sayısını bir kez yazdır
if (!window.__numexOlcum){
window.__numexOlcum = true;
console.info('[YAPI] bolum=' + bolum + ' parca=' + YAPI_PARCA.length +
' oran=' + yapiOran.toFixed(2) + ' hedef=' + yapiHedef.toFixed(2));
}
// Rüzgârda ağır ağır salınan yıldız ışığı hissi için hafif ışık titremesi
const t = saat.getElapsedTime();
gunes.intensity *= 1 + Math.sin(t * 0.4) * 0.004;
// Yükselti etkisi: bulut düzlemini çok yavaş kaydır
if (bulutlar){
bulutlar.rotation.y = t * 0.004;
}
cizici.render(sahne, aktifKamera);
sayaciGuncelle();
}
// Yükleniyor ekranını kapat
setTimeout(() => {
const yuk = document.getElementById('yukleniyor');
if (yuk){
yuk.style.opacity = '0';
setTimeout(() => yuk.remove(), 700);
}
}, 600);
dongu();
/* =========================================================
BAŞLANGIÇ ZAMAN DİLİMİ PARLAKLIĞI (ipucu metni)
========================================================= */
</script>
</body>
</html>