MORE RE-ORGANIZING

This commit is contained in:
2026-09-08 14:00:27 -07:00
parent 0c1ea70738
commit ad41f0e140
248 changed files with 1091 additions and 0 deletions

View File

@@ -1,73 +0,0 @@
import { access, mkdir } from 'node:fs/promises'
import { constants } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'
const here = dirname(fileURLToPath(import.meta.url))
const harnessRoot = resolve(here, '..')
const projectRoot = resolve(harnessRoot, '..')
const blendFile = resolve(projectRoot, 'blender_prototype', 'sanctification_card_material_prototype.blend')
const pythonScript = resolve(here, 'export_card_mesh.py')
const outputFile = resolve(harnessRoot, 'public', 'reference', 'card-mesh.glb')
const explicit = process.env.BLENDER_EXECUTABLE
const platformCandidates = process.platform === 'win32'
? [
'C:\\Program Files\\Blender Foundation\\Blender 5.2\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 5.1\\blender.exe',
'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe',
]
: [
'/usr/bin/blender',
'/usr/local/bin/blender',
'/snap/bin/blender',
]
async function exists(path) {
try {
await access(path, constants.X_OK)
return true
} catch {
return false
}
}
let blender = explicit
if (blender && !(await exists(blender))) {
throw new Error(`BLENDER_EXECUTABLE does not exist or is not executable: ${blender}`)
}
if (!blender) {
for (const candidate of platformCandidates) {
if (await exists(candidate)) {
blender = candidate
break
}
}
}
if (!blender) blender = 'blender'
await mkdir(dirname(outputFile), { recursive: true })
const args = [
'--background',
blendFile,
'--python',
pythonScript,
'--',
'--output',
outputFile,
]
console.log(`Exporting card mesh with ${blender}`)
const child = spawn(blender, args, { stdio: 'inherit', shell: false })
const exitCode = await new Promise((resolveExit, reject) => {
child.once('error', reject)
child.once('exit', (code) => resolveExit(code ?? 1))
})
if (exitCode !== 0) {
throw new Error(`Blender export failed with exit code ${exitCode}`)
}
console.log(`Card mesh exported to ${outputFile}`)

View File

@@ -1,67 +0,0 @@
import argparse
import sys
from pathlib import Path
import bpy
def script_args():
separator = sys.argv.index("--") if "--" in sys.argv else len(sys.argv)
parser = argparse.ArgumentParser()
parser.add_argument("--output", required=True)
return parser.parse_args(sys.argv[separator + 1:])
def slot_material(name, color):
material = bpy.data.materials.get(name) or bpy.data.materials.new(name)
material.diffuse_color = (*color, 1.0)
return material
args = script_args()
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
source = bpy.data.objects.get("CARD_David_Foil")
if source is None:
raise RuntimeError("Expected CARD_David_Foil in the Blender prototype")
bpy.ops.object.select_all(action="DESELECT")
export_object = source.copy()
export_object.data = source.data.copy()
export_object.name = "SanctificationCardMesh"
export_object.data.name = "SanctificationCardMesh"
export_object.animation_data_clear()
export_object.location = (0, 0, 0)
export_object.rotation_euler = (0, 0, 0)
export_object.scale = (1, 1, 1)
bpy.context.scene.collection.objects.link(export_object)
material_indices = [polygon.material_index for polygon in export_object.data.polygons]
export_object.data.materials.clear()
export_object.data.materials.append(slot_material("SLOT_FRONT", (0.8, 0.8, 0.8)))
export_object.data.materials.append(slot_material("SLOT_BACK", (0.25, 0.25, 0.25)))
export_object.data.materials.append(slot_material("SLOT_EDGE", (0.5, 0.42, 0.28)))
for polygon, material_index in zip(export_object.data.polygons, material_indices):
polygon.material_index = material_index
export_object.select_set(True)
bpy.context.view_layer.objects.active = export_object
bpy.ops.export_scene.gltf(
filepath=str(output),
export_format="GLB",
use_selection=True,
export_apply=True,
export_materials="EXPORT",
export_texcoords=True,
export_normals=True,
export_yup=True,
)
print({
"output": str(output),
"object": export_object.name,
"mesh": export_object.data.name,
"materials": [material.name for material in export_object.data.materials],
})

View File

@@ -1,170 +0,0 @@
import { createHash } from 'node:crypto'
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { PNG } from 'pngjs'
const here = dirname(fileURLToPath(import.meta.url))
const harnessRoot = resolve(here, '..')
const projectRoot = resolve(harnessRoot, '..')
const outputRoot = resolve(harnessRoot, 'public', 'reference')
const renderRoot = resolve(projectRoot, 'blender_prototype', 'renders')
await mkdir(resolve(outputRoot, 'renders'), { recursive: true })
const sources = {
david: resolve(projectRoot, 'legendary.png'),
timothy: resolve(projectRoot, 'common.png'),
back: resolve(projectRoot, 'card-back.png'),
}
await Promise.all([
copyFile(sources.david, resolve(outputRoot, 'david-front.png')),
copyFile(sources.timothy, resolve(outputRoot, 'timothy-front.png')),
copyFile(sources.back, resolve(outputRoot, 'card-back.png')),
])
const referenceRenders = [
'david_foil_closeup.png',
'david_foil_reflection.png',
'david_holographic_head_on.png',
'david_holographic_spectrum_angle.png',
'david_holographic_opposite_angle.png',
'card_three_quarter_thickness.png',
]
await Promise.all(referenceRenders.map((name) =>
copyFile(resolve(renderRoot, name), resolve(outputRoot, 'renders', name)),
))
const smoothstep = (edge0, edge1, value) => {
const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)))
return t * t * (3 - 2 * t)
}
const imageHsv = (r, g, b) => {
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
return {
saturation: max === 0 ? 0 : (max - min) / max,
value: max,
}
}
async function generateMask(sourcePath, outputPath, kind) {
const source = PNG.sync.read(await readFile(sourcePath))
const mask = new PNG({ width: source.width, height: source.height })
for (let y = 0; y < source.height; y += 1) {
for (let x = 0; x < source.width; x += 1) {
const index = (y * source.width + x) * 4
const { saturation, value } = imageHsv(
source.data[index] / 255,
source.data[index + 1] / 255,
source.data[index + 2] / 255,
)
let coverage
if (kind === 'david-no-cutouts') {
const u = x / (source.width - 1)
const v = 1 - y / (source.height - 1)
coverage = smoothstep(0.18, 0.62, saturation) * smoothstep(0.015, 0.16, value)
const side = smoothstep(0.34, 0.41, Math.abs(u - 0.5))
const above = smoothstep(0.245, 0.29, v)
const below = 1 - smoothstep(0.07, 0.105, v)
coverage *= Math.max(side, above, below)
} else {
coverage = smoothstep(0.08, 0.58, saturation) * smoothstep(0.015, 0.16, value)
}
const byte = Math.round(Math.max(0, Math.min(1, coverage)) * 255)
mask.data[index] = byte
mask.data[index + 1] = byte
mask.data[index + 2] = byte
mask.data[index + 3] = 255
}
}
await writeFile(outputPath, PNG.sync.write(mask))
}
async function generateLinenNormal(outputPath) {
const size = 512
const png = new PNG({ width: size, height: size })
const heightAt = (x, y) => {
const weave = Math.sin(x * 0.27) * 0.55 + Math.sin(y * 0.31) * 0.45
const cross = Math.sin((x + y) * 0.075) * 0.16
return weave + cross
}
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const dx = heightAt(x + 1, y) - heightAt(x - 1, y)
const dy = heightAt(x, y + 1) - heightAt(x, y - 1)
const nx = -dx * 0.22
const ny = -dy * 0.22
const length = Math.hypot(nx, ny, 1)
const index = (y * size + x) * 4
png.data[index] = Math.round((nx / length * 0.5 + 0.5) * 255)
png.data[index + 1] = Math.round((ny / length * 0.5 + 0.5) * 255)
png.data[index + 2] = Math.round((1 / length * 0.5 + 0.5) * 255)
png.data[index + 3] = 255
}
}
await writeFile(outputPath, PNG.sync.write(png))
}
await Promise.all([
generateMask(sources.david, resolve(outputRoot, 'david-finish-mask.png'), 'david-no-cutouts'),
generateMask(sources.timothy, resolve(outputRoot, 'timothy-finish-mask.png'), 'automatic'),
generateLinenNormal(resolve(outputRoot, 'linen-normal.png')),
])
const generatedFiles = [
'david-front.png',
'david-finish-mask.png',
'timothy-front.png',
'timothy-finish-mask.png',
'card-back.png',
'linen-normal.png',
...referenceRenders.map((name) => `renders/${name}`),
]
const hashes = {}
for (const name of generatedFiles) {
hashes[name] = createHash('sha256')
.update(await readFile(resolve(outputRoot, name)))
.digest('hex')
}
await writeFile(resolve(outputRoot, 'manifest.json'), JSON.stringify({
schemaVersion: 2,
referenceRevision: 'runtime-look-v4-2026-09-07',
sourceMaterialReference: 'blender_prototype/premium_finishes.py',
genericMaterialReference: 'blender_prototype/card_finish_material.py',
geometry: {
source: 'runtime-procedural',
widthMeters: 0.063,
heightMeters: 0.0882,
thicknessMeters: 0.0004,
cornerRadiusMeters: 0.0028,
},
fixtures: {
david: {
artwork: 'david-front.png',
finishMask: 'david-finish-mask.png',
maskRule: 'Premium v3 saturation/value and title-panel protection; face, hand, and lamb ellipse cutouts removed',
},
timothy: {
artwork: 'timothy-front.png',
finishMask: 'timothy-finish-mask.png',
maskRule: 'Artwork-independent saturation/value coverage',
},
},
normalMap: 'linen-normal.png',
referenceRenders: referenceRenders.map((name) => `renders/${name}`),
hashes,
}, null, 2))
console.log(`Reference assets ready in ${outputRoot}`)