Working but slow

This commit is contained in:
2026-07-06 23:11:42 +02:00
parent 09c1fba8e6
commit f93bcfb8d4
15 changed files with 4443 additions and 4205 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js'
import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js'
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
@@ -389,7 +389,7 @@ export function stepNaga3d(cells: Cells, settings: SceneParams) {
? universeFromCoordinates(coordinates)
: seedUniverseFromCells(cells)
const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0
const seed = typeof simulation.seed === 'string' ? simulation.seed : 'naga'
const seed = resolveRngSeed(simulation, 'naga')
const topology: NagaTopology = {
boundary: resolveBoundaryCondition(simulation.grid),
size
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js'
import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { Cells, SceneParams } from '../types.js'
import { emptyCellsLike } from './cellGrid.js'
@@ -17,7 +17,7 @@ export function stepWildfire2d(cells: Cells, settings: SceneParams) {
const width = cells[0]?.length ?? 0
const boundary = resolveBoundaryCondition(settings.simulation?.grid)
const spreadProbability = parseProbability(settings.simulation?.spreadProbability, 1)
const seed = typeof settings.simulation?.seed === 'string' ? settings.simulation.seed : 'wildfire'
const seed = resolveRngSeed(settings.simulation, 'wildfire')
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
+1 -1
View File
@@ -95,7 +95,7 @@ registerCaEngineRuntime(
id: 'naga-3d',
label: 'Naga 3D',
supportedClasses: [{ dimensions: 3, states: 7 }],
defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', seed: 'naga', edgeWrap: true },
defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', rng_seed: 'naga', edgeWrap: true },
step: stepNaga3d
},
{ replace: true }
@@ -36,10 +36,27 @@ function clampProbability(value: unknown, fallback: number) {
return Math.max(0, Math.min(1, typeof value === 'number' && Number.isFinite(value) ? value : fallback))
}
function resolveIcSeed(params: JsonObject, fallback: string) {
if (typeof params.ic_seed === 'string' && params.ic_seed.trim()) return params.ic_seed
if (typeof params.seed === 'string' && params.seed.trim()) return params.seed
return fallback
}
function randomNormal(random: () => number) {
const left = Math.max(Number.EPSILON, random())
const right = Math.max(Number.EPSILON, random())
return Math.sqrt(-2 * Math.log(left)) * Math.cos(2 * Math.PI * right)
}
function sampleNagaLength(meanLength: number, lengthStdDev: number, maxLength: number, random: () => number) {
if (lengthStdDev <= 0) return clampInteger(meanLength, 5, 1, maxLength)
return clampInteger(meanLength + randomNormal(random) * lengthStdDev, meanLength, 1, maxLength)
}
function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const density = clampDensity(params.density)
const seed = typeof params.seed === 'string' ? params.seed : 'studio-seed'
const random = seededRandom(seed)
const icSeed = resolveIcSeed(params, 'studio-seed')
const random = seededRandom(icSeed)
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const cells: number[][] = []
@@ -57,7 +74,7 @@ function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: {
kind: 'random-soup',
density,
seed
ic_seed: icSeed
}
}
}
@@ -83,11 +100,13 @@ function inBounds(x: number, y: number, z: number, width: number, height: number
function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const maxLength = Math.max(1, width * height * depth)
const count = clampInteger(params.count, 3, 1, Math.max(1, width * height * depth))
const length = clampInteger(params.length, 5, 1, Math.max(1, width * height * depth))
const meanLength = clampInteger(params.meanLength ?? params.length, 5, 1, maxLength)
const lengthStdDev = Math.max(0, Math.min(maxLength, typeof params.lengthStdDev === 'number' && Number.isFinite(params.lengthStdDev) ? params.lengthStdDev : 0))
const sameTypeProbability = clampProbability(params.sameTypeProbability, 0.8)
const seed = typeof params.seed === 'string' ? params.seed : 'naga-markov'
const random = seededRandom(seed)
const icSeed = resolveIcSeed(params, 'naga-markov')
const random = seededRandom(icSeed)
const occupied = new Set<string>()
const cells: number[][] = []
const maxAttempts = count * 300
@@ -100,6 +119,7 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
let y = Math.floor(random() * height)
let z = Math.floor(random() * depth)
let arrow = randomNagaArrow(random)
const length = sampleNagaLength(meanLength, lengthStdDev, maxLength, random)
const proposed: number[][] = []
const proposedKeys = new Set<string>()
let valid = true
@@ -164,9 +184,10 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: {
kind: 'naga-markov',
count,
length,
meanLength,
lengthStdDev,
sameTypeProbability,
seed,
ic_seed: icSeed,
created
}
}
+6
View File
@@ -72,6 +72,12 @@ export function seededRandom(seed: string) {
}
}
export function resolveRngSeed(simulation: JsonObject | undefined, fallback: string) {
if (typeof simulation?.rng_seed === 'string' && simulation.rng_seed.trim()) return simulation.rng_seed
if (typeof simulation?.seed === 'string' && simulation.seed.trim()) return simulation.seed
return fallback
}
export function isBoundaryCondition(value: unknown): value is BoundaryCondition {
return value === 'wrap' || value === 'mirror' || value === 'fixed'
}
+39 -22
View File
@@ -72,7 +72,7 @@ interface IcgConfig {
dimensions: number
states: number
density: number
seed: string
icSeed: string
}
interface EngineConfig {
@@ -324,7 +324,7 @@ function defaultIcgConfig(): IcgConfig {
dimensions: 2,
states: 2,
density: 0.28,
seed: 'studio-seed'
icSeed: 'studio-seed'
}
}
@@ -353,16 +353,16 @@ function icgParamsSchema(config: IcgConfig): JsonObject {
max: 1,
step: 0.01
},
seed: {
ic_seed: {
type: 'string',
label: 'Seed',
default: config.seed
label: 'IC seed',
default: config.icSeed
}
}
}
function icgDefaultParams(config: IcgConfig): JsonObject {
return { density: config.density, seed: config.seed }
return { density: config.density, ic_seed: config.icSeed }
}
function ruleLabel(ruleId: string) {
@@ -1644,8 +1644,8 @@ function IcgConfigFields({
</label>
</div>
<label>
Seed
<input value={config.seed} onChange={(event) => onChange((current) => ({ ...current, seed: event.target.value }))} />
IC seed
<input value={config.icSeed} onChange={(event) => onChange((current) => ({ ...current, icSeed: event.target.value }))} />
</label>
</div>
)
@@ -1817,7 +1817,11 @@ function configFromIcg(generator: InitialConditionGenerator): IcgConfig {
dimensions: supportedClass?.dimensions ?? 2,
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
density: typeof generator.default_params.density === 'number' ? generator.default_params.density : 0.28,
seed: typeof generator.default_params.seed === 'string' ? generator.default_params.seed : 'studio-seed'
icSeed: typeof generator.default_params.ic_seed === 'string'
? generator.default_params.ic_seed
: typeof generator.default_params.seed === 'string'
? generator.default_params.seed
: 'studio-seed'
}
}
@@ -2168,7 +2172,8 @@ function LibraryEditor({
const [selectedEngineId, setSelectedEngineId] = React.useState('')
const [icgDensity, setIcgDensity] = React.useState(0.28)
const [icgNagaCount, setIcgNagaCount] = React.useState(3)
const [icgNagaLength, setIcgNagaLength] = React.useState(5)
const [icgNagaMeanLength, setIcgNagaMeanLength] = React.useState(5)
const [icgNagaLengthStdDev, setIcgNagaLengthStdDev] = React.useState(0)
const [icgNagaSameTypeProbability, setIcgNagaSameTypeProbability] = React.useState(0.8)
const [icgSeed, setIcgSeed] = React.useState('studio-seed')
const [nodeName, setNodeName] = React.useState('Untitled preset')
@@ -2664,24 +2669,28 @@ function LibraryEditor({
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
if (typeof generator?.default_params.density === 'number') setIcgDensity(generator.default_params.density)
if (typeof generator?.default_params.count === 'number') setIcgNagaCount(generator.default_params.count)
if (typeof generator?.default_params.length === 'number') setIcgNagaLength(generator.default_params.length)
if (typeof generator?.default_params.meanLength === 'number') setIcgNagaMeanLength(generator.default_params.meanLength)
else if (typeof generator?.default_params.length === 'number') setIcgNagaMeanLength(generator.default_params.length)
if (typeof generator?.default_params.lengthStdDev === 'number') setIcgNagaLengthStdDev(generator.default_params.lengthStdDev)
if (typeof generator?.default_params.sameTypeProbability === 'number') {
setIcgNagaSameTypeProbability(generator.default_params.sameTypeProbability)
}
if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed)
if (typeof generator?.default_params.ic_seed === 'string') setIcgSeed(generator.default_params.ic_seed)
else if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed)
}
function selectedIcgParams(generator: InitialConditionGenerator) {
if (generator.generator_kind === 'naga-markov') {
return {
count: icgNagaCount,
length: icgNagaLength,
meanLength: icgNagaMeanLength,
lengthStdDev: icgNagaLengthStdDev,
sameTypeProbability: icgNagaSameTypeProbability,
seed: icgSeed
ic_seed: icgSeed
}
}
return { density: Math.max(0, Math.min(1, icgDensity)), seed: icgSeed }
return { density: Math.max(0, Math.min(1, icgDensity)), ic_seed: icgSeed }
}
function applySelectedIcg() {
@@ -3204,12 +3213,6 @@ function LibraryEditor({
</label>
{selectedIcg?.generator_kind === 'naga-markov' ? (
<>
<div className="root-config-grid">
<label>
Length
<input min={1} step={1} type="number" value={icgNagaLength} onChange={(event) => setIcgNagaLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
</div>
<div className="icg-controls">
<label>
Naga count
@@ -3217,6 +3220,20 @@ function LibraryEditor({
</label>
<strong>{icgNagaCount}</strong>
</div>
<div className="icg-controls">
<label>
Mean length
<input max={64} min={1} step={1} type="range" value={icgNagaMeanLength} onChange={(event) => setIcgNagaMeanLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
<strong>{icgNagaMeanLength}</strong>
</div>
<div className="icg-controls">
<label>
Length std. dev.
<input max={32} min={0} step={1} type="range" value={icgNagaLengthStdDev} onChange={(event) => setIcgNagaLengthStdDev(Math.max(0, Number(event.target.value) || 0))} />
</label>
<strong>{icgNagaLengthStdDev}</strong>
</div>
<div className="icg-controls">
<label>
Same type probability
@@ -3235,7 +3252,7 @@ function LibraryEditor({
</div>
)}
<label>
Seed
IC seed
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
</label>
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
+52 -44
View File
@@ -30,7 +30,6 @@ interface SceneBundle {
bounds: THREE.LineSegments
camera: THREE.PerspectiveCamera
controls: OrbitControls
glowMesh: THREE.InstancedMesh | null
grid: THREE.GridHelper
mesh: THREE.InstancedMesh | null
mountNode: HTMLDivElement
@@ -60,23 +59,59 @@ function disposeMesh(scene: THREE.Scene, mesh: THREE.InstancedMesh | null) {
}
function createVoxelMaterial(primaryColor: THREE.Color) {
return new THREE.MeshStandardMaterial({
color: primaryColor,
emissive: primaryColor.clone().multiplyScalar(0.58),
emissiveIntensity: 0.48,
metalness: 0.04,
roughness: 0.68,
vertexColors: true
})
}
return new THREE.ShaderMaterial({
defines: {
USE_INSTANCING_COLOR: ''
},
uniforms: {
ambientStrength: { value: 0.56 },
diffuseStrength: { value: 0.58 },
glowStrength: { value: 0.34 },
lightDirection: { value: new THREE.Vector3(0.45, 0.82, 0.36).normalize() },
primaryColor: { value: primaryColor }
},
vertexShader: `
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
function createVoxelGlowMaterial() {
return new THREE.MeshBasicMaterial({
blending: THREE.AdditiveBlending,
depthWrite: false,
opacity: 0.26,
transparent: true,
vertexColors: true
void main() {
vColor = instanceColor;
vNormal = normalize(mat3(modelMatrix * instanceMatrix) * normal);
vec4 worldPosition = modelMatrix * instanceMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`,
fragmentShader: `
uniform float ambientStrength;
uniform float diffuseStrength;
uniform float glowStrength;
uniform vec3 lightDirection;
uniform vec3 primaryColor;
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
float diffuse = max(dot(normal, normalize(lightDirection)), 0.0);
float reverseFill = max(dot(normal, normalize(vec3(-0.45, 0.42, -0.68))), 0.0);
float rim = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.15);
float faceLift = pow(max(abs(normal.y), 0.0), 0.7) * 0.08;
vec3 color = max(vColor, primaryColor * 0.12);
vec3 lit = color * (ambientStrength + diffuse * diffuseStrength + reverseFill * 0.16 + faceLift);
vec3 glow = color * (0.18 + rim * glowStrength);
vec3 finalColor = lit + glow;
gl_FragColor = vec4(finalColor, 1.0);
}
`,
toneMapped: true
})
}
@@ -123,18 +158,6 @@ function createVoxelMesh(size: number, options: VoxelRendererOptions) {
return mesh
}
function createVoxelGlowMesh(size: number, options: VoxelRendererOptions) {
const spacing = WORLD_SIZE / size
const edge = spacing * (1 - clamp(options.cellGap, 0.02, 0.45)) * 1.18
const geometry = new THREE.BoxGeometry(edge, edge, edge)
const material = createVoxelGlowMaterial()
const mesh = new THREE.InstancedMesh(geometry, material, Math.max(1, size ** 3))
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
mesh.castShadow = false
mesh.receiveShadow = false
return mesh
}
const CATEGORICAL_STATE_COLORS = [
'#70f45f',
'#f0c94a',
@@ -218,17 +241,12 @@ function colorForCell(
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.mesh = createVoxelMesh(size, options)
bundle.glowMesh = createVoxelGlowMesh(size, options)
bundle.scene.add(bundle.glowMesh)
bundle.scene.add(bundle.mesh)
const mesh = bundle.mesh
const glowMesh = bundle.glowMesh
const dummy = new THREE.Object3D()
const color = new THREE.Color()
const glowColor = new THREE.Color()
const spacing = WORLD_SIZE / size
const origin = -WORLD_SIZE / 2 + spacing / 2
const statePalette = createStatePalette(cells, options)
@@ -244,22 +262,14 @@ function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number,
colorForCell(bundle, cell, size, ruleId, statePalette, color)
mesh.setMatrixAt(instanceIndex, dummy.matrix)
mesh.setColorAt(instanceIndex, color)
glowColor.copy(color).lerp(new THREE.Color('#ffffff'), 0.16).multiplyScalar(1.18)
glowMesh.setMatrixAt(instanceIndex, dummy.matrix)
glowMesh.setColorAt(instanceIndex, glowColor)
instanceIndex += 1
}
mesh.count = instanceIndex
glowMesh.count = instanceIndex
mesh.instanceMatrix.needsUpdate = true
glowMesh.instanceMatrix.needsUpdate = true
if (mesh.instanceColor) {
mesh.instanceColor.needsUpdate = true
}
if (glowMesh.instanceColor) {
glowMesh.instanceColor.needsUpdate = true
}
}
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
@@ -309,7 +319,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
bounds,
camera,
controls,
glowMesh: null,
grid,
mesh: null,
mountNode: targetNode,
@@ -347,7 +356,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
window.cancelAnimationFrame(bundle.animationFrame)
bundle.resizeObserver.disconnect()
disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.bounds.geometry.dispose()
disposeMaterial(bundle.bounds.material)
bundle.grid.geometry.dispose()