skog, mjuka skuggor, jämnare övergång vatten på av avstånd

This commit is contained in:
2026-11-02 23:25:51 +01:00
parent cb5070dc1c
commit f447fe8e51
196 changed files with 5583 additions and 143 deletions

View File

@@ -0,0 +1,90 @@
"""Fetch reproducible Poly Haven glTF sources and PBR ground materials (CC0)."""
import concurrent.futures, hashlib, json, pathlib, urllib.request, time
ROOT = pathlib.Path(__file__).resolve().parents[2]
DEST = ROOT / 'Content' / 'Vegitations'
ASSETS = ['pine_tree_01', 'fir_tree_01', 'tree_small_02', 'fir_sapling_medium',
'shrub_01', 'shrub_04', 'fern_02', 'grass_medium_01', 'grass_medium_02', 'weed_plant_02']
HEADERS = {'User-Agent': 'UniverseVegetationImporter/1.0 (Poly Haven CC0 asset preparation)'}
def request(url):
return urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=120)
def md5(path):
h = hashlib.md5()
with path.open('rb') as f:
for block in iter(lambda: f.read(4*1024*1024), b''): h.update(block)
return h.hexdigest()
def fetch(item):
path = DEST / item['path']
if not path.resolve().is_relative_to(DEST.resolve()): raise RuntimeError('Asset path escapes content folder')
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and path.stat().st_size == item['size'] and md5(path) == item['md5']:
return
partial = path.with_suffix(path.suffix + '.partial')
for attempt in range(4):
try:
with request(item['url']) as response, partial.open('wb') as output:
received = 0
while True:
block = response.read(1024*1024)
if not block: break
received += len(block)
if received > item['size']: raise RuntimeError('Download exceeds declared size: '+str(path))
output.write(block)
if partial.stat().st_size != item['size'] or md5(partial) != item['md5']:
raise RuntimeError('Checksum/length mismatch: ' + str(path))
partial.replace(path)
print('Downloaded', item['path'], round(item['size']/1048576, 1), 'MiB', flush=True)
return
except Exception:
if attempt == 3: raise
time.sleep(attempt + 1)
def main():
DEST.mkdir(parents=True, exist_ok=True)
manifest_path = DEST / 'sources.json'
if manifest_path.exists():
manifest = json.loads(manifest_path.read_text())
else:
files = []
for asset in ASSETS:
with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f)
model = data['gltf']['2k']['gltf']
entries = {'model.gltf': model, **model.get('include', {})}
for relative, entry in entries.items():
files.append({**{k: entry[k] for k in ('url','size','md5')}, 'path': asset + '/source/' + relative})
# Ground materials share the same biome field as the plants.
for asset in ['forest_ground_04', 'withered_grass', 'snow_02']:
with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f)
for kind in ['diff', 'nor_gl', 'rough']:
key = next(k for k in data if k.lower() in ({'diff': ['diff','diffuse'], 'rough':['rough','roughness'], 'nor_gl':['nor_gl']}[kind]))
entry = data[key]['2k']['jpg']
files.append({**{k:entry[k] for k in ('url','size','md5')}, 'path':'ground/'+asset+'/'+kind+'.jpg'})
manifest = {'provider':'Poly Haven', 'license':'CC0', 'source':'https://polyhaven.com', 'files':files}
size = sum(f['size'] for f in files)
if size > 2_000_000_000: raise RuntimeError('Original asset budget exceeded: '+str(size))
manifest_path.write_text(json.dumps(manifest, indent=2)+'\n')
if not manifest.get('foliageMasksIncluded'):
for asset in ASSETS:
with request('https://api.polyhaven.com/files/' + asset) as f: data = json.load(f)
for kind, variants in data.items():
if 'alpha' not in kind.lower() and 'dry_diff' not in kind.lower(): continue
formats=variants.get('2k',{})
extension='png' if 'png' in formats else 'jpg'
if extension not in formats: continue
entry=formats[extension]
manifest['files'].append({**{k:entry[k] for k in ('url','size','md5')},
'path':asset+'/source/masks/'+kind+'.'+extension})
manifest['foliageMasksIncluded']=True
if sum(f['size'] for f in manifest['files'])>2_000_000_000: raise RuntimeError('Asset budget exceeded')
manifest_path.write_text(json.dumps(manifest,indent=2)+'\n')
print('Poly Haven CC0 assets:', len(manifest['files']), 'files,', round(sum(f['size'] for f in manifest['files'])/1048576), 'MiB', flush=True)
if any(f['size']<=0 for f in manifest['files']) or sum(f['size'] for f in manifest['files'])>2_000_000_000:
raise RuntimeError('Source download budget exceeded')
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
list(executor.map(fetch, manifest['files']))
print('All source checksums verified.', flush=True)
if __name__ == '__main__': main()

738
tools/vegetation/package-lock.json generated Normal file
View File

@@ -0,0 +1,738 @@
{
"name": "universe-vegetation-preparation",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "universe-vegetation-preparation",
"dependencies": {
"@gltf-transform/core": "^4.5.1",
"@gltf-transform/extensions": "^4.5.1",
"@gltf-transform/functions": "^4.5.1",
"meshoptimizer": "^1.3.0",
"sharp": "^0.35.5"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@gltf-transform/core": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@gltf-transform/core/-/core-4.5.1.tgz",
"integrity": "sha512-OWgBYssKHKoXMIrMb2N20jXqWRnMP9HdtcTFQBEAJtz4P+OOxx5fozl5eAeK7nktIs5uDE/lu4NbkE5VCZaR9g==",
"license": "MIT",
"dependencies": {
"property-graph": "^4.1.0"
},
"funding": {
"url": "https://github.com/sponsors/donmccurdy"
}
},
"node_modules/@gltf-transform/extensions": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@gltf-transform/extensions/-/extensions-4.5.1.tgz",
"integrity": "sha512-3jTe+GWRy4fj1TU790LPsO+rpO/4byUWPTpNkjy6M6h8RvrUivRf12Mptc0770K4ZczibpJDNHXZFkFsFMRDLA==",
"license": "MIT",
"dependencies": {
"@gltf-transform/core": "^4.5.1",
"ktx-parse": "^1.1.0"
},
"funding": {
"url": "https://github.com/sponsors/donmccurdy"
}
},
"node_modules/@gltf-transform/functions": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.5.1.tgz",
"integrity": "sha512-rfm4nQY2DJ+ip1e15xp7YfQwhRno3kPLcGw4EZiRk1dVwMV3cdFMcF08glYcO/+oLyYkUEs3xXMJYXa0cPgcTg==",
"license": "MIT",
"dependencies": {
"@gltf-transform/core": "^4.5.1",
"@gltf-transform/extensions": "^4.5.1",
"ktx-parse": "^1.1.0",
"ndarray": "^1.0.19",
"ndarray-lanczos": "^0.3.0",
"ndarray-pixels": "^5.2.0"
},
"funding": {
"url": "https://github.com/sponsors/donmccurdy"
}
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
"integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
"integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.4"
}
},
"node_modules/@img/sharp-freebsd-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
"integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
"integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
"integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
"integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
"integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
"integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
"integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
"integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
"integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
"integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
"integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
"integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
"integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
"integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
"integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
"integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
"integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
"integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
"integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
"integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.11.3"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-webcontainers-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
"integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
"integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
"integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
"integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@types/ndarray": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.1.0.tgz",
"integrity": "sha512-go+C6vHBUuO9jH6Tj6qEZ63JEGoMSxFdmd+6QAtthGD6xumBfIz3RbNDDs2aoRVLf9naH8lk5jnWy2UHHKgYQQ==",
"license": "MIT"
},
"node_modules/cwise-compiler": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
"integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==",
"license": "MIT",
"dependencies": {
"uniq": "^1.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/iota-array": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==",
"license": "MIT"
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/ktx-parse": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz",
"integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==",
"license": "MIT"
},
"node_modules/meshoptimizer": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.3.0.tgz",
"integrity": "sha512-2oZqMAcEEiCtXWUjIGtZ71oPx1B0ZxYGTqIKsXB1wzWFhleNsCFxQuUJzgCV706bRcwPWtMGf1odmQvAcqKkIw==",
"license": "MIT"
},
"node_modules/ndarray": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.1.1.tgz",
"integrity": "sha512-AsE8w/M2MS+0pD3nP5qbRRtJFeJJJaJ+qXZaZr0XjVGXhy6LIj2TERTlkpdK2AI3WufoOP+pwPk3q2ilAJcZ9g==",
"license": "MIT",
"dependencies": {
"iota-array": "^1.0.0",
"is-buffer": "^1.0.2"
}
},
"node_modules/ndarray-lanczos": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz",
"integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==",
"license": "MIT",
"dependencies": {
"@types/ndarray": "^1.0.11",
"ndarray": "^1.0.19"
}
},
"node_modules/ndarray-ops": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz",
"integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==",
"license": "MIT",
"dependencies": {
"cwise-compiler": "^1.0.0"
}
},
"node_modules/ndarray-pixels": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz",
"integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==",
"license": "MIT",
"dependencies": {
"@types/ndarray": "^1.0.14",
"ndarray": "^1.0.19",
"ndarray-ops": "^1.2.2",
"sharp": "^0.35.0"
}
},
"node_modules/property-graph": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz",
"integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
"semver": "^7.8.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.35.5",
"@img/sharp-darwin-x64": "0.35.5",
"@img/sharp-freebsd-wasm32": "0.35.5",
"@img/sharp-libvips-darwin-arm64": "1.3.4",
"@img/sharp-libvips-darwin-x64": "1.3.4",
"@img/sharp-libvips-linux-arm": "1.3.4",
"@img/sharp-libvips-linux-arm64": "1.3.4",
"@img/sharp-libvips-linux-ppc64": "1.3.4",
"@img/sharp-libvips-linux-riscv64": "1.3.4",
"@img/sharp-libvips-linux-s390x": "1.3.4",
"@img/sharp-libvips-linux-x64": "1.3.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
"@img/sharp-libvips-linuxmusl-x64": "1.3.4",
"@img/sharp-linux-arm": "0.35.5",
"@img/sharp-linux-arm64": "0.35.5",
"@img/sharp-linux-ppc64": "0.35.5",
"@img/sharp-linux-riscv64": "0.35.5",
"@img/sharp-linux-s390x": "0.35.5",
"@img/sharp-linux-x64": "0.35.5",
"@img/sharp-linuxmusl-arm64": "0.35.5",
"@img/sharp-linuxmusl-x64": "0.35.5",
"@img/sharp-webcontainers-wasm32": "0.35.5",
"@img/sharp-win32-arm64": "0.35.5",
"@img/sharp-win32-ia32": "0.35.5",
"@img/sharp-win32-x64": "0.35.5"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
"integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
"license": "0BSD",
"optional": true
},
"node_modules/uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==",
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "universe-vegetation-preparation",
"private": true,
"type": "module",
"scripts": {
"prepare": "node --max-old-space-size=16384 prepare.mjs"
},
"dependencies": {
"@gltf-transform/core": "^4.5.1",
"@gltf-transform/extensions": "^4.5.1",
"@gltf-transform/functions": "^4.5.1",
"meshoptimizer": "^1.3.0",
"sharp": "^0.35.5"
}
}

View File

@@ -0,0 +1,143 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {NodeIO, Document, getBounds} from '@gltf-transform/core';
import {ALL_EXTENSIONS} from '@gltf-transform/extensions';
import {prune, dedup, flatten, transformMesh, cloneDocument, mergeDocuments, simplify, compactPrimitive, unpartition} from '@gltf-transform/functions';
import {MeshoptSimplifier} from 'meshoptimizer';
import sharp from 'sharp';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../Content/Vegitations');
const manifest=JSON.parse(await fs.readFile(path.join(root,'sources.json'),'utf8'));
const ids=[...new Set(manifest.files.filter(f=>f.path.endsWith('/source/model.gltf')).map(f=>f.path.split('/')[0]))];
const io=new NodeIO().registerExtensions(ALL_EXTENSIONS);
await MeshoptSimplifier.ready;
for(const id of ids) {
const selected=process.argv.slice(2).filter(a=>!a.startsWith('--'));
if(selected.length&&!selected.includes(id))continue;
const out=path.join(root,id,'prepared');await fs.mkdir(out,{recursive:true});
const cards=JSON.parse(await fs.readFile(path.join(out,'cards.json'),'utf8').catch(()=>'null'));
const sourceOnly=process.argv.includes('--source-only');
const stamp=JSON.stringify({version:5,cards:!!cards,files:manifest.files.filter(f=>f.path.startsWith(id+'/')).map(f=>f.md5)});
if(sourceOnly&&await fs.stat(path.join(root,id,'source/bake-source.glb')).catch(()=>null))continue;
if(!sourceOnly&&await fs.readFile(path.join(out,'stamp.json'),'utf8').catch(()=>null)===stamp){console.log('Cached',id);continue;}
console.log('Preparing',id);
const doc=await io.read(path.join(root,id,'source/model.gltf'));
await doc.transform(flatten());
const scene=doc.getRoot().getDefaultScene()??doc.getRoot().listScenes()[0];
const all=scene.listChildren();
const chosen=all.length<=3?all:[all[0],all[Math.floor(all.length/3)],all[Math.floor(all.length*2/3)]];
for(const node of all)if(!chosen.includes(node))node.dispose();
// Apply source transforms and give each variant a gravity-aligned, bottom-centred origin.
for(let i=0;i<chosen.length;i++) {
const node=chosen[i],mesh=node.getMesh();
if(!mesh)throw new Error('Expected a static mesh at root: '+id);
transformMesh(mesh,node.getWorldMatrix());node.setMatrix([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);
const b=getBounds(node),offset=[-(b.min[0]+b.max[0])*.5,-b.min[1],-(b.min[2]+b.max[2])*.5];
transformMesh(mesh,[1,0,0,0,0,1,0,0,0,0,1,0,...offset,1]);
node.setName('variant_'+i);
}
if(sourceOnly) {
await doc.transform(unpartition());await io.write(path.join(root,id,'source/bake-source.glb'),doc);
console.log('Prepared full-detail baking source',id);continue;
}
// Dense geometric foliage is baked in small spatial clusters, preserving its volume
// instead of reducing millions of individual needles/leaves to isolated triangles.
if(cards)for(let variant=0;variant<chosen.length;variant++) {
const mesh=chosen[variant].getMesh();
for(const p of [...mesh.listPrimitives()])if(/twig|leaves/.test(p.getMaterial().getName().toLowerCase())){mesh.removePrimitive(p);p.dispose();}
const color=doc.createTexture('canopy').setImage(await fs.readFile(path.join(out,`cards-${variant}-color.png`))).setMimeType('image/png');
const normal=doc.createTexture('canopy_normal').setImage(await fs.readFile(path.join(out,`cards-${variant}-normal.png`))).setMimeType('image/png');
const material=doc.createMaterial('baked_canopy').setBaseColorTexture(color).setNormalTexture(normal)
.setMetallicFactor(0).setRoughnessFactor(.9).setAlphaMode('MASK').setAlphaCutoff(.4).setDoubleSided(true);
const pos=[],norm=[],uv=[],indices=[];
for(const card of cards.Variants[variant]) {
const index=pos.length/3;
for(const [x,y] of [[-.5,-.5],[.5,-.5],[.5,.5],[-.5,.5]]) {
pos.push(...card.Center.map((v,k)=>v+(card.Right[k]*x+card.Up[k]*y)*card.Size));
norm.push(...card.Normal);
uv.push(card.Uv[0]+(x+.5)*card.Uv[2],card.Uv[1]+(.5-y)*card.Uv[3]);
}
indices.push(index,index+1,index+2,index,index+2,index+3);
}
const buffer=doc.getRoot().listBuffers()[0];
const accessor=(type,array)=>doc.createAccessor().setType(type).setArray(array).setBuffer(buffer);
mesh.addPrimitive(doc.createPrimitive().setAttribute('POSITION',accessor('VEC3',new Float32Array(pos)))
.setAttribute('NORMAL',accessor('VEC3',new Float32Array(norm))).setAttribute('TEXCOORD_0',accessor('VEC2',new Float32Array(uv)))
.setIndices(accessor('SCALAR',new Uint32Array(indices))).setMaterial(material));
}
await doc.transform(prune());
const masks=manifest.files.filter(f=>f.path.startsWith(id+'/source/masks/')&&f.path.toLowerCase().includes('alpha'));
for(const material of doc.getRoot().listMaterials()) {
const name=material.getName().toLowerCase();
if(name==='baked_canopy')continue;
const alpha=masks.find(f=>path.basename(f.path).toLowerCase()==='alpha.png'||name.includes(path.basename(f.path).toLowerCase().replace('_alpha.png','')));
const foliage=material.getAlphaMode()!=='OPAQUE'||/twig|leaves|grass|fern|shrub|weed/.test(name);
material.setMetallicFactor(0).setDoubleSided(foliage);
if(foliage&&alpha&&material.getBaseColorTexture()) {
const texture=material.getBaseColorTexture();
const info=await sharp(texture.getImage()).metadata();
const a=await sharp(await fs.readFile(path.join(root,alpha.path))).resize(info.width,info.height).extractChannel(0).toBuffer();
const rgba=await sharp(texture.getImage()).removeAlpha().joinChannel(a).png().toBuffer();
const replacement=doc.createTexture(name+'_alpha').setImage(rgba).setMimeType('image/png');
material.setBaseColorTexture(replacement).setAlphaMode('MASK').setAlphaCutoff(.4);
}
}
const tree=/tree|sapling/.test(id),budgets=tree?[12000,2000,350]:/shrub/.test(id)?[12000,3000,600]:[2500,800,200];
const merged=new Document();const stats=[];
for(let lod=0;lod<3;lod++) {
const copy=cloneDocument(doc),sc=copy.getRoot().listScenes()[0];sc.setName('LOD'+lod);
for(const node of sc.listChildren()) {
const primitives=node.getMesh().listPrimitives();
const total=primitives.reduce((n,p)=>n+(p.getIndices()?.getCount()??p.getAttribute('POSITION').getCount())/3,0);
const canopy=primitives.filter(p=>p.getMaterial().getName()==='baked_canopy').reduce((n,p)=>n+p.getIndices().getCount()/3,0);
for(const p of primitives) {
const count=p.getIndices()?.getCount()??p.getAttribute('POSITION').getCount();
const target=p.getMaterial().getName()==='baked_canopy'?count:Math.max(6,Math.floor((budgets[lod]-canopy)*count/Math.max(1,total-canopy)/3)*3);
if(count<=target)continue;
// The library performs topology/attribute-preserving reduction first.
// simplify() acts on the whole document below; retain the target per primitive for a bounded fallback.
p.setExtras({...p.getExtras(),target});
}
}
let original=0;for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives())original+=p.getIndices().getCount()/3;
const retained=[];
for(const mesh of copy.getRoot().listMeshes())for(const p of [...mesh.listPrimitives()])
if(p.getMaterial().getName()==='baked_canopy'){mesh.removePrimitive(p);retained.push([mesh,p]);}
await copy.transform(simplify({simplifier:MeshoptSimplifier,ratio:Math.min(1,budgets[lod]*chosen.length/original),error:.01*(lod+1),lockBorder:false}));
for(const [mesh,p] of retained)mesh.addPrimitive(p);
for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives()) {
const indices=new Uint32Array(p.getIndices().getArray()),positions=new Float32Array(p.getAttribute('POSITION').getArray());
const target=p.getExtras().target;
if(target&&indices.length>target*1.1) {
let [reduced]=MeshoptSimplifier.simplify(indices,positions,3,target,1,['Permissive','Prune']);
if(reduced.length>target*1.1)[reduced]=MeshoptSimplifier.simplifySloppy(indices,positions,3,null,target,1);
p.setIndices(copy.createAccessor().setType('SCALAR').setArray(reduced).setBuffer(copy.getRoot().listBuffers()[0]));
compactPrimitive(p);
}
p.setExtras({});
}
await copy.transform(prune());
let triangles=0;for(const m of copy.getRoot().listMeshes())for(const p of m.listPrimitives())triangles+=p.getIndices().getCount()/3;
stats.push(triangles);mergeDocuments(merged,copy);
}
merged.getRoot().setDefaultScene(merged.getRoot().listScenes()[0]);
await merged.transform(dedup(),unpartition(),prune());
for(const ext of merged.getRoot().listExtensionsUsed())ext.dispose();
await io.write(path.join(out,'model.glb'),merged);
const dryFile=manifest.files.find(f=>f.path.startsWith(id+'/source/masks/dry_diff'));
if(dryFile) {
const dry=cloneDocument(merged);
const mask=masks[0];
const image=await sharp(await fs.readFile(path.join(root,dryFile.path))).metadata();
const a=await sharp(await fs.readFile(path.join(root,mask.path))).resize(image.width,image.height).extractChannel(0).toBuffer();
const rgba=await sharp(await fs.readFile(path.join(root,dryFile.path))).removeAlpha().joinChannel(a).png().toBuffer();
const texture=dry.createTexture('dry_grass').setImage(rgba).setMimeType('image/png');
for(const m of dry.getRoot().listMaterials())m.setBaseColorTexture(texture);
await dry.transform(prune());await io.write(path.join(out,'dry.glb'),dry);
}
const variantBounds=chosen.map(n=>getBounds(n));
await fs.writeFile(path.join(out,'preparation.json'),JSON.stringify({source:'https://polyhaven.com/a/'+id,license:'CC0',budgets,triangles:stats,variants:variantBounds},null,2));
await fs.writeFile(path.join(out,'stamp.json'),stamp);
console.log('Prepared',id,stats);
}