Working but slow
This commit is contained in:
@@ -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'
|
import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js'
|
||||||
|
|
||||||
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
|
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
|
||||||
@@ -389,7 +389,7 @@ export function stepNaga3d(cells: Cells, settings: SceneParams) {
|
|||||||
? universeFromCoordinates(coordinates)
|
? universeFromCoordinates(coordinates)
|
||||||
: seedUniverseFromCells(cells)
|
: seedUniverseFromCells(cells)
|
||||||
const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0
|
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 = {
|
const topology: NagaTopology = {
|
||||||
boundary: resolveBoundaryCondition(simulation.grid),
|
boundary: resolveBoundaryCondition(simulation.grid),
|
||||||
size
|
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 type { Cells, SceneParams } from '../types.js'
|
||||||
import { emptyCellsLike } from './cellGrid.js'
|
import { emptyCellsLike } from './cellGrid.js'
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export function stepWildfire2d(cells: Cells, settings: SceneParams) {
|
|||||||
const width = cells[0]?.length ?? 0
|
const width = cells[0]?.length ?? 0
|
||||||
const boundary = resolveBoundaryCondition(settings.simulation?.grid)
|
const boundary = resolveBoundaryCondition(settings.simulation?.grid)
|
||||||
const spreadProbability = parseProbability(settings.simulation?.spreadProbability, 1)
|
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 y = 0; y < height; y += 1) {
|
||||||
for (let x = 0; x < width; x += 1) {
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ registerCaEngineRuntime(
|
|||||||
id: 'naga-3d',
|
id: 'naga-3d',
|
||||||
label: 'Naga 3D',
|
label: 'Naga 3D',
|
||||||
supportedClasses: [{ dimensions: 3, states: 7 }],
|
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
|
step: stepNaga3d
|
||||||
},
|
},
|
||||||
{ replace: true }
|
{ 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))
|
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 {
|
function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
|
||||||
const density = clampDensity(params.density)
|
const density = clampDensity(params.density)
|
||||||
const seed = typeof params.seed === 'string' ? params.seed : 'studio-seed'
|
const icSeed = resolveIcSeed(params, 'studio-seed')
|
||||||
const random = seededRandom(seed)
|
const random = seededRandom(icSeed)
|
||||||
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
|
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
|
||||||
const cells: number[][] = []
|
const cells: number[][] = []
|
||||||
|
|
||||||
@@ -57,7 +74,7 @@ function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput)
|
|||||||
generator: {
|
generator: {
|
||||||
kind: 'random-soup',
|
kind: 'random-soup',
|
||||||
density,
|
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 {
|
function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
|
||||||
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
|
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 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 sameTypeProbability = clampProbability(params.sameTypeProbability, 0.8)
|
||||||
const seed = typeof params.seed === 'string' ? params.seed : 'naga-markov'
|
const icSeed = resolveIcSeed(params, 'naga-markov')
|
||||||
const random = seededRandom(seed)
|
const random = seededRandom(icSeed)
|
||||||
const occupied = new Set<string>()
|
const occupied = new Set<string>()
|
||||||
const cells: number[][] = []
|
const cells: number[][] = []
|
||||||
const maxAttempts = count * 300
|
const maxAttempts = count * 300
|
||||||
@@ -100,6 +119,7 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
|
|||||||
let y = Math.floor(random() * height)
|
let y = Math.floor(random() * height)
|
||||||
let z = Math.floor(random() * depth)
|
let z = Math.floor(random() * depth)
|
||||||
let arrow = randomNagaArrow(random)
|
let arrow = randomNagaArrow(random)
|
||||||
|
const length = sampleNagaLength(meanLength, lengthStdDev, maxLength, random)
|
||||||
const proposed: number[][] = []
|
const proposed: number[][] = []
|
||||||
const proposedKeys = new Set<string>()
|
const proposedKeys = new Set<string>()
|
||||||
let valid = true
|
let valid = true
|
||||||
@@ -164,9 +184,10 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
|
|||||||
generator: {
|
generator: {
|
||||||
kind: 'naga-markov',
|
kind: 'naga-markov',
|
||||||
count,
|
count,
|
||||||
length,
|
meanLength,
|
||||||
|
lengthStdDev,
|
||||||
sameTypeProbability,
|
sameTypeProbability,
|
||||||
seed,
|
ic_seed: icSeed,
|
||||||
created
|
created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
export function isBoundaryCondition(value: unknown): value is BoundaryCondition {
|
||||||
return value === 'wrap' || value === 'mirror' || value === 'fixed'
|
return value === 'wrap' || value === 'mirror' || value === 'fixed'
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-22
@@ -72,7 +72,7 @@ interface IcgConfig {
|
|||||||
dimensions: number
|
dimensions: number
|
||||||
states: number
|
states: number
|
||||||
density: number
|
density: number
|
||||||
seed: string
|
icSeed: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EngineConfig {
|
interface EngineConfig {
|
||||||
@@ -324,7 +324,7 @@ function defaultIcgConfig(): IcgConfig {
|
|||||||
dimensions: 2,
|
dimensions: 2,
|
||||||
states: 2,
|
states: 2,
|
||||||
density: 0.28,
|
density: 0.28,
|
||||||
seed: 'studio-seed'
|
icSeed: 'studio-seed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,16 +353,16 @@ function icgParamsSchema(config: IcgConfig): JsonObject {
|
|||||||
max: 1,
|
max: 1,
|
||||||
step: 0.01
|
step: 0.01
|
||||||
},
|
},
|
||||||
seed: {
|
ic_seed: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
label: 'Seed',
|
label: 'IC seed',
|
||||||
default: config.seed
|
default: config.icSeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function icgDefaultParams(config: IcgConfig): JsonObject {
|
function icgDefaultParams(config: IcgConfig): JsonObject {
|
||||||
return { density: config.density, seed: config.seed }
|
return { density: config.density, ic_seed: config.icSeed }
|
||||||
}
|
}
|
||||||
|
|
||||||
function ruleLabel(ruleId: string) {
|
function ruleLabel(ruleId: string) {
|
||||||
@@ -1644,8 +1644,8 @@ function IcgConfigFields({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Seed
|
IC seed
|
||||||
<input value={config.seed} onChange={(event) => onChange((current) => ({ ...current, seed: event.target.value }))} />
|
<input value={config.icSeed} onChange={(event) => onChange((current) => ({ ...current, icSeed: event.target.value }))} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1817,7 +1817,11 @@ function configFromIcg(generator: InitialConditionGenerator): IcgConfig {
|
|||||||
dimensions: supportedClass?.dimensions ?? 2,
|
dimensions: supportedClass?.dimensions ?? 2,
|
||||||
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
|
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
|
||||||
density: typeof generator.default_params.density === 'number' ? generator.default_params.density : 0.28,
|
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 [selectedEngineId, setSelectedEngineId] = React.useState('')
|
||||||
const [icgDensity, setIcgDensity] = React.useState(0.28)
|
const [icgDensity, setIcgDensity] = React.useState(0.28)
|
||||||
const [icgNagaCount, setIcgNagaCount] = React.useState(3)
|
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 [icgNagaSameTypeProbability, setIcgNagaSameTypeProbability] = React.useState(0.8)
|
||||||
const [icgSeed, setIcgSeed] = React.useState('studio-seed')
|
const [icgSeed, setIcgSeed] = React.useState('studio-seed')
|
||||||
const [nodeName, setNodeName] = React.useState('Untitled preset')
|
const [nodeName, setNodeName] = React.useState('Untitled preset')
|
||||||
@@ -2664,24 +2669,28 @@ function LibraryEditor({
|
|||||||
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
|
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
|
||||||
if (typeof generator?.default_params.density === 'number') setIcgDensity(generator.default_params.density)
|
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.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') {
|
if (typeof generator?.default_params.sameTypeProbability === 'number') {
|
||||||
setIcgNagaSameTypeProbability(generator.default_params.sameTypeProbability)
|
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) {
|
function selectedIcgParams(generator: InitialConditionGenerator) {
|
||||||
if (generator.generator_kind === 'naga-markov') {
|
if (generator.generator_kind === 'naga-markov') {
|
||||||
return {
|
return {
|
||||||
count: icgNagaCount,
|
count: icgNagaCount,
|
||||||
length: icgNagaLength,
|
meanLength: icgNagaMeanLength,
|
||||||
|
lengthStdDev: icgNagaLengthStdDev,
|
||||||
sameTypeProbability: icgNagaSameTypeProbability,
|
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() {
|
function applySelectedIcg() {
|
||||||
@@ -3204,12 +3213,6 @@ function LibraryEditor({
|
|||||||
</label>
|
</label>
|
||||||
{selectedIcg?.generator_kind === 'naga-markov' ? (
|
{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">
|
<div className="icg-controls">
|
||||||
<label>
|
<label>
|
||||||
Naga count
|
Naga count
|
||||||
@@ -3217,6 +3220,20 @@ function LibraryEditor({
|
|||||||
</label>
|
</label>
|
||||||
<strong>{icgNagaCount}</strong>
|
<strong>{icgNagaCount}</strong>
|
||||||
</div>
|
</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">
|
<div className="icg-controls">
|
||||||
<label>
|
<label>
|
||||||
Same type probability
|
Same type probability
|
||||||
@@ -3235,7 +3252,7 @@ function LibraryEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<label>
|
<label>
|
||||||
Seed
|
IC seed
|
||||||
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
|
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
|
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ interface SceneBundle {
|
|||||||
bounds: THREE.LineSegments
|
bounds: THREE.LineSegments
|
||||||
camera: THREE.PerspectiveCamera
|
camera: THREE.PerspectiveCamera
|
||||||
controls: OrbitControls
|
controls: OrbitControls
|
||||||
glowMesh: THREE.InstancedMesh | null
|
|
||||||
grid: THREE.GridHelper
|
grid: THREE.GridHelper
|
||||||
mesh: THREE.InstancedMesh | null
|
mesh: THREE.InstancedMesh | null
|
||||||
mountNode: HTMLDivElement
|
mountNode: HTMLDivElement
|
||||||
@@ -60,23 +59,59 @@ function disposeMesh(scene: THREE.Scene, mesh: THREE.InstancedMesh | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createVoxelMaterial(primaryColor: THREE.Color) {
|
function createVoxelMaterial(primaryColor: THREE.Color) {
|
||||||
return new THREE.MeshStandardMaterial({
|
return new THREE.ShaderMaterial({
|
||||||
color: primaryColor,
|
defines: {
|
||||||
emissive: primaryColor.clone().multiplyScalar(0.58),
|
USE_INSTANCING_COLOR: ''
|
||||||
emissiveIntensity: 0.48,
|
},
|
||||||
metalness: 0.04,
|
uniforms: {
|
||||||
roughness: 0.68,
|
ambientStrength: { value: 0.56 },
|
||||||
vertexColors: true
|
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() {
|
void main() {
|
||||||
return new THREE.MeshBasicMaterial({
|
vColor = instanceColor;
|
||||||
blending: THREE.AdditiveBlending,
|
vNormal = normalize(mat3(modelMatrix * instanceMatrix) * normal);
|
||||||
depthWrite: false,
|
|
||||||
opacity: 0.26,
|
vec4 worldPosition = modelMatrix * instanceMatrix * vec4(position, 1.0);
|
||||||
transparent: true,
|
vWorldPosition = worldPosition.xyz;
|
||||||
vertexColors: true
|
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
|
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 = [
|
const CATEGORICAL_STATE_COLORS = [
|
||||||
'#70f45f',
|
'#70f45f',
|
||||||
'#f0c94a',
|
'#f0c94a',
|
||||||
@@ -218,17 +241,12 @@ function colorForCell(
|
|||||||
|
|
||||||
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
|
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
|
||||||
disposeMesh(bundle.scene, bundle.mesh)
|
disposeMesh(bundle.scene, bundle.mesh)
|
||||||
disposeMesh(bundle.scene, bundle.glowMesh)
|
|
||||||
bundle.mesh = createVoxelMesh(size, options)
|
bundle.mesh = createVoxelMesh(size, options)
|
||||||
bundle.glowMesh = createVoxelGlowMesh(size, options)
|
|
||||||
bundle.scene.add(bundle.glowMesh)
|
|
||||||
bundle.scene.add(bundle.mesh)
|
bundle.scene.add(bundle.mesh)
|
||||||
|
|
||||||
const mesh = bundle.mesh
|
const mesh = bundle.mesh
|
||||||
const glowMesh = bundle.glowMesh
|
|
||||||
const dummy = new THREE.Object3D()
|
const dummy = new THREE.Object3D()
|
||||||
const color = new THREE.Color()
|
const color = new THREE.Color()
|
||||||
const glowColor = new THREE.Color()
|
|
||||||
const spacing = WORLD_SIZE / size
|
const spacing = WORLD_SIZE / size
|
||||||
const origin = -WORLD_SIZE / 2 + spacing / 2
|
const origin = -WORLD_SIZE / 2 + spacing / 2
|
||||||
const statePalette = createStatePalette(cells, options)
|
const statePalette = createStatePalette(cells, options)
|
||||||
@@ -244,22 +262,14 @@ function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number,
|
|||||||
colorForCell(bundle, cell, size, ruleId, statePalette, color)
|
colorForCell(bundle, cell, size, ruleId, statePalette, color)
|
||||||
mesh.setMatrixAt(instanceIndex, dummy.matrix)
|
mesh.setMatrixAt(instanceIndex, dummy.matrix)
|
||||||
mesh.setColorAt(instanceIndex, color)
|
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
|
instanceIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mesh.count = instanceIndex
|
mesh.count = instanceIndex
|
||||||
glowMesh.count = instanceIndex
|
|
||||||
mesh.instanceMatrix.needsUpdate = true
|
mesh.instanceMatrix.needsUpdate = true
|
||||||
glowMesh.instanceMatrix.needsUpdate = true
|
|
||||||
if (mesh.instanceColor) {
|
if (mesh.instanceColor) {
|
||||||
mesh.instanceColor.needsUpdate = true
|
mesh.instanceColor.needsUpdate = true
|
||||||
}
|
}
|
||||||
if (glowMesh.instanceColor) {
|
|
||||||
glowMesh.instanceColor.needsUpdate = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
|
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
|
||||||
@@ -309,7 +319,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
|
|||||||
bounds,
|
bounds,
|
||||||
camera,
|
camera,
|
||||||
controls,
|
controls,
|
||||||
glowMesh: null,
|
|
||||||
grid,
|
grid,
|
||||||
mesh: null,
|
mesh: null,
|
||||||
mountNode: targetNode,
|
mountNode: targetNode,
|
||||||
@@ -347,7 +356,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
|
|||||||
window.cancelAnimationFrame(bundle.animationFrame)
|
window.cancelAnimationFrame(bundle.animationFrame)
|
||||||
bundle.resizeObserver.disconnect()
|
bundle.resizeObserver.disconnect()
|
||||||
disposeMesh(bundle.scene, bundle.mesh)
|
disposeMesh(bundle.scene, bundle.mesh)
|
||||||
disposeMesh(bundle.scene, bundle.glowMesh)
|
|
||||||
bundle.bounds.geometry.dispose()
|
bundle.bounds.geometry.dispose()
|
||||||
disposeMaterial(bundle.bounds.material)
|
disposeMaterial(bundle.bounds.material)
|
||||||
bundle.grid.geometry.dispose()
|
bundle.grid.geometry.dispose()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>CA Studio Admin</title>
|
<title>CA Studio Admin</title>
|
||||||
<script type="module" crossorigin src="/admin/assets/index-COp4VUl7.js"></script>
|
<script type="module" crossorigin src="/admin/assets/index-DowkWVYj.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-D7EhCe8j.css">
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-D7EhCe8j.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
UPDATE ca_engines
|
||||||
|
SET
|
||||||
|
params_schema =
|
||||||
|
(params_schema - 'seed') ||
|
||||||
|
jsonb_build_object(
|
||||||
|
'rng_seed',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'string',
|
||||||
|
'label', 'RNG seed',
|
||||||
|
'default', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'),
|
||||||
|
'group', 'Randomness'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
default_params =
|
||||||
|
(default_params - 'seed') ||
|
||||||
|
jsonb_build_object('rng_seed', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'))
|
||||||
|
WHERE slug = 'naga-3d';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
UPDATE ca_initial_condition_generators
|
||||||
|
SET
|
||||||
|
params_schema =
|
||||||
|
(params_schema - 'seed' - 'length') ||
|
||||||
|
jsonb_build_object(
|
||||||
|
'ic_seed',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'string',
|
||||||
|
'label', 'IC seed',
|
||||||
|
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov')
|
||||||
|
),
|
||||||
|
'meanLength',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'range',
|
||||||
|
'label', 'Mean length',
|
||||||
|
'default', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
|
||||||
|
'min', 1,
|
||||||
|
'max', 64,
|
||||||
|
'step', 1
|
||||||
|
),
|
||||||
|
'lengthStdDev',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'range',
|
||||||
|
'label', 'Length std. dev.',
|
||||||
|
'default', COALESCE(default_params->'lengthStdDev', '0'::jsonb),
|
||||||
|
'min', 0,
|
||||||
|
'max', 32,
|
||||||
|
'step', 1
|
||||||
|
)
|
||||||
|
),
|
||||||
|
default_params =
|
||||||
|
(default_params - 'seed' - 'length') ||
|
||||||
|
jsonb_build_object(
|
||||||
|
'ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov'),
|
||||||
|
'meanLength', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
|
||||||
|
'lengthStdDev', COALESCE(default_params->'lengthStdDev', '0'::jsonb)
|
||||||
|
)
|
||||||
|
WHERE slug = 'naga-markov-chains';
|
||||||
|
|
||||||
|
UPDATE ca_initial_condition_generators
|
||||||
|
SET
|
||||||
|
params_schema =
|
||||||
|
(params_schema - 'seed') ||
|
||||||
|
jsonb_build_object(
|
||||||
|
'ic_seed',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'string',
|
||||||
|
'label', 'IC seed',
|
||||||
|
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
default_params =
|
||||||
|
(default_params - 'seed') ||
|
||||||
|
jsonb_build_object('ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed'))
|
||||||
|
WHERE slug <> 'naga-markov-chains';
|
||||||
@@ -188,6 +188,35 @@ describe('CA engine runtimes', () => {
|
|||||||
expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]]))
|
expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]]))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses simulation rng_seed to choose deterministic Naga heads', () => {
|
||||||
|
const current = cells(6, 6)
|
||||||
|
const baseParams = {
|
||||||
|
caClass: { dimensions: 3, states: 7 },
|
||||||
|
renderer: { id: 'voxel-3d' },
|
||||||
|
simulation: {
|
||||||
|
engineId: 'naga-3d',
|
||||||
|
ruleId: 'naga-3d',
|
||||||
|
grid: { size: [6, 6, 6], boundary: 'wrap' },
|
||||||
|
initialCondition: {
|
||||||
|
type: 'cells',
|
||||||
|
cells: [[1, 2, 2, 1], [4, 2, 2, 1]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies SceneParams
|
||||||
|
|
||||||
|
const leftSeed = stepCaSimulation(current, settings({
|
||||||
|
...baseParams,
|
||||||
|
simulation: { ...baseParams.simulation, rng_seed: 'bar' }
|
||||||
|
}))
|
||||||
|
const rightSeed = stepCaSimulation(current, settings({
|
||||||
|
...baseParams,
|
||||||
|
simulation: { ...baseParams.simulation, rng_seed: 'alpha' }
|
||||||
|
}))
|
||||||
|
|
||||||
|
expect(leftSeed.settings.simulation?.initialCondition?.cells).toEqual([[4, 2, 2, 1], [0, 2, 2, 1]])
|
||||||
|
expect(rightSeed.settings.simulation?.initialCondition?.cells).toEqual([[1, 2, 2, 1], [3, 2, 2, 1]])
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => {
|
it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => {
|
||||||
const current = cells(5, 5)
|
const current = cells(5, 5)
|
||||||
const params = settings({
|
const params = settings({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('CA initial condition runtimes', () => {
|
|||||||
const input = {
|
const input = {
|
||||||
caClass: { dimensions: 2, states: 2 },
|
caClass: { dimensions: 2, states: 2 },
|
||||||
settings: { simulation: { grid: { size: [5, 5, 1] } } },
|
settings: { simulation: { grid: { size: [5, 5, 1] } } },
|
||||||
params: { density: 0.35, seed: 'repeatable' }
|
params: { density: 0.35, ic_seed: 'repeatable' }
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(generateInitialCondition('random-soup', input)).toEqual(generateInitialCondition('random-soup', input))
|
expect(generateInitialCondition('random-soup', input)).toEqual(generateInitialCondition('random-soup', input))
|
||||||
@@ -29,11 +29,11 @@ describe('CA initial condition runtimes', () => {
|
|||||||
const generated = generateInitialCondition('random-soup', {
|
const generated = generateInitialCondition('random-soup', {
|
||||||
caClass: { dimensions: 2, states: 2 },
|
caClass: { dimensions: 2, states: 2 },
|
||||||
settings: { simulation: { grid: { size: [3, 3, 1] } } },
|
settings: { simulation: { grid: { size: [3, 3, 1] } } },
|
||||||
params: { density: 2, seed: 'full' }
|
params: { density: 2, ic_seed: 'full' }
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(generated.cells).toHaveLength(9)
|
expect(generated.cells).toHaveLength(9)
|
||||||
expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, seed: 'full' })
|
expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, ic_seed: 'full' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lets custom initial condition runtimes plug into the same contract', () => {
|
it('lets custom initial condition runtimes plug into the same contract', () => {
|
||||||
@@ -54,7 +54,7 @@ describe('CA initial condition runtimes', () => {
|
|||||||
const generated = generateInitialCondition('naga-markov', {
|
const generated = generateInitialCondition('naga-markov', {
|
||||||
caClass: { dimensions: 3, states: 7 },
|
caClass: { dimensions: 3, states: 7 },
|
||||||
settings: { simulation: { grid: { size: [12, 12, 12] } } },
|
settings: { simulation: { grid: { size: [12, 12, 12] } } },
|
||||||
params: { count: 2, length: 5, sameTypeProbability: 1, seed: 'straight-nagas' }
|
params: { count: 2, meanLength: 5, lengthStdDev: 0, sameTypeProbability: 1, ic_seed: 'straight-nagas' }
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(generated.cells).toHaveLength(10)
|
expect(generated.cells).toHaveLength(10)
|
||||||
@@ -62,9 +62,10 @@ describe('CA initial condition runtimes', () => {
|
|||||||
expect(generated.generator).toMatchObject({
|
expect(generated.generator).toMatchObject({
|
||||||
kind: 'naga-markov',
|
kind: 'naga-markov',
|
||||||
count: 2,
|
count: 2,
|
||||||
length: 5,
|
meanLength: 5,
|
||||||
|
lengthStdDev: 0,
|
||||||
sameTypeProbability: 1,
|
sameTypeProbability: 1,
|
||||||
seed: 'straight-nagas',
|
ic_seed: 'straight-nagas',
|
||||||
created: 2
|
created: 2
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -93,6 +94,31 @@ describe('CA initial condition runtimes', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('samples Naga Markov chain lengths from deterministic mean and deviation settings', () => {
|
||||||
|
const generated = generateInitialCondition('naga-markov', {
|
||||||
|
caClass: { dimensions: 3, states: 7 },
|
||||||
|
settings: { simulation: { grid: { size: [30, 30, 30] } } },
|
||||||
|
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const repeated = generateInitialCondition('naga-markov', {
|
||||||
|
caClass: { dimensions: 3, states: 7 },
|
||||||
|
settings: { simulation: { grid: { size: [30, 30, 30] } } },
|
||||||
|
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(generated).toEqual(repeated)
|
||||||
|
expect(generated.generator).toMatchObject({
|
||||||
|
kind: 'naga-markov',
|
||||||
|
count: 8,
|
||||||
|
meanLength: 7,
|
||||||
|
lengthStdDev: 3,
|
||||||
|
ic_seed: 'variable-nagas',
|
||||||
|
created: 8
|
||||||
|
})
|
||||||
|
expect(generated.cells.length).not.toBe(56)
|
||||||
|
})
|
||||||
|
|
||||||
it('rejects duplicate runtime ids and missing implementations', () => {
|
it('rejects duplicate runtime ids and missing implementations', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
registerInitialConditionRuntime({
|
registerInitialConditionRuntime({
|
||||||
|
|||||||
@@ -56,11 +56,17 @@ describe('CA Studio API', () => {
|
|||||||
default_params: {
|
default_params: {
|
||||||
ruleId: 'naga-3d',
|
ruleId: 'naga-3d',
|
||||||
rendererId: 'voxel-3d',
|
rendererId: 'voxel-3d',
|
||||||
seed: 'naga',
|
rng_seed: 'naga',
|
||||||
edgeWrap: true
|
edgeWrap: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }])
|
expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }])
|
||||||
|
expect(naga.params_schema.rng_seed).toMatchObject({
|
||||||
|
type: 'string',
|
||||||
|
label: 'RNG seed',
|
||||||
|
default: 'naga',
|
||||||
|
group: 'Randomness'
|
||||||
|
})
|
||||||
expect(naga.params_schema.edgeWrap).toMatchObject({
|
expect(naga.params_schema.edgeWrap).toMatchObject({
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Edge wrap',
|
label: 'Edge wrap',
|
||||||
@@ -84,15 +90,29 @@ describe('CA Studio API', () => {
|
|||||||
generator_kind: 'naga-markov',
|
generator_kind: 'naga-markov',
|
||||||
default_params: {
|
default_params: {
|
||||||
count: 3,
|
count: 3,
|
||||||
length: 5,
|
meanLength: 5,
|
||||||
|
lengthStdDev: 0,
|
||||||
sameTypeProbability: 0.8,
|
sameTypeProbability: 0.8,
|
||||||
seed: 'naga-markov'
|
ic_seed: 'naga-markov'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
expect(generator.params_schema.meanLength).toMatchObject({
|
||||||
|
type: 'range',
|
||||||
|
default: 5
|
||||||
|
})
|
||||||
|
expect(generator.params_schema.lengthStdDev).toMatchObject({
|
||||||
|
type: 'range',
|
||||||
|
default: 0
|
||||||
|
})
|
||||||
expect(generator.params_schema.sameTypeProbability).toMatchObject({
|
expect(generator.params_schema.sameTypeProbability).toMatchObject({
|
||||||
type: 'range',
|
type: 'range',
|
||||||
default: 0.8
|
default: 0.8
|
||||||
})
|
})
|
||||||
|
expect(generator.params_schema.ic_seed).toMatchObject({
|
||||||
|
type: 'string',
|
||||||
|
label: 'IC seed',
|
||||||
|
default: 'naga-markov'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user