Süpürge, çapa ve çiti yeniden kur; kapanma/alaca parametrelerini sınırla
Üçü de render'a bakılarak elden geçirildi. Ortak sebep: her birinde modelin KONUSU eksikti. çit — birleşim yoktu. Dört kare çubuk ve önlerine konmuş iki lata; derinlik/boy oranı 54:1, yani karton. Artık gerçek zıvana var: direk iki yanak + köprü bloklarından kuruluyor, delik geometrik olarak var oluyor, kiriş içinden geçip uçlarda karşı yüzden taşıyor. Ayrıca kiriş dağılımı formülü kaç kiriş olursa olsun hep 0.28–0.78 aralığını dolduruyordu — üstü ve altı doldurmak yapısal olarak imkânsızdı. 440 → 860 üçgen, derinlik 0.09 → 0.31 m. çapa — kaz boynu yoktu. Ağzı sapın ekseninden öne taşıyan kıvrık boyun olmadan model "kürsü / yol tabelası" diye okunuyordu. Boyun latheGeometry ile kurulup bükülüyor, ağız boyun ucunun teğetine hizalanıyor; ikisi de hesapla, gözle değil. süpürge — demet içi boş bir koni kabuğuydu: bütün levhalar tek halkada. Üç eşmerkezli halka (6/10/16) kütleyi kuruyor. Bağlar artık demetin dış yarıçapına oturuyor; eskiden ayrı bir tahmin formülüyle levhaların İÇİNDE kalıyor ve hiç görünmüyorlardı. Ölçerek bulunan iki gerçek hata: - bendGeometry'nin etkisi geometrinin y=0'a göre nerede durduğuna bağlı. Orijinde ORTALANMIŞ bir gövde simetrik bükülüyor: iki uç aynı yöne gidiyor, siluet değişmiyor. Çapanın ağzında Z aralığı 0.0337'den 0.0327'ye DÜŞÜYORDU — yani yorumda "sorunu çözüyor" dediğim kavis hiç yoktu. Tabanı orijinde kurulunca kaçış 44 mm. Yardımcıya yazıldı. - Süpürgede açılma işareti tersti: rotateX(+f) levhanın ucunu -Z'ye atıyor, rotateY onu EKSENE doğru çeviriyor. Levhalar ekseni geçip karşı tarafa savruluyordu; demetin kabuk olmasının asıl sebebi buydu. Gözle değil ölçüyle yakalandı (genişlik 0.23 beklerken 0.13). Sistemik: kapanma yarıçapı ve alaca hücresi modelin en büyük ölçüsünden türetiliyordu. 4.89 m'lik çitte hücre 0.27 m çıkıyor, yani bütün direk tek hücreye düşüyor ve doku sistemi hiçbir şey yapmıyordu; kapanma yarıçapı 0.69 m ile her şeyi battaniye gibi karartıyordu. İkisi de mutlak sınırlara alındı — ahşabın damar lekesi nesnenin boyundan bağımsızdır. Doğrulama: test yamalarının gerçek config alanlarına işaret ettiği artık denetleniyor. Çapa yeniden yazılırken `bladeAngle` alanı kalkmıştı ama test hâlâ onu yamalıyordu, yani o varyant sessizce varsayılan modeli sınıyordu. render.ts: --angles ile turntable. Tek bir 3/4 açı yanıltıcı — çit ancak tam yandan bakınca cılız görünüyor.
This commit is contained in:
parent
8a982160ac
commit
72f219d087
19 changed files with 1190 additions and 517 deletions
|
|
@ -1,13 +1,33 @@
|
|||
/**
|
||||
* @medieval-kit/wooden-fence
|
||||
*
|
||||
* Modüler çit bölümü. Kitin tek "tekrarlanabilir" parçası: `sections` alanını
|
||||
* artırarak uzatılır, ya da tek bölüm alıp sahnede kopyalanır.
|
||||
* Zıvanalı riven çit: yarılmış kalın direkler, direğin İÇİNDEN geçen kirişler.
|
||||
*
|
||||
* Direkler yatay kirişlerden dışa taşıyor; kirişlerin uçları direklerin içinde
|
||||
* kalıyor. İkisi de z-fighting'i yapı gereği imkânsız kılıyor.
|
||||
* İKİNCİ deneme ve sebebi tek kelimeyle söylenebilir: BİRLEŞİM. İlk hâlde
|
||||
* dört kare çubuk ve önlerine konmuş iki ince lata vardı; hiçbir noktada iki
|
||||
* parçanın birbirine nasıl tutunduğu görünmüyordu, dolayısıyla nesnenin bütün
|
||||
* konusu eksikti. Render'da çit değil "çitin teknik resmi" gibi okunuyordu —
|
||||
* 4.89 × 1.10 × 0.09 m, yani derinlik/boy oranı 54:1, karton.
|
||||
*
|
||||
* Gerçek riven post-and-rail çitinde direğe DİKDÖRTGEN BİR DELİK açılır ve
|
||||
* kiriş o delikten geçer. Modelin tamamı bu tek gerçeğin etrafında yeniden
|
||||
* kuruldu:
|
||||
*
|
||||
* - Direk artık tek kutu değil: iki YANAK ve aralarındaki KÖPRÜ blokları.
|
||||
* Delik böylece geometrik olarak var oluyor, boyanmış bir çentik değil.
|
||||
* `bakeOcclusion` da ağzını kendiliğinden karartıyor.
|
||||
* - Kiriş delikten geçip çitin iki ucunda karşı yüzden TAŞIYOR. Zıvana dili
|
||||
* siluete giren tek yatay çıkıntı ve "bu nasıl duruyor" sorusunu tek
|
||||
* başına cevaplıyor.
|
||||
* - Kiriş delikten dar: her yanda birkaç milimlik boşluk kalıyor, yani delik
|
||||
* kapanmıyor. Deliği delik gösteren şey o boşluk.
|
||||
*
|
||||
* Bir de dizilim: eski kiriş dağılımı `0.28 + 0.5·r/(count−1)` idi, yani kaç
|
||||
* kiriş olursa olsun HEP 0.28–0.78 aralığını dolduruyordu. Üstü ve altı
|
||||
* doldurmak yapısal olarak imkânsızdı; siluetin üst kenarı bu yüzden boştu.
|
||||
*/
|
||||
import { Color } from 'three'
|
||||
import type { BufferGeometry } from 'three'
|
||||
|
||||
import {
|
||||
chamferedBoxGeometry,
|
||||
|
|
@ -15,24 +35,31 @@ import {
|
|||
jitter,
|
||||
MEDIEVAL_PALETTE,
|
||||
mergeColoured,
|
||||
taperedBoxGeometry,
|
||||
} from '../core/index.ts'
|
||||
|
||||
export interface WoodenFenceConfig {
|
||||
/** Bölüm sayısı. Her bölüm iki direk arası. */
|
||||
readonly sections: number
|
||||
/** Bir bölümün uzunluğu (metre). */
|
||||
/** Bir bölümün uzunluğu (metre). Kiriş bir kütükten yarıldığı için 2–3 m. */
|
||||
readonly sectionLength: number
|
||||
readonly height: number
|
||||
/** Yatay kiriş sayısı. */
|
||||
readonly railCount: number
|
||||
/** Direklerin eğrilik ve boy sapması. 0 = fabrikasyon düzgünlük. */
|
||||
readonly rough: number
|
||||
/** Bir uca payanda konsun mu (0/1). */
|
||||
readonly brace: number
|
||||
readonly seed: number
|
||||
}
|
||||
|
||||
export const woodenFenceDefaults: WoodenFenceConfig = {
|
||||
sections: 3,
|
||||
sectionLength: 1.6,
|
||||
height: 1.1,
|
||||
railCount: 2,
|
||||
sections: 2,
|
||||
sectionLength: 2.4,
|
||||
height: 1.25,
|
||||
railCount: 3,
|
||||
rough: 1,
|
||||
brace: 1,
|
||||
seed: 12,
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +68,11 @@ export type WoodenFenceParts = 'posts' | 'rails'
|
|||
export function createModel(overrides: Partial<WoodenFenceConfig> = {}) {
|
||||
return createKitModel<WoodenFenceConfig, 'oak', WoodenFenceParts>({
|
||||
id: 'wooden-fence',
|
||||
// Alaca hücresi elle veriliyor: çit 4.8 m uzun, otomatik türetme onu
|
||||
// modelin ölçeğinden çıkarınca tek direk tek hücreye düşüyor ve doku
|
||||
// sistemi hiçbir şey yapmıyor. Ahşabın damar lekesi nesnenin boyundan
|
||||
// bağımsız olarak birkaç santimdir.
|
||||
mottle: { cell: 0.05 },
|
||||
defaults: woodenFenceDefaults,
|
||||
slots: ['oak'],
|
||||
build: ({ config, random }) => {
|
||||
|
|
@ -50,49 +82,191 @@ export function createModel(overrides: Partial<WoodenFenceConfig> = {}) {
|
|||
tint.offsetHSL(jitter(random, 0.012), jitter(random, 0.05), lift + jitter(random, 0.06))
|
||||
return tint
|
||||
}
|
||||
|
||||
const sections = Math.max(1, config.sections)
|
||||
const post = config.height * 0.085
|
||||
const total = sections * config.sectionLength
|
||||
const half = config.height / 2
|
||||
|
||||
const posts = []
|
||||
for (let i = 0; i <= sections; i += 1) {
|
||||
const x = -total / 2 + i * config.sectionLength
|
||||
// Her direk kendi boyunda: sıra hâlinde dizildiğinde tekdüze durmasın.
|
||||
const lift = jitter(random, config.height * 0.035)
|
||||
posts.push(chamferedBoxGeometry(
|
||||
[post, post],
|
||||
[post * 0.78, post * 0.78],
|
||||
config.height + lift,
|
||||
post * 0.13,
|
||||
[x, lift / 2, 0],
|
||||
shade(-0.03),
|
||||
))
|
||||
/** Damar ucu: yarılmış yüzey ve kesik uçlar. Çit bunu hiç kullanmıyordu. */
|
||||
const endGrain = (lift = 0): Color => {
|
||||
tint.copy(MEDIEVAL_PALETTE.oakEnd)
|
||||
tint.offsetHSL(jitter(random, 0.01), jitter(random, 0.04), lift + jitter(random, 0.05))
|
||||
return tint
|
||||
}
|
||||
const soil = (): Color => {
|
||||
tint.copy(MEDIEVAL_PALETTE.oak)
|
||||
tint.offsetHSL(jitter(random, 0.01), -0.3 + jitter(random, 0.04), -0.19 + jitter(random, 0.04))
|
||||
return tint
|
||||
}
|
||||
|
||||
const rails = []
|
||||
const count = Math.max(1, config.railCount)
|
||||
const sections = Math.max(1, Math.round(config.sections))
|
||||
const count = Math.max(1, Math.round(config.railCount))
|
||||
const total = sections * config.sectionLength
|
||||
const half = config.height / 2
|
||||
const rough = Math.max(0, config.rough)
|
||||
|
||||
// --- Ölçüler, hepsi yükseklikten türetiliyor --------------------------
|
||||
const postW = config.height * 0.12 // direğin çit boyunca genişliği
|
||||
const mortise = config.height * 0.062 // deliğin Z açıklığı
|
||||
const cheek = config.height * 0.03 // deliğin iki yanındaki et
|
||||
const postD = mortise + cheek * 2 // direğin toplam derinliği
|
||||
const railH = config.height * 0.098 // kirişin dikey yüksekliği
|
||||
const railD = config.height * 0.053 // kirişin derinliği — delikten DAR
|
||||
const tenon = config.height * 0.088 // uçlardaki taşma
|
||||
|
||||
// Kiriş yükseklikleri. Üs 1.12: aralıklar aşağı doğru sıkışıyor, çünkü
|
||||
// altından geçmeye çalışan hayvan küçük olandır.
|
||||
const railT = Array.from({ length: count }, (_, r) =>
|
||||
count === 1 ? 0.55 : 0.19 + 0.71 * Math.pow(r / (count - 1), 1.12))
|
||||
|
||||
// --- Direkler -----------------------------------------------------------
|
||||
const postPieces: BufferGeometry[] = []
|
||||
const slotHalf = railH / 2 + config.height * 0.008
|
||||
|
||||
for (let i = 0; i <= sections; i += 1) {
|
||||
const x = -total / 2 + i * config.sectionLength
|
||||
const postH = config.height + jitter(random, 0.075 * rough)
|
||||
const pieces: BufferGeometry[] = []
|
||||
|
||||
// İki yanak: deliğin duvarları. Tam boy, tabandan tepeye.
|
||||
for (const side of [-1, 1]) {
|
||||
pieces.push(chamferedBoxGeometry(
|
||||
[postW, cheek],
|
||||
[postW * 0.81, cheek * 0.94],
|
||||
postH,
|
||||
cheek * 0.2,
|
||||
[0, postH / 2, side * (mortise + cheek) / 2],
|
||||
shade(-0.11),
|
||||
shade(0.02),
|
||||
))
|
||||
}
|
||||
|
||||
// Köprüler: yuvaların ARASINI dolduran bloklar. Delik tam olarak
|
||||
// bunların bıraktığı boşluk. Kesitleri yanakların İÇİNDE kalıyor
|
||||
// (±Z yüzleri yanak katısına gömülü), yani hiçbir yüz çifti eş
|
||||
// düzlemde değil.
|
||||
//
|
||||
// Köprüler direğin iki UCUNA kadar gitmiyor: uçları yanakların
|
||||
// uçlarıyla aynı düzleme oturup titriyordu. İçeri çekilen paylar
|
||||
// görünmüyor — alttaki toprak yığınının, üstteki başlığın içinde
|
||||
// kalıyor.
|
||||
const inset = cheek * 0.3
|
||||
const bounds = [0, ...railT.flatMap((t) => [t * postH - slotHalf, t * postH + slotHalf]), postH]
|
||||
for (let k = 0; k + 1 < bounds.length; k += 2) {
|
||||
const lo = Math.max(inset, bounds[k]!)
|
||||
const hi = Math.min(postH - inset, bounds[k + 1]!)
|
||||
if (hi - lo < 1e-4) continue
|
||||
const taper = 1 - 0.19 * (lo / postH)
|
||||
pieces.push(taperedBoxGeometry(
|
||||
[postW * taper * 0.96, mortise + cheek * 1.1],
|
||||
[postW * (taper - 0.03) * 0.96, mortise + cheek * 1.1],
|
||||
hi - lo,
|
||||
[0, (lo + hi) / 2, 0],
|
||||
shade(-0.07),
|
||||
))
|
||||
}
|
||||
|
||||
// Başlık: baltayla yontulmuş, suyu akıtan sırt. Tabanı gövdenin İÇİNDE
|
||||
// ve kesiti gövdenin o yükseklikteki kesitinden BÜYÜK — `toolSocket`
|
||||
// deseninin aynısı, eş düzlem yüz çifti bu yüzden oluşmuyor.
|
||||
pieces.push(taperedBoxGeometry(
|
||||
[postW * 0.88, postD * 0.98],
|
||||
[postW * 0.74, postD * 0.13],
|
||||
config.height * 0.1,
|
||||
[0, postH - config.height * 0.016, 0],
|
||||
endGrain(-0.03),
|
||||
endGrain(0.07),
|
||||
))
|
||||
|
||||
// Kur → DÖNDÜR → taşı. Eski kod merkezi doğrudan geometri çağrısına
|
||||
// geçirdiği için döndürmek imkânsızdı; ızgara gibi dizilmesinin sebebi
|
||||
// buydu. Dönüşler küçük tutuluyor: 0.045 rad, delik yolunda 7 mm yanal
|
||||
// kayma demek ve delik payı 8 mm.
|
||||
const post = mergeColoured(pieces)
|
||||
post.rotateY(jitter(random, 0.045 * rough))
|
||||
post.rotateZ(jitter(random, 0.03 * rough))
|
||||
post.rotateX(jitter(random, 0.018 * rough))
|
||||
const sink = config.height * (0.012 + Math.abs(jitter(random, 0.012)))
|
||||
post.translate(x, -half - sink, 0)
|
||||
postPieces.push(post)
|
||||
|
||||
// Toprak yığını. DÖNMEZ: direğin eğikliği yığını yerden kaldırırdı.
|
||||
postPieces.push(taperedBoxGeometry(
|
||||
[postW * 2.2, postD * 2],
|
||||
[postW * 1.25, postD * 1.15],
|
||||
config.height * 0.11,
|
||||
[x, -half + config.height * 0.018, 0],
|
||||
soil(),
|
||||
))
|
||||
}
|
||||
|
||||
// --- Kirişler -------------------------------------------------------------
|
||||
const railPieces: BufferGeometry[] = []
|
||||
for (let r = 0; r < count; r += 1) {
|
||||
const t = count === 1 ? 0.55 : 0.28 + (0.5 * r) / (count - 1)
|
||||
const y = -half + t * config.height
|
||||
const y = -half + railT[r]! * config.height + jitter(random, config.height * 0.005)
|
||||
for (let i = 0; i < sections; i += 1) {
|
||||
const x = -total / 2 + (i + 0.5) * config.sectionLength
|
||||
// Kirişin uçları komşu direklerin ORTASINA kadar uzanır.
|
||||
rails.push(chamferedBoxGeometry(
|
||||
[config.sectionLength + post * 0.6, post * 0.5],
|
||||
[config.sectionLength + post * 0.6, post * 0.5],
|
||||
post * 0.62,
|
||||
post * 0.13,
|
||||
[x, y + jitter(random, config.height * 0.012), 0],
|
||||
shade(0.02),
|
||||
))
|
||||
const xc = -total / 2 + (i + 0.5) * config.sectionLength
|
||||
// Gövde iki komşu direğin deliğine giriyor ve orada komşu bölmenin
|
||||
// gövdesiyle uç uca buluşuyor.
|
||||
const body = chamferedBoxGeometry(
|
||||
[config.sectionLength + postW * 0.55, railD],
|
||||
[config.sectionLength + postW * 0.55, railD * 0.88],
|
||||
railH,
|
||||
railH * 0.09,
|
||||
[xc, y, jitter(random, railD * 0.06)],
|
||||
shade(0.05),
|
||||
shade(0.1),
|
||||
)
|
||||
railPieces.push(body)
|
||||
}
|
||||
|
||||
// Zıvana dili: YALNIZ iki uçta. Karşı yüzden taşan bu parça siluete
|
||||
// giren tek yatay çıkıntı; kirişin direğin içinden geçtiğini tek
|
||||
// başına anlatıyor. Ara direklerde taşma yok, çünkü orada iki gövde
|
||||
// deliğin içinde buluşuyor.
|
||||
for (const side of [-1, 1]) {
|
||||
const px = side * total / 2
|
||||
railPieces.push(taperedBoxGeometry(
|
||||
[tenon * 2, railD * 0.9],
|
||||
[tenon * 1.7, railD * 0.76],
|
||||
railH * 0.82,
|
||||
[px + side * tenon * 0.72, y, 0],
|
||||
shade(0.02),
|
||||
endGrain(0.05),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Payanda ---------------------------------------------------------------
|
||||
// Modelin tek eksen dışı hattı. Uç direği tarlaya doğru destekliyor.
|
||||
if (config.brace >= 0.5) {
|
||||
const rise = config.height * 0.72
|
||||
const run = config.sectionLength * 0.3
|
||||
const length = Math.hypot(run, rise)
|
||||
const atStart = random() < 0.5
|
||||
const brace = chamferedBoxGeometry(
|
||||
[postW * 0.72, postD * 0.36],
|
||||
[postW * 0.56, postD * 0.3],
|
||||
length,
|
||||
postW * 0.06,
|
||||
[0, 0, 0],
|
||||
shade(-0.05),
|
||||
endGrain(0.02),
|
||||
)
|
||||
// İşaret TERS görünüyor ama doğrusu bu: payandanın TEPESİ direğe
|
||||
// yaslanır, AYAĞI tarlaya basar. Ters çevrildiğinde ayağı direğin
|
||||
// dibinde, tepesi havada kalan bir çubuk çıkıyordu — hiçbir şeyi
|
||||
// desteklemeyen bir payanda.
|
||||
const angle = Math.atan2(run, rise)
|
||||
brace.rotateZ(atStart ? angle : -angle)
|
||||
brace.translate(
|
||||
(atStart ? -1 : 1) * (total / 2 - run / 2),
|
||||
-half + rise / 2 + config.height * 0.02,
|
||||
// Direğin arka yüzü EĞİK (konik), payandanınki dik — hiçbir
|
||||
// yükseklikte eş düzleme gelmiyorlar.
|
||||
-postD * 0.62,
|
||||
)
|
||||
postPieces.push(brace)
|
||||
}
|
||||
|
||||
return {
|
||||
posts: { slot: 'oak', geometry: mergeColoured(posts) },
|
||||
rails: { slot: 'oak', geometry: mergeColoured(rails) },
|
||||
posts: { slot: 'oak', geometry: mergeColoured(postPieces) },
|
||||
rails: { slot: 'oak', geometry: mergeColoured(railPieces) },
|
||||
}
|
||||
},
|
||||
}, overrides)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue