More clean up
This commit is contained in:
parent
c94ffcc506
commit
f03da42b5e
43 changed files with 818 additions and 1076 deletions
|
|
@ -4,7 +4,7 @@ export const DISABLED_FLAG_VALUE = '0';
|
||||||
export const UNIT_INTERVAL_INPUT_MIN = '0';
|
export const UNIT_INTERVAL_INPUT_MIN = '0';
|
||||||
export const UNIT_INTERVAL_INPUT_MAX = '1';
|
export const UNIT_INTERVAL_INPUT_MAX = '1';
|
||||||
|
|
||||||
export const DEFAULT_AUDIO_VOLUME = 0.42;
|
export const DEFAULT_AUDIO_VOLUME = 0.5;
|
||||||
|
|
||||||
export const APP_STORAGE_KEYS = {
|
export const APP_STORAGE_KEYS = {
|
||||||
audioMuted: 'fleeting-garden:audio-muted',
|
audioMuted: 'fleeting-garden:audio-muted',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DEFAULT_AUDIO_VOLUME } from '../app-constants';
|
||||||
import type { PianoNoteRole } from './garden-audio-types';
|
import type { PianoNoteRole } from './garden-audio-types';
|
||||||
|
|
||||||
type GardenAudioChordQuality = 'major' | 'minor';
|
type GardenAudioChordQuality = 'major' | 'minor';
|
||||||
|
|
@ -7,81 +8,106 @@ export interface GardenAudioChord {
|
||||||
quality: GardenAudioChordQuality;
|
quality: GardenAudioChordQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GardenAudioVibeProfile {
|
export interface GardenAudioVibeSettings {
|
||||||
|
idleIntensity: number;
|
||||||
|
bpm: number;
|
||||||
|
rampUpIntensity: number;
|
||||||
|
rampUpTime: number;
|
||||||
|
noteLength: number;
|
||||||
|
notePitchOffset: number;
|
||||||
|
brightness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GardenAudioVibeProfile extends GardenAudioVibeSettings {
|
||||||
rootMidi: number;
|
rootMidi: number;
|
||||||
scale: Array<number>;
|
scale: Array<number>;
|
||||||
brightness: number;
|
|
||||||
delayTimeMultiplier: number;
|
|
||||||
progression: Array<GardenAudioChord>;
|
progression: Array<GardenAudioChord>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GardenAudioConfig {
|
export const createGardenAudioConfig = () => ({
|
||||||
masterVolume: number;
|
masterVolume: DEFAULT_AUDIO_VOLUME,
|
||||||
fadeInSeconds: number;
|
fadeInSeconds: 0.45,
|
||||||
updateRampSeconds: number;
|
updateRampSeconds: 0.08,
|
||||||
delay: {
|
delay: {
|
||||||
timeSeconds: number;
|
timeSeconds: 0.46,
|
||||||
feedback: number;
|
feedback: 0.12,
|
||||||
wetGain: number;
|
wetGain: 0.044,
|
||||||
erasingActivity: number;
|
erasingActivity: 0.12,
|
||||||
activityFeedbackWeight: number;
|
activityFeedbackWeight: 0.08,
|
||||||
feedbackMax: number;
|
feedbackMax: 0.32,
|
||||||
feedbackMin: number;
|
feedbackMin: 0.04,
|
||||||
outputActivityWeight: number;
|
outputActivityWeight: 0.5,
|
||||||
outputBase: number;
|
outputBase: 0.65,
|
||||||
outputActivityDuck: number;
|
outputActivityDuck: 0.28,
|
||||||
timeRampSeconds: number;
|
timeRampSeconds: 0.12,
|
||||||
};
|
},
|
||||||
piano: {
|
piano: {
|
||||||
maxVoices: number;
|
maxVoices: 24,
|
||||||
gain: number;
|
gain: 0.48,
|
||||||
sustainSeconds: number;
|
sustainSeconds: 0.42,
|
||||||
sustainLevel: number;
|
sustainLevel: 0.32,
|
||||||
releaseSeconds: number;
|
releaseSeconds: 0.24,
|
||||||
lowpassHz: number;
|
lowpassHz: 7600,
|
||||||
gainAttackSeconds: number;
|
gainAttackSeconds: 0.006,
|
||||||
lowpassMaxHz: number;
|
lowpassMaxHz: 12000,
|
||||||
lowpassMinHz: number;
|
lowpassMinHz: 1400,
|
||||||
sustainBase: number;
|
sustainBase: 0.45,
|
||||||
sustainVelocityRange: number;
|
sustainVelocityRange: 0.55,
|
||||||
};
|
},
|
||||||
rhythm: {
|
rhythm: {
|
||||||
bpm: number;
|
idleIntensity: 0.08,
|
||||||
stepsPerBeat: number;
|
bpm: 74,
|
||||||
stepsPerBar: number;
|
stepsPerBeat: 4,
|
||||||
sparseActivity: number;
|
stepsPerBar: 16,
|
||||||
};
|
sparseActivity: 0.055,
|
||||||
|
},
|
||||||
eraser: {
|
eraser: {
|
||||||
minIntervalSeconds: number;
|
minIntervalSeconds: 0.12,
|
||||||
noiseGain: number;
|
noiseGain: 0.028,
|
||||||
filterMinHz: number;
|
filterMinHz: 650,
|
||||||
filterMaxHz: number;
|
filterMaxHz: 3600,
|
||||||
durationSeconds: number;
|
durationSeconds: 0.08,
|
||||||
pan: number;
|
pan: 0,
|
||||||
pianoActivity: number;
|
pianoActivity: 0,
|
||||||
};
|
},
|
||||||
energy: {
|
energy: {
|
||||||
attackSeconds: number;
|
attackSeconds: 0.08,
|
||||||
decaySeconds: number;
|
decaySeconds: 0.9,
|
||||||
immediateActivityScale: number;
|
immediateActivityScale: 0.85,
|
||||||
releaseSeconds: number;
|
releaseSeconds: 1.15,
|
||||||
strokeDecaySeconds: number;
|
strokeDecaySeconds: 0.32,
|
||||||
};
|
},
|
||||||
graph: {
|
graph: {
|
||||||
pianoBusGains: Record<PianoNoteRole, number>;
|
pianoBusGains: {
|
||||||
pianoBusActivityDucking: Record<PianoNoteRole, number>;
|
pad: 0.86,
|
||||||
noiseBusGain: number;
|
support: 0.94,
|
||||||
};
|
texture: 0.88,
|
||||||
|
gesture: 1,
|
||||||
|
brush: 0.9,
|
||||||
|
stinger: 0.92,
|
||||||
|
} satisfies Record<PianoNoteRole, number>,
|
||||||
|
pianoBusActivityDucking: {
|
||||||
|
pad: 0.42,
|
||||||
|
support: 0.18,
|
||||||
|
texture: -0.06,
|
||||||
|
gesture: 0,
|
||||||
|
brush: -0.08,
|
||||||
|
stinger: 0,
|
||||||
|
} satisfies Record<PianoNoteRole, number>,
|
||||||
|
noiseBusGain: 0.72,
|
||||||
|
},
|
||||||
input: {
|
input: {
|
||||||
fullActivitySpeed: number;
|
fullActivitySpeed: 0.86,
|
||||||
activityNoiseFloorSpeed: number;
|
activityNoiseFloorSpeed: 0.025,
|
||||||
activityCurve: number;
|
activityCurve: 0.74,
|
||||||
activitySoftCeiling: number;
|
activitySoftCeiling: 0.96,
|
||||||
activityAttackSeconds: number;
|
activityAttackSeconds: 0.055,
|
||||||
activityReleaseSeconds: number;
|
activityReleaseSeconds: 0.2,
|
||||||
minAudibleDistance: number;
|
minAudibleDistance: 0.0025,
|
||||||
manicActivityThreshold: number;
|
manicActivityThreshold: 0.9,
|
||||||
manicReleaseThreshold: number;
|
manicReleaseThreshold: 0.76,
|
||||||
maniaSmoothingSeconds: number;
|
maniaSmoothingSeconds: 0.12,
|
||||||
};
|
},
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export type GardenAudioConfig = ReturnType<typeof createGardenAudioConfig>;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { approach, clamp01 } from '../utils/math';
|
import { approach, clamp01 } from '../utils/math';
|
||||||
import type { GardenAudioConfig } from './garden-audio-config';
|
import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
||||||
|
|
||||||
export class GardenAudioEnergy {
|
export class GardenAudioEnergy {
|
||||||
private isGestureActive = false;
|
private isGestureActive = false;
|
||||||
|
|
@ -19,13 +19,10 @@ export class GardenAudioEnergy {
|
||||||
this.targetEnergy = 0;
|
this.targetEnergy = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public recordStroke(strokeEnergy: number): void {
|
public recordStroke(strokeEnergy: number, profile: GardenAudioVibeProfile): void {
|
||||||
this.targetEnergy = Math.max(this.targetEnergy, strokeEnergy);
|
this.targetEnergy = Math.max(this.targetEnergy, strokeEnergy);
|
||||||
if (this.isGestureActive) {
|
if (this.isGestureActive) {
|
||||||
this.energy = Math.max(
|
this.energy = Math.max(this.energy, strokeEnergy * profile.rampUpIntensity);
|
||||||
this.energy,
|
|
||||||
strokeEnergy * this.config.energy.immediateActivityScale
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +31,7 @@ export class GardenAudioEnergy {
|
||||||
this.energy = 0;
|
this.energy = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(now: number): void {
|
public update(now: number, profile: GardenAudioVibeProfile): void {
|
||||||
if (this.lastEnergyUpdateAt <= 0) {
|
if (this.lastEnergyUpdateAt <= 0) {
|
||||||
this.lastEnergyUpdateAt = now;
|
this.lastEnergyUpdateAt = now;
|
||||||
return;
|
return;
|
||||||
|
|
@ -51,7 +48,7 @@ export class GardenAudioEnergy {
|
||||||
if (!this.isGestureActive) {
|
if (!this.isGestureActive) {
|
||||||
timeConstant = this.config.energy.releaseSeconds;
|
timeConstant = this.config.energy.releaseSeconds;
|
||||||
} else if (target > this.energy) {
|
} else if (target > this.energy) {
|
||||||
timeConstant = this.config.energy.attackSeconds;
|
timeConstant = profile.rampUpTime;
|
||||||
}
|
}
|
||||||
this.energy = approach(this.energy, target, elapsedSeconds, timeConstant);
|
this.energy = approach(this.energy, target, elapsedSeconds, timeConstant);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import { clamp } from '../utils/math';
|
import { clamp } from '../utils/math';
|
||||||
import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config';
|
import type { GardenAudioConfig } from './garden-audio-config';
|
||||||
import type { PianoNoteRole } from './garden-audio-types';
|
import type { PianoNoteRole } from './garden-audio-types';
|
||||||
|
|
||||||
type NavigatorWithAudioSession = Navigator & {
|
type NavigatorWithAudioSession = Navigator & {
|
||||||
audioSession?: { type: 'auto' | 'playback' | 'ambient' | 'transient' | 'transient-solo' | 'play-and-record' };
|
audioSession?: {
|
||||||
|
type:
|
||||||
|
| 'auto'
|
||||||
|
| 'playback'
|
||||||
|
| 'ambient'
|
||||||
|
| 'transient'
|
||||||
|
| 'transient-solo'
|
||||||
|
| 'play-and-record';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const outputHighPassFrequencyHz = 45;
|
const outputHighPassFrequencyHz = 45;
|
||||||
|
|
@ -113,19 +121,19 @@ export class GardenAudioGraph {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyDelayProfile(profile: GardenAudioVibeProfile): void {
|
public applyDelayProfile(): void {
|
||||||
if (!this.context || !this.delayNode) {
|
if (!this.context || !this.delayNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delayNode.delayTime.setTargetAtTime(
|
this.delayNode.delayTime.setTargetAtTime(
|
||||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
this.config.delay.timeSeconds,
|
||||||
this.context.currentTime,
|
this.context.currentTime,
|
||||||
this.config.delay.timeRampSeconds
|
this.config.delay.timeRampSeconds
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateDelay(profile: GardenAudioVibeProfile, activity: number): void {
|
public updateDelay(activity: number): void {
|
||||||
if (!this.context || !this.delayNode || !this.delayFeedback || !this.delayOutput) {
|
if (!this.context || !this.delayNode || !this.delayFeedback || !this.delayOutput) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +141,7 @@ export class GardenAudioGraph {
|
||||||
const now = this.context.currentTime;
|
const now = this.context.currentTime;
|
||||||
const normalizedActivity = clamp(activity, 0, 1);
|
const normalizedActivity = clamp(activity, 0, 1);
|
||||||
this.delayNode.delayTime.setTargetAtTime(
|
this.delayNode.delayTime.setTargetAtTime(
|
||||||
this.config.delay.timeSeconds * profile.delayTimeMultiplier,
|
this.config.delay.timeSeconds,
|
||||||
now,
|
now,
|
||||||
this.config.delay.timeRampSeconds
|
this.config.delay.timeRampSeconds
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,34 @@
|
||||||
import type { VibePreset } from '../vibes';
|
import type { VibePreset } from '../vibes';
|
||||||
import type { GardenAudioVibeProfile } from './garden-audio-config';
|
import type { GardenAudioChord, GardenAudioVibeProfile } from './garden-audio-config';
|
||||||
|
|
||||||
export const PITCH_SEMITONES_PER_OCTAVE = 12;
|
export const PITCH_SEMITONES_PER_OCTAVE = 12;
|
||||||
|
|
||||||
export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => vibe.audio;
|
const DEFAULT_PROGRESSION: ReadonlyArray<GardenAudioChord> = [
|
||||||
|
{ rootOffset: 0, quality: 'major' },
|
||||||
|
{ rootOffset: 9, quality: 'minor' },
|
||||||
|
{ rootOffset: 5, quality: 'major' },
|
||||||
|
{ rootOffset: 7, quality: 'major' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_ROOT_MIDI = 57;
|
||||||
|
const DEFAULT_SCALE: ReadonlyArray<number> = [0, 2, 4, 7, 9];
|
||||||
|
|
||||||
|
const profileCache = new WeakMap<VibePreset, GardenAudioVibeProfile>();
|
||||||
|
|
||||||
|
export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => {
|
||||||
|
let profile = profileCache.get(vibe);
|
||||||
|
if (!profile) {
|
||||||
|
profile = {
|
||||||
|
...vibe.audio,
|
||||||
|
rootMidi: DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset,
|
||||||
|
scale: DEFAULT_SCALE as Array<number>,
|
||||||
|
progression: DEFAULT_PROGRESSION as Array<GardenAudioChord>,
|
||||||
|
};
|
||||||
|
profileCache.set(vibe, profile);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(profile, vibe.audio);
|
||||||
|
profile.rootMidi = DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset;
|
||||||
|
return profile;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export interface PianoNote {
|
||||||
role?: PianoNoteRole;
|
role?: PianoNoteRole;
|
||||||
delaySend?: number;
|
delaySend?: number;
|
||||||
lowpassHz?: number;
|
lowpassHz?: number;
|
||||||
|
sustainSeconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PianoNoteRole =
|
export type PianoNoteRole =
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export class GardenAudio {
|
||||||
|
|
||||||
this.lifecycle = 'started';
|
this.lifecycle = 'started';
|
||||||
this.applyVibe(vibe);
|
this.applyVibe(vibe);
|
||||||
this.pianoEngine.prime(context.currentTime);
|
this.pianoEngine.prime(context.currentTime, getVibeProfile(vibe));
|
||||||
this.graph.setMasterGain(this.masterVolume, startupRampSeconds);
|
this.graph.setMasterGain(this.masterVolume, startupRampSeconds);
|
||||||
|
|
||||||
const pianoLoad = this.piano.loadIfIdle(context);
|
const pianoLoad = this.piano.loadIfIdle(context);
|
||||||
|
|
@ -122,7 +122,7 @@ export class GardenAudio {
|
||||||
void pianoLoad
|
void pianoLoad
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this.graph.context === context && this.lifecycle !== 'destroyed') {
|
if (this.graph.context === context && this.lifecycle !== 'destroyed') {
|
||||||
this.pianoEngine.cue(context.currentTime);
|
this.pianoEngine.cue(context.currentTime, getVibeProfile(vibe));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
@ -207,7 +207,8 @@ export class GardenAudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.applyVibe(snapshot.vibe);
|
this.applyVibe(snapshot.vibe);
|
||||||
this.energy.update(context.currentTime);
|
const profile = getVibeProfile(snapshot.vibe);
|
||||||
|
this.energy.update(context.currentTime, profile);
|
||||||
|
|
||||||
if (snapshot.isErasing) {
|
if (snapshot.isErasing) {
|
||||||
this.energy.silence();
|
this.energy.silence();
|
||||||
|
|
@ -254,7 +255,8 @@ export class GardenAudio {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.energy.recordStroke(strokeEnergy);
|
const profile = getVibeProfile(stroke.vibe);
|
||||||
|
this.energy.recordStroke(strokeEnergy, profile);
|
||||||
this.pianoEngine.recordStroke({
|
this.pianoEngine.recordStroke({
|
||||||
vibe: stroke.vibe,
|
vibe: stroke.vibe,
|
||||||
now,
|
now,
|
||||||
|
|
@ -347,11 +349,10 @@ export class GardenAudio {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = getVibeProfile(snapshot.vibe);
|
|
||||||
const activity = snapshot.isErasing
|
const activity = snapshot.isErasing
|
||||||
? this.config.delay.erasingActivity
|
? this.config.delay.erasingActivity
|
||||||
: this.energy.getLevel();
|
: this.energy.getLevel();
|
||||||
this.graph.updateDelay(profile, activity);
|
this.graph.updateDelay(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyVibe(vibe: VibePreset): void {
|
private applyVibe(vibe: VibePreset): void {
|
||||||
|
|
@ -360,7 +361,8 @@ export class GardenAudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentVibeId = vibe.id;
|
this.currentVibeId = vibe.id;
|
||||||
this.graph.applyDelayProfile(getVibeProfile(vibe));
|
const profile = getVibeProfile(vibe);
|
||||||
this.pianoEngine.cue(this.graph.context.currentTime);
|
this.graph.applyDelayProfile();
|
||||||
|
this.pianoEngine.cue(this.graph.context.currentTime, profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,7 @@ const getChordIntervals = (
|
||||||
: chordVoicings.minorClosed;
|
: chordVoicings.minorClosed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const degreeToSemitone = (
|
const degreeToSemitone = (profile: GardenAudioVibeProfile, degree: number): number => {
|
||||||
profile: GardenAudioVibeProfile,
|
|
||||||
degree: number
|
|
||||||
): number => {
|
|
||||||
const scaleIndex =
|
const scaleIndex =
|
||||||
((degree % profile.scale.length) + profile.scale.length) % profile.scale.length;
|
((degree % profile.scale.length) + profile.scale.length) % profile.scale.length;
|
||||||
const octave = Math.floor(degree / profile.scale.length);
|
const octave = Math.floor(degree / profile.scale.length);
|
||||||
|
|
@ -96,6 +93,7 @@ interface BrushPhraseLayer {
|
||||||
export class GenerativePianoEngine {
|
export class GenerativePianoEngine {
|
||||||
private nextBeatStep: number | null = null;
|
private nextBeatStep: number | null = null;
|
||||||
private timelineStartedAt: number | null = null;
|
private timelineStartedAt: number | null = null;
|
||||||
|
private activeProfile: GardenAudioVibeProfile | null = null;
|
||||||
private isWaitingForGestureAccent = false;
|
private isWaitingForGestureAccent = false;
|
||||||
private lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
private lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
||||||
private lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
private lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
||||||
|
|
@ -124,13 +122,17 @@ export class GenerativePianoEngine {
|
||||||
return generativePianoTuning;
|
return generativePianoTuning;
|
||||||
}
|
}
|
||||||
|
|
||||||
public prime(now: number): void {
|
public prime(now: number, profile: GardenAudioVibeProfile): void {
|
||||||
|
this.activeProfile = profile;
|
||||||
this.timelineStartedAt ??= now;
|
this.timelineStartedAt ??= now;
|
||||||
this.nextBeatStep ??= 0;
|
this.nextBeatStep ??= 0;
|
||||||
this.nextBrushStreamStep ??= 0;
|
this.nextBrushStreamStep ??= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public cue(now: number): void {
|
public cue(now: number, profile?: GardenAudioVibeProfile): void {
|
||||||
|
if (profile) {
|
||||||
|
this.activeProfile = profile;
|
||||||
|
}
|
||||||
this.nextBeatStep = 0;
|
this.nextBeatStep = 0;
|
||||||
this.timelineStartedAt = now;
|
this.timelineStartedAt = now;
|
||||||
this.nextBrushStreamStep = 0;
|
this.nextBrushStreamStep = 0;
|
||||||
|
|
@ -148,10 +150,9 @@ export class GenerativePianoEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
public release(vibe: VibePreset, now: number): number {
|
public release(vibe: VibePreset, now: number): number {
|
||||||
this.prime(now);
|
|
||||||
this.isWaitingForGestureAccent = false;
|
|
||||||
|
|
||||||
const profile = getVibeProfile(vibe);
|
const profile = getVibeProfile(vibe);
|
||||||
|
this.prime(now, profile);
|
||||||
|
this.isWaitingForGestureAccent = false;
|
||||||
const releaseStep = this.getReleaseResolutionStep(now);
|
const releaseStep = this.getReleaseResolutionStep(now);
|
||||||
const releaseStart = Math.max(now, this.getTimeForStep(releaseStep));
|
const releaseStart = Math.max(now, this.getTimeForStep(releaseStep));
|
||||||
|
|
||||||
|
|
@ -198,7 +199,8 @@ export class GenerativePianoEngine {
|
||||||
activity,
|
activity,
|
||||||
maniaAmount = 0,
|
maniaAmount = 0,
|
||||||
}: StrokeAccentRequest): void {
|
}: StrokeAccentRequest): void {
|
||||||
this.prime(now);
|
const profile = getVibeProfile(vibe);
|
||||||
|
this.prime(now, profile);
|
||||||
const strength = clamp01(activity);
|
const strength = clamp01(activity);
|
||||||
const normalizedManiaAmount = clamp01(maniaAmount);
|
const normalizedManiaAmount = clamp01(maniaAmount);
|
||||||
const styleIndex = this.getStyleIndex(now);
|
const styleIndex = this.getStyleIndex(now);
|
||||||
|
|
@ -242,14 +244,14 @@ export class GenerativePianoEngine {
|
||||||
activity,
|
activity,
|
||||||
lookaheadSeconds = GENERATIVE_LOOKAHEAD_SECONDS,
|
lookaheadSeconds = GENERATIVE_LOOKAHEAD_SECONDS,
|
||||||
}: RenderLookaheadRequest): void {
|
}: RenderLookaheadRequest): void {
|
||||||
this.prime(now);
|
const profile = getVibeProfile(vibe);
|
||||||
|
this.prime(now, profile);
|
||||||
this.skipLateBeats(now);
|
this.skipLateBeats(now);
|
||||||
|
|
||||||
if (this.nextBeatStep === null) {
|
if (this.nextBeatStep === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = getVibeProfile(vibe);
|
|
||||||
const lookaheadEnd = now + lookaheadSeconds;
|
const lookaheadEnd = now + lookaheadSeconds;
|
||||||
while (this.getTimeForStep(this.nextBeatStep) <= lookaheadEnd) {
|
while (this.getTimeForStep(this.nextBeatStep) <= lookaheadEnd) {
|
||||||
const beatIndex = this.getBeatIndexForStep(this.nextBeatStep);
|
const beatIndex = this.getBeatIndexForStep(this.nextBeatStep);
|
||||||
|
|
@ -286,7 +288,7 @@ export class GenerativePianoEngine {
|
||||||
{ baseMidi: rootMidi, offsets },
|
{ baseMidi: rootMidi, offsets },
|
||||||
this.generation.padRegisters[index]
|
this.generation.padRegisters[index]
|
||||||
);
|
);
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity: stinger.velocities[index],
|
velocity: stinger.velocities[index],
|
||||||
pan: stinger.pans[index],
|
pan: stinger.pans[index],
|
||||||
|
|
@ -302,6 +304,7 @@ export class GenerativePianoEngine {
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
this.nextBeatStep = null;
|
this.nextBeatStep = null;
|
||||||
this.timelineStartedAt = null;
|
this.timelineStartedAt = null;
|
||||||
|
this.activeProfile = null;
|
||||||
this.isWaitingForGestureAccent = false;
|
this.isWaitingForGestureAccent = false;
|
||||||
this.lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
this.lastGestureAccentAt = Number.NEGATIVE_INFINITY;
|
||||||
this.lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
this.lastStrokeAccentStep = Number.NEGATIVE_INFINITY;
|
||||||
|
|
@ -314,6 +317,13 @@ export class GenerativePianoEngine {
|
||||||
this.brushStreamNoteCountsByBar.clear();
|
this.brushStreamNoteCountsByBar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private playProfileNote(profile: GardenAudioVibeProfile, note: PianoNote): void {
|
||||||
|
this.playNote({
|
||||||
|
...note,
|
||||||
|
sustainSeconds: profile.noteLength,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private renderBeat({
|
private renderBeat({
|
||||||
profile,
|
profile,
|
||||||
beatIndex,
|
beatIndex,
|
||||||
|
|
@ -398,7 +408,7 @@ export class GenerativePianoEngine {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
this.lastPadMidiByVoice[index] = midi;
|
this.lastPadMidiByVoice[index] = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
velocity + expression * this.generation.padChord.expressionVelocityWeight,
|
velocity + expression * this.generation.padChord.expressionVelocityWeight,
|
||||||
|
|
@ -433,8 +443,13 @@ export class GenerativePianoEngine {
|
||||||
|
|
||||||
offsetsByVoice.forEach((offsets, index) => {
|
offsetsByVoice.forEach((offsets, index) => {
|
||||||
const register = this.generation.padRegisters[index];
|
const register = this.generation.padRegisters[index];
|
||||||
const midi = this.chooseMidi({ baseMidi: rootMidi, offsets }, register, null, false);
|
const midi = this.chooseMidi(
|
||||||
this.playNote({
|
{ baseMidi: rootMidi, offsets },
|
||||||
|
register,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity: release.velocities[index],
|
velocity: release.velocities[index],
|
||||||
startTime: startTime + index * release.strumSeconds,
|
startTime: startTime + index * release.strumSeconds,
|
||||||
|
|
@ -470,7 +485,7 @@ export class GenerativePianoEngine {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.lastMidiByStyle[styleIndex] = midi;
|
this.lastMidiByStyle[styleIndex] = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.supportNote.velocityBase +
|
(this.generation.supportNote.velocityBase +
|
||||||
|
|
@ -516,7 +531,7 @@ export class GenerativePianoEngine {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.lastMidiByStyle[styleIndex] = midi;
|
this.lastMidiByStyle[styleIndex] = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.textureNote.velocityBase +
|
(this.generation.textureNote.velocityBase +
|
||||||
|
|
@ -563,7 +578,7 @@ export class GenerativePianoEngine {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.lastMidiByStyle[styleIndex] = midi;
|
this.lastMidiByStyle[styleIndex] = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.gestureAccent.velocityBase +
|
(this.generation.gestureAccent.velocityBase +
|
||||||
|
|
@ -608,7 +623,7 @@ export class GenerativePianoEngine {
|
||||||
|
|
||||||
this.lastMidiByStyle[styleIndex] = midi;
|
this.lastMidiByStyle[styleIndex] = midi;
|
||||||
this.lastBrushStreamMidi = midi;
|
this.lastBrushStreamMidi = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.touchNote.velocityBase +
|
(this.generation.touchNote.velocityBase +
|
||||||
|
|
@ -818,7 +833,7 @@ export class GenerativePianoEngine {
|
||||||
|
|
||||||
this.lastBrushStreamMidi = midi;
|
this.lastBrushStreamMidi = midi;
|
||||||
this.lastMidiByStyle[styleIndex] = midi;
|
this.lastMidiByStyle[styleIndex] = midi;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi,
|
midi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.brushStream.velocityBase +
|
(this.generation.brushStream.velocityBase +
|
||||||
|
|
@ -851,7 +866,7 @@ export class GenerativePianoEngine {
|
||||||
this.generation.brushStreamEcho.maxMidi
|
this.generation.brushStreamEcho.maxMidi
|
||||||
? midi + this.generation.brushStreamEcho.octaveSemitones
|
? midi + this.generation.brushStreamEcho.octaveSemitones
|
||||||
: midi - this.generation.brushStreamEcho.octaveSemitones;
|
: midi - this.generation.brushStreamEcho.octaveSemitones;
|
||||||
this.playNote({
|
this.playProfileNote(profile, {
|
||||||
midi: echoMidi,
|
midi: echoMidi,
|
||||||
velocity:
|
velocity:
|
||||||
(this.generation.brushStreamEcho.velocityBase +
|
(this.generation.brushStreamEcho.velocityBase +
|
||||||
|
|
@ -1159,14 +1174,18 @@ export class GenerativePianoEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExpression(activity: number): number {
|
private getExpression(activity: number): number {
|
||||||
|
const liftedActivity = Math.max(
|
||||||
|
activity,
|
||||||
|
this.activeProfile?.idleIntensity ?? this.config.rhythm.idleIntensity
|
||||||
|
);
|
||||||
return clamp01(
|
return clamp01(
|
||||||
(activity - this.config.rhythm.sparseActivity) /
|
(liftedActivity - this.config.rhythm.sparseActivity) /
|
||||||
(1 - this.config.rhythm.sparseActivity)
|
(1 - this.config.rhythm.sparseActivity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBeatDurationSeconds(): number {
|
private getBeatDurationSeconds(): number {
|
||||||
return 60 / this.config.rhythm.bpm;
|
return 60 / (this.activeProfile?.bpm ?? this.config.rhythm.bpm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStepDurationSeconds(): number {
|
private getStepDurationSeconds(): number {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export class PianoSampler {
|
||||||
role,
|
role,
|
||||||
delaySend = 0,
|
delaySend = 0,
|
||||||
lowpassHz = this.config.piano.lowpassHz,
|
lowpassHz = this.config.piano.lowpassHz,
|
||||||
|
sustainSeconds: profileSustainSeconds = this.config.piano.sustainSeconds,
|
||||||
}: PianoNote): void {
|
}: PianoNote): void {
|
||||||
const { context } = this.graph;
|
const { context } = this.graph;
|
||||||
const eventBus = this.graph.getPianoBus(role);
|
const eventBus = this.graph.getPianoBus(role);
|
||||||
|
|
@ -92,12 +93,11 @@ export class PianoSampler {
|
||||||
this.config.piano.gain * noteVelocity
|
this.config.piano.gain * noteVelocity
|
||||||
);
|
);
|
||||||
const sustainSeconds =
|
const sustainSeconds =
|
||||||
this.config.piano.sustainSeconds *
|
profileSustainSeconds *
|
||||||
(this.config.piano.sustainBase +
|
(this.config.piano.sustainBase +
|
||||||
noteVelocity * this.config.piano.sustainVelocityRange);
|
noteVelocity * this.config.piano.sustainVelocityRange);
|
||||||
const sustainAt =
|
const sustainAt =
|
||||||
scheduledStart +
|
scheduledStart + Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
||||||
Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds);
|
|
||||||
const releaseAt = sustainAt + sustainSeconds;
|
const releaseAt = sustainAt + sustainSeconds;
|
||||||
const stopAt = releaseAt + this.config.piano.releaseSeconds;
|
const stopAt = releaseAt + this.config.piano.releaseSeconds;
|
||||||
const source = context.createBufferSource();
|
const source = context.createBufferSource();
|
||||||
|
|
@ -150,7 +150,7 @@ export class PianoSampler {
|
||||||
const releaseAt =
|
const releaseAt =
|
||||||
scheduledStart +
|
scheduledStart +
|
||||||
clamp(
|
clamp(
|
||||||
durationSeconds + this.config.piano.sustainSeconds * 0.5,
|
durationSeconds + profileSustainSeconds * 0.5,
|
||||||
pianoSamplerTuning.minDurationSeconds,
|
pianoSamplerTuning.minDurationSeconds,
|
||||||
pianoSamplerTuning.synthMaxDurationSeconds
|
pianoSamplerTuning.synthMaxDurationSeconds
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { APP_STORAGE_KEYS, DEFAULT_AUDIO_VOLUME } from './app-constants';
|
import { APP_STORAGE_KEYS, DEFAULT_AUDIO_VOLUME } from './app-constants';
|
||||||
|
import { createGardenAudioConfig } from './audio/garden-audio-config';
|
||||||
import { defaultSettings } from './config/default-settings';
|
import { defaultSettings } from './config/default-settings';
|
||||||
import { runtimeControls } from './config/runtime-controls';
|
import { runtimeControls } from './config/runtime-controls';
|
||||||
import type { GardenAppConfig } from './config/types';
|
import type { GardenAppConfig } from './config/types';
|
||||||
|
|
@ -14,90 +15,7 @@ export type {
|
||||||
} from './config/types';
|
} from './config/types';
|
||||||
|
|
||||||
export const appConfig = {
|
export const appConfig = {
|
||||||
audio: {
|
audio: createGardenAudioConfig(),
|
||||||
masterVolume: DEFAULT_AUDIO_VOLUME,
|
|
||||||
fadeInSeconds: 0.45,
|
|
||||||
updateRampSeconds: 0.08,
|
|
||||||
delay: {
|
|
||||||
timeSeconds: 0.46,
|
|
||||||
feedback: 0.12,
|
|
||||||
wetGain: 0.044,
|
|
||||||
erasingActivity: 0.12,
|
|
||||||
activityFeedbackWeight: 0.08,
|
|
||||||
feedbackMax: 0.32,
|
|
||||||
feedbackMin: 0.04,
|
|
||||||
outputActivityWeight: 0.5,
|
|
||||||
outputBase: 0.65,
|
|
||||||
outputActivityDuck: 0.28,
|
|
||||||
timeRampSeconds: 0.12,
|
|
||||||
},
|
|
||||||
piano: {
|
|
||||||
maxVoices: 24,
|
|
||||||
gain: 0.48,
|
|
||||||
sustainSeconds: 0.42,
|
|
||||||
sustainLevel: 0.32,
|
|
||||||
releaseSeconds: 0.24,
|
|
||||||
lowpassHz: 7600,
|
|
||||||
gainAttackSeconds: 0.006,
|
|
||||||
lowpassMaxHz: 12000,
|
|
||||||
lowpassMinHz: 1400,
|
|
||||||
sustainBase: 0.45,
|
|
||||||
sustainVelocityRange: 0.55,
|
|
||||||
},
|
|
||||||
rhythm: {
|
|
||||||
bpm: 74,
|
|
||||||
stepsPerBeat: 4,
|
|
||||||
stepsPerBar: 16,
|
|
||||||
sparseActivity: 0.055,
|
|
||||||
},
|
|
||||||
eraser: {
|
|
||||||
minIntervalSeconds: 0.12,
|
|
||||||
noiseGain: 0.028,
|
|
||||||
filterMinHz: 650,
|
|
||||||
filterMaxHz: 3600,
|
|
||||||
durationSeconds: 0.08,
|
|
||||||
pan: 0,
|
|
||||||
pianoActivity: 0,
|
|
||||||
},
|
|
||||||
energy: {
|
|
||||||
attackSeconds: 0.08,
|
|
||||||
decaySeconds: 0.9,
|
|
||||||
immediateActivityScale: 0.85,
|
|
||||||
releaseSeconds: 1.15,
|
|
||||||
strokeDecaySeconds: 0.32,
|
|
||||||
},
|
|
||||||
graph: {
|
|
||||||
pianoBusGains: {
|
|
||||||
pad: 0.86,
|
|
||||||
support: 0.94,
|
|
||||||
texture: 0.88,
|
|
||||||
gesture: 1,
|
|
||||||
brush: 0.9,
|
|
||||||
stinger: 0.92,
|
|
||||||
},
|
|
||||||
pianoBusActivityDucking: {
|
|
||||||
pad: 0.42,
|
|
||||||
support: 0.18,
|
|
||||||
texture: -0.06,
|
|
||||||
gesture: 0,
|
|
||||||
brush: -0.08,
|
|
||||||
stinger: 0,
|
|
||||||
},
|
|
||||||
noiseBusGain: 0.72,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
fullActivitySpeed: 0.86,
|
|
||||||
activityNoiseFloorSpeed: 0.025,
|
|
||||||
activityCurve: 0.74,
|
|
||||||
activitySoftCeiling: 0.96,
|
|
||||||
activityAttackSeconds: 0.055,
|
|
||||||
activityReleaseSeconds: 0.2,
|
|
||||||
minAudibleDistance: 0.0025,
|
|
||||||
manicActivityThreshold: 0.9,
|
|
||||||
manicReleaseThreshold: 0.76,
|
|
||||||
maniaSmoothingSeconds: 0.12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
deltaTime: {
|
deltaTime: {
|
||||||
maxDeltaTimeSeconds: 1 / 30,
|
maxDeltaTimeSeconds: 1 / 30,
|
||||||
minDeltaTimeSeconds: 1 / 240,
|
minDeltaTimeSeconds: 1 / 240,
|
||||||
|
|
@ -124,7 +42,7 @@ export const appConfig = {
|
||||||
noiseHashMultiplier: 43758.5453123,
|
noiseHashMultiplier: 43758.5453123,
|
||||||
noiseHashX: 12.9898,
|
noiseHashX: 12.9898,
|
||||||
noiseHashY: 78.233,
|
noiseHashY: 78.233,
|
||||||
noiseTextureFormat: 'rgba8unorm',
|
noiseTextureFormat: 'r8unorm',
|
||||||
noiseTextureSize: 2048,
|
noiseTextureSize: 2048,
|
||||||
},
|
},
|
||||||
brush: {
|
brush: {
|
||||||
|
|
@ -261,7 +179,7 @@ export const appConfig = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tuningPane: {
|
tuningPane: {
|
||||||
expandedDepth: 1,
|
showFpsOverlay: import.meta.env.DEV,
|
||||||
startHidden: true,
|
startHidden: true,
|
||||||
title: 'Garden Config',
|
title: 'Garden Config',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { colorInteractionSettings } from './color-interactions';
|
|
||||||
import type { GardenAppConfig } from './types';
|
import type { GardenAppConfig } from './types';
|
||||||
|
|
||||||
export const defaultSettings: GardenAppConfig['defaultSettings'] = {
|
export const defaultSettings: GardenAppConfig['defaultSettings'] = {
|
||||||
selectedColorIndex: 0,
|
selectedColorIndex: 0,
|
||||||
...colorInteractionSettings,
|
|
||||||
|
|
||||||
turnWhenLost: 0.8,
|
turnWhenLost: 0.8,
|
||||||
forwardRotationScale: 0.25,
|
forwardRotationScale: 0.25,
|
||||||
|
sensorOffsetAngle: 32,
|
||||||
introNearDistanceMin: 28,
|
introNearDistanceMin: 28,
|
||||||
introNearDistanceInner: 4,
|
introNearDistanceInner: 4,
|
||||||
introNearSensorOffsetMultiplier: 0.75,
|
introNearSensorOffsetMultiplier: 0.75,
|
||||||
|
|
@ -19,7 +18,7 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = {
|
||||||
introStepStopDistance: 0.5,
|
introStepStopDistance: 0.5,
|
||||||
randomTimeScale: 0.34816,
|
randomTimeScale: 0.34816,
|
||||||
|
|
||||||
diffusionRateBrush: 0.35,
|
diffusionRateTrails: 0.22,
|
||||||
decayRateBrush: 18,
|
decayRateBrush: 18,
|
||||||
diffusionDecayRateDivisor: 1000,
|
diffusionDecayRateDivisor: 1000,
|
||||||
diffusionNeighborDivisor: 8,
|
diffusionNeighborDivisor: 8,
|
||||||
|
|
@ -33,7 +32,6 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = {
|
||||||
brushCurveSegmentBrushRadiusRatio: 0.65,
|
brushCurveSegmentBrushRadiusRatio: 0.65,
|
||||||
brushSmoothingMinSampleDistance: 0.5,
|
brushSmoothingMinSampleDistance: 0.5,
|
||||||
|
|
||||||
brushSizeVariation: 0.5,
|
|
||||||
brushAlpha: 1,
|
brushAlpha: 1,
|
||||||
brushDiscardThreshold: 0.02,
|
brushDiscardThreshold: 0.02,
|
||||||
brushCoarseNoiseScale: 160,
|
brushCoarseNoiseScale: 160,
|
||||||
|
|
@ -58,5 +56,4 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = {
|
||||||
renderTraceNormalizationFloor: 1,
|
renderTraceNormalizationFloor: 1,
|
||||||
renderBrushColorBase: 1.2,
|
renderBrushColorBase: 1.2,
|
||||||
renderBrushColorStrengthMultiplier: 1.6,
|
renderBrushColorStrengthMultiplier: 1.6,
|
||||||
backgroundGrainStrength: 0.018,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,380 +11,100 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = {
|
||||||
color3ToColor1: colorInteractionControl('3 -> 1'),
|
color3ToColor1: colorInteractionControl('3 -> 1'),
|
||||||
color3ToColor2: colorInteractionControl('3 -> 2'),
|
color3ToColor2: colorInteractionControl('3 -> 2'),
|
||||||
color3ToColor3: colorInteractionControl('3 -> 3'),
|
color3ToColor3: colorInteractionControl('3 -> 3'),
|
||||||
brushEffectDuration: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 0.5,
|
|
||||||
max: 20,
|
|
||||||
step: 0.05,
|
|
||||||
},
|
|
||||||
brushSize: {
|
brushSize: {
|
||||||
folder: 'Brush',
|
folder: 'Brush',
|
||||||
|
label: 'brush size',
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 60,
|
max: 60,
|
||||||
step: 0.25,
|
step: 0.25,
|
||||||
},
|
},
|
||||||
|
spawnPerPixel: {
|
||||||
|
folder: 'Brush',
|
||||||
|
label: 'agents per brush pixel',
|
||||||
|
min: 0.01,
|
||||||
|
max: 1,
|
||||||
|
step: 0.001,
|
||||||
|
},
|
||||||
brushSizeVariation: {
|
brushSizeVariation: {
|
||||||
folder: 'Brush',
|
folder: 'Brush',
|
||||||
|
label: 'brush variance',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
},
|
},
|
||||||
brushAlpha: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
brushDiscardThreshold: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 0.25,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
brushCoarseNoiseScale: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 1,
|
|
||||||
max: 480,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
brushGrainNoiseScale: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 1,
|
|
||||||
max: 180,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
brushGrainNoiseOffsetX: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: -2,
|
|
||||||
max: 2,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
brushGrainNoiseOffsetY: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: -2,
|
|
||||||
max: 2,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
brushGrainMinStrength: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
brushGrainMaxStrength: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 2,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
brushCurveResolution: {
|
|
||||||
folder: 'Brush',
|
|
||||||
integer: true,
|
|
||||||
label: 'curve resolution',
|
|
||||||
min: 1,
|
|
||||||
max: 32,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
brushSmoothingMinSampleDistance: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
brushCurveMinSegmentSpacing: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0.1,
|
|
||||||
max: 32,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
brushCurveSegmentBrushRadiusRatio: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
brushCurveMinBrushRadius: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0.1,
|
|
||||||
max: 16,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
brushCurveMirrorResolutionExponent: {
|
|
||||||
folder: 'Brush',
|
|
||||||
min: 0,
|
|
||||||
max: 2,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
clarity: {
|
|
||||||
folder: 'Render',
|
|
||||||
min: 0.00001,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
renderTraceNormalizationFloor: {
|
|
||||||
folder: 'Render',
|
|
||||||
min: 0.01,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
renderBrushColorBase: {
|
|
||||||
folder: 'Render',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
renderBrushColorStrengthMultiplier: {
|
|
||||||
folder: 'Render',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
backgroundGrainStrength: {
|
|
||||||
folder: 'Render',
|
|
||||||
min: 0,
|
|
||||||
max: 0.12,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
internalRenderAreaMegapixels: {
|
|
||||||
folder: 'Render',
|
|
||||||
label: 'internal area (MP)',
|
|
||||||
min: 0.5,
|
|
||||||
max: 16.6,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
decayRateBrush: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 0.1,
|
|
||||||
max: 100,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
decayRateTrails: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 0.1,
|
|
||||||
max: 5000,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
diffusionRateBrush: {
|
diffusionRateBrush: {
|
||||||
folder: 'Diffusion',
|
folder: 'Brush',
|
||||||
|
label: 'brush diffusion',
|
||||||
min: 0.001,
|
min: 0.001,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.001,
|
step: 0.001,
|
||||||
},
|
},
|
||||||
diffusionDecayRateDivisor: {
|
|
||||||
folder: 'Diffusion',
|
sensorOffsetDistance: {
|
||||||
min: 1,
|
folder: 'Agents',
|
||||||
max: 5000,
|
label: 'sensor distance',
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
diffusionNeighborDivisor: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 1,
|
|
||||||
max: 16,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
brushDecayAlphaOffset: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 1,
|
|
||||||
max: 1.1,
|
|
||||||
step: 0.0001,
|
|
||||||
},
|
|
||||||
diffusionRateTrails: {
|
|
||||||
folder: 'Diffusion',
|
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 200,
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
eraserSize: {
|
|
||||||
folder: 'Brush',
|
|
||||||
integer: true,
|
|
||||||
min: 24,
|
|
||||||
max: 240,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
eraserMaskAlphaThreshold: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
eraserLineDistanceEpsilon: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 0.01,
|
|
||||||
step: 0.00001,
|
|
||||||
},
|
|
||||||
eraserClearRed: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
eraserClearGreen: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
eraserClearBlue: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
eraserClearAlpha: {
|
|
||||||
folder: 'Eraser',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
individualTrailWeight: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
adaptiveCapInitial: {
|
|
||||||
folder: 'Agent',
|
|
||||||
integer: true,
|
|
||||||
label: 'adaptive cap initial',
|
|
||||||
min: 50_000,
|
|
||||||
max: 2_000_000,
|
|
||||||
step: 10_000,
|
|
||||||
},
|
|
||||||
adaptiveCapMin: {
|
|
||||||
folder: 'Agent',
|
|
||||||
integer: true,
|
|
||||||
label: 'adaptive cap min',
|
|
||||||
min: 0,
|
|
||||||
max: 500_000,
|
|
||||||
step: 10_000,
|
|
||||||
},
|
|
||||||
maxAgentCount: {
|
|
||||||
folder: 'Agent',
|
|
||||||
integer: true,
|
|
||||||
label: 'max agent count',
|
|
||||||
step: 10_000,
|
|
||||||
},
|
|
||||||
mirrorSegmentCount: {
|
|
||||||
folder: 'Brush',
|
|
||||||
integer: true,
|
|
||||||
min: 1,
|
|
||||||
max: 12,
|
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
moveSpeed: {
|
moveSpeed: {
|
||||||
folder: 'Agent',
|
folder: 'Agents',
|
||||||
|
label: 'move speed',
|
||||||
min: 10,
|
min: 10,
|
||||||
max: 500,
|
max: 500,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
selectedColorIndex: {
|
|
||||||
folder: 'Brush',
|
|
||||||
integer: true,
|
|
||||||
min: 0,
|
|
||||||
max: 2,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
sensorOffsetAngle: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 90,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
sensorOffsetDistance: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 200,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
spawnPerPixel: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0.01,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
turnSpeed: {
|
turnSpeed: {
|
||||||
folder: 'Agent',
|
folder: 'Agents',
|
||||||
|
label: 'turn speed',
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 200,
|
max: 200,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
turnWhenLost: {
|
individualTrailWeight: {
|
||||||
folder: 'Agent',
|
folder: 'Agents',
|
||||||
|
label: 'individual trail weight',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.001,
|
step: 0.001,
|
||||||
},
|
},
|
||||||
forwardRotationScale: {
|
decayRateTrails: {
|
||||||
folder: 'Agent',
|
folder: 'Agents',
|
||||||
min: 0,
|
label: 'trail decay',
|
||||||
|
min: 800,
|
||||||
|
max: 1000,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
clarity: {
|
||||||
|
folder: 'Look',
|
||||||
|
label: 'clarity',
|
||||||
|
min: 0.00001,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.001,
|
step: 0.001,
|
||||||
},
|
},
|
||||||
introNearDistanceMin: {
|
backgroundGrainStrength: {
|
||||||
folder: 'Agent',
|
folder: 'Look',
|
||||||
|
label: 'grain strength',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 0.12,
|
||||||
|
step: 0.001,
|
||||||
|
},
|
||||||
|
|
||||||
|
maxAgentCount: {
|
||||||
|
folder: 'Performance',
|
||||||
|
integer: true,
|
||||||
|
label: 'max agent count',
|
||||||
|
min: 0,
|
||||||
|
max: 2_000_000,
|
||||||
|
step: 10_000,
|
||||||
|
},
|
||||||
|
internalRenderAreaMegapixels: {
|
||||||
|
folder: 'Performance',
|
||||||
|
label: 'internal resolution (MP)',
|
||||||
|
min: 0.5,
|
||||||
|
max: 16.6,
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
},
|
},
|
||||||
introNearDistanceInner: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
introNearSensorOffsetMultiplier: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
introTargetAngleBlend: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
introProgressCutoff: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
introTurnRateMultiplier: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 8,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
introRandomTurnMultiplier: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 2,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
introFarMoveMultiplier: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 6,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
introNearMoveMultiplier: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
step: 0.001,
|
|
||||||
},
|
|
||||||
introStepStopDistance: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 8,
|
|
||||||
step: 0.01,
|
|
||||||
},
|
|
||||||
randomTimeScale: {
|
|
||||||
folder: 'Agent',
|
|
||||||
min: 0,
|
|
||||||
max: 4,
|
|
||||||
step: 0.00001,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
GardenAudioConfig,
|
GardenAudioConfig,
|
||||||
GardenAudioVibeProfile,
|
GardenAudioVibeSettings,
|
||||||
} from '../audio/garden-audio-config';
|
} from '../audio/garden-audio-config';
|
||||||
import type { AgentSettings } from '../pipelines/agents/agent-settings';
|
import type { AgentSettings } from '../pipelines/agents/agent-settings';
|
||||||
import type { BrushSettings } from '../pipelines/brush/brush-settings';
|
import type { BrushSettings } from '../pipelines/brush/brush-settings';
|
||||||
|
|
@ -45,19 +45,29 @@ export type GardenRuntimeSettings = {
|
||||||
DiffusionSettings &
|
DiffusionSettings &
|
||||||
RenderSettings;
|
RenderSettings;
|
||||||
|
|
||||||
type RuntimeSettingControlConfig = {
|
type RuntimeSettingControlConfig = Partial<
|
||||||
[Key in keyof GardenRuntimeSettings]: NumberControlConfig;
|
Record<keyof GardenRuntimeSettings, NumberControlConfig>
|
||||||
};
|
>;
|
||||||
|
|
||||||
type GardenVibeSettings = Pick<
|
type GardenVibeSettings = Pick<
|
||||||
GardenRuntimeSettings,
|
GardenRuntimeSettings,
|
||||||
|
| 'backgroundGrainStrength'
|
||||||
| 'brushSize'
|
| 'brushSize'
|
||||||
|
| 'brushSizeVariation'
|
||||||
| 'clarity'
|
| 'clarity'
|
||||||
|
| 'color1ToColor1'
|
||||||
|
| 'color1ToColor2'
|
||||||
|
| 'color1ToColor3'
|
||||||
|
| 'color2ToColor1'
|
||||||
|
| 'color2ToColor2'
|
||||||
|
| 'color2ToColor3'
|
||||||
|
| 'color3ToColor1'
|
||||||
|
| 'color3ToColor2'
|
||||||
|
| 'color3ToColor3'
|
||||||
| 'decayRateTrails'
|
| 'decayRateTrails'
|
||||||
| 'diffusionRateTrails'
|
| 'diffusionRateBrush'
|
||||||
| 'individualTrailWeight'
|
| 'individualTrailWeight'
|
||||||
| 'moveSpeed'
|
| 'moveSpeed'
|
||||||
| 'sensorOffsetAngle'
|
|
||||||
| 'sensorOffsetDistance'
|
| 'sensorOffsetDistance'
|
||||||
| 'spawnPerPixel'
|
| 'spawnPerPixel'
|
||||||
| 'turnSpeed'
|
| 'turnSpeed'
|
||||||
|
|
@ -83,7 +93,7 @@ export interface VibePreset {
|
||||||
colors: [RgbColor, RgbColor, RgbColor];
|
colors: [RgbColor, RgbColor, RgbColor];
|
||||||
backgroundColor: RgbColor;
|
backgroundColor: RgbColor;
|
||||||
settings: GardenVibeSettings;
|
settings: GardenVibeSettings;
|
||||||
audio: GardenAudioVibeProfile;
|
audio: GardenAudioVibeSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GardenAppConfig {
|
export interface GardenAppConfig {
|
||||||
|
|
@ -239,7 +249,7 @@ export interface GardenAppConfig {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
tuningPane: {
|
tuningPane: {
|
||||||
expandedDepth: number;
|
showFpsOverlay: boolean;
|
||||||
startHidden: boolean;
|
startHidden: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
import type { GardenAudioChord } from '../audio/garden-audio-config';
|
import type { GardenAudioVibeSettings } from '../audio/garden-audio-config';
|
||||||
|
import { colorInteractionSettings } from './color-interactions';
|
||||||
import { VibeId, type VibePreset } from './types';
|
import { VibeId, type VibePreset } from './types';
|
||||||
|
|
||||||
const majorProgression: Array<GardenAudioChord> = [
|
const defaultAudioSettings = {
|
||||||
{ rootOffset: 0, quality: 'major' },
|
idleIntensity: 0.08,
|
||||||
{ rootOffset: 9, quality: 'minor' },
|
bpm: 74,
|
||||||
{ rootOffset: 5, quality: 'major' },
|
rampUpIntensity: 0.85,
|
||||||
{ rootOffset: 7, quality: 'major' },
|
rampUpTime: 0.08,
|
||||||
];
|
noteLength: 0.42,
|
||||||
|
notePitchOffset: 0,
|
||||||
const minorProgression: Array<GardenAudioChord> = [
|
} satisfies Omit<GardenAudioVibeSettings, 'brightness'>;
|
||||||
{ rootOffset: 0, quality: 'minor' },
|
|
||||||
{ rootOffset: 8, quality: 'major' },
|
|
||||||
{ rootOffset: 3, quality: 'major' },
|
|
||||||
{ rootOffset: 10, quality: 'major' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const majorPentatonic = [0, 2, 4, 7, 9];
|
|
||||||
const suspendedPentatonic = [0, 2, 5, 7, 9];
|
|
||||||
const mixolydianPentatonic = [0, 2, 4, 7, 10];
|
|
||||||
const dorianHexatonic = [0, 2, 3, 5, 7, 10];
|
|
||||||
const darkMinorPentatonic = [0, 2, 3, 7, 10];
|
|
||||||
|
|
||||||
export const defaultVibeId = VibeId.CandyRain;
|
export const defaultVibeId = VibeId.CandyRain;
|
||||||
|
|
||||||
|
|
@ -34,23 +24,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [16, 21, 31],
|
backgroundColor: [16, 21, 31],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.018,
|
||||||
brushSize: 14,
|
brushSize: 14,
|
||||||
|
brushSizeVariation: 0.5,
|
||||||
clarity: 0.62,
|
clarity: 0.62,
|
||||||
decayRateTrails: 965,
|
decayRateTrails: 965,
|
||||||
diffusionRateTrails: 0.22,
|
diffusionRateBrush: 0.35,
|
||||||
individualTrailWeight: 0.07,
|
individualTrailWeight: 0.07,
|
||||||
moveSpeed: 82,
|
moveSpeed: 82,
|
||||||
sensorOffsetAngle: 34,
|
|
||||||
sensorOffsetDistance: 38,
|
sensorOffsetDistance: 38,
|
||||||
spawnPerPixel: 0.22,
|
spawnPerPixel: 0.22,
|
||||||
turnSpeed: 58,
|
turnSpeed: 58,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 57,
|
...defaultAudioSettings,
|
||||||
scale: majorPentatonic,
|
|
||||||
brightness: 1.04,
|
brightness: 1.04,
|
||||||
delayTimeMultiplier: 0.92,
|
|
||||||
progression: majorProgression,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -63,28 +52,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [23, 32, 22],
|
backgroundColor: [23, 32, 22],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.014,
|
||||||
brushSize: 16,
|
brushSize: 16,
|
||||||
|
brushSizeVariation: 0.35,
|
||||||
clarity: 0.68,
|
clarity: 0.68,
|
||||||
decayRateTrails: 975,
|
decayRateTrails: 975,
|
||||||
diffusionRateTrails: 0.18,
|
diffusionRateBrush: 0.28,
|
||||||
individualTrailWeight: 0.06,
|
individualTrailWeight: 0.06,
|
||||||
moveSpeed: 70,
|
moveSpeed: 70,
|
||||||
sensorOffsetAngle: 28,
|
|
||||||
sensorOffsetDistance: 46,
|
sensorOffsetDistance: 46,
|
||||||
spawnPerPixel: 0.18,
|
spawnPerPixel: 0.18,
|
||||||
turnSpeed: 44,
|
turnSpeed: 44,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 53,
|
...defaultAudioSettings,
|
||||||
scale: mixolydianPentatonic,
|
|
||||||
brightness: 0.92,
|
brightness: 0.92,
|
||||||
delayTimeMultiplier: 1.08,
|
|
||||||
progression: [
|
|
||||||
{ rootOffset: 0, quality: 'major' },
|
|
||||||
{ rootOffset: 7, quality: 'major' },
|
|
||||||
{ rootOffset: 9, quality: 'minor' },
|
|
||||||
{ rootOffset: 5, quality: 'major' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -97,23 +80,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [15, 24, 34],
|
backgroundColor: [15, 24, 34],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.022,
|
||||||
brushSize: 13,
|
brushSize: 13,
|
||||||
|
brushSizeVariation: 0.58,
|
||||||
clarity: 0.58,
|
clarity: 0.58,
|
||||||
decayRateTrails: 955,
|
decayRateTrails: 955,
|
||||||
diffusionRateTrails: 0.28,
|
diffusionRateBrush: 0.42,
|
||||||
individualTrailWeight: 0.055,
|
individualTrailWeight: 0.055,
|
||||||
moveSpeed: 90,
|
moveSpeed: 90,
|
||||||
sensorOffsetAngle: 36,
|
|
||||||
sensorOffsetDistance: 35,
|
sensorOffsetDistance: 35,
|
||||||
spawnPerPixel: 0.25,
|
spawnPerPixel: 0.25,
|
||||||
turnSpeed: 62,
|
turnSpeed: 62,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 50,
|
...defaultAudioSettings,
|
||||||
scale: dorianHexatonic,
|
|
||||||
brightness: 1,
|
brightness: 1,
|
||||||
delayTimeMultiplier: 1.12,
|
|
||||||
progression: minorProgression,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -126,23 +108,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [20, 18, 29],
|
backgroundColor: [20, 18, 29],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.018,
|
||||||
brushSize: 12,
|
brushSize: 12,
|
||||||
|
brushSizeVariation: 0.45,
|
||||||
clarity: 0.64,
|
clarity: 0.64,
|
||||||
decayRateTrails: 968,
|
decayRateTrails: 968,
|
||||||
diffusionRateTrails: 0.2,
|
diffusionRateBrush: 0.32,
|
||||||
individualTrailWeight: 0.065,
|
individualTrailWeight: 0.065,
|
||||||
moveSpeed: 76,
|
moveSpeed: 76,
|
||||||
sensorOffsetAngle: 32,
|
|
||||||
sensorOffsetDistance: 42,
|
sensorOffsetDistance: 42,
|
||||||
spawnPerPixel: 0.2,
|
spawnPerPixel: 0.2,
|
||||||
turnSpeed: 52,
|
turnSpeed: 52,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 49,
|
...defaultAudioSettings,
|
||||||
scale: darkMinorPentatonic,
|
|
||||||
brightness: 0.9,
|
brightness: 0.9,
|
||||||
delayTimeMultiplier: 1.24,
|
|
||||||
progression: minorProgression,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -155,23 +136,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [25, 23, 22],
|
backgroundColor: [25, 23, 22],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.024,
|
||||||
brushSize: 15,
|
brushSize: 15,
|
||||||
|
brushSizeVariation: 0.62,
|
||||||
clarity: 0.55,
|
clarity: 0.55,
|
||||||
decayRateTrails: 948,
|
decayRateTrails: 948,
|
||||||
diffusionRateTrails: 0.32,
|
diffusionRateBrush: 0.48,
|
||||||
individualTrailWeight: 0.05,
|
individualTrailWeight: 0.05,
|
||||||
moveSpeed: 96,
|
moveSpeed: 96,
|
||||||
sensorOffsetAngle: 40,
|
|
||||||
sensorOffsetDistance: 32,
|
sensorOffsetDistance: 32,
|
||||||
spawnPerPixel: 0.24,
|
spawnPerPixel: 0.24,
|
||||||
turnSpeed: 70,
|
turnSpeed: 70,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 56,
|
...defaultAudioSettings,
|
||||||
scale: majorPentatonic,
|
|
||||||
brightness: 1.08,
|
brightness: 1.08,
|
||||||
delayTimeMultiplier: 0.86,
|
|
||||||
progression: majorProgression,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -184,28 +164,22 @@ export const vibePresets: Array<VibePreset> = [
|
||||||
],
|
],
|
||||||
backgroundColor: [16, 24, 32],
|
backgroundColor: [16, 24, 32],
|
||||||
settings: {
|
settings: {
|
||||||
|
...colorInteractionSettings,
|
||||||
|
backgroundGrainStrength: 0.012,
|
||||||
brushSize: 18,
|
brushSize: 18,
|
||||||
|
brushSizeVariation: 0.28,
|
||||||
clarity: 0.7,
|
clarity: 0.7,
|
||||||
decayRateTrails: 982,
|
decayRateTrails: 982,
|
||||||
diffusionRateTrails: 0.14,
|
diffusionRateBrush: 0.24,
|
||||||
individualTrailWeight: 0.075,
|
individualTrailWeight: 0.075,
|
||||||
moveSpeed: 62,
|
moveSpeed: 62,
|
||||||
sensorOffsetAngle: 26,
|
|
||||||
sensorOffsetDistance: 52,
|
sensorOffsetDistance: 52,
|
||||||
spawnPerPixel: 0.16,
|
spawnPerPixel: 0.16,
|
||||||
turnSpeed: 40,
|
turnSpeed: 40,
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
rootMidi: 62,
|
...defaultAudioSettings,
|
||||||
scale: suspendedPentatonic,
|
|
||||||
brightness: 0.88,
|
brightness: 0.88,
|
||||||
delayTimeMultiplier: 1.32,
|
|
||||||
progression: [
|
|
||||||
{ rootOffset: 0, quality: 'major' },
|
|
||||||
{ rootOffset: 5, quality: 'major' },
|
|
||||||
{ rootOffset: 9, quality: 'minor' },
|
|
||||||
{ rootOffset: 7, quality: 'major' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export class GameLoopResources {
|
||||||
public readonly eraserAgentPipeline: EraserAgentPipeline;
|
public readonly eraserAgentPipeline: EraserAgentPipeline;
|
||||||
public readonly eraserTexturePipeline: EraserTexturePipeline;
|
public readonly eraserTexturePipeline: EraserTexturePipeline;
|
||||||
public readonly diffusionPipeline: DiffusionPipeline;
|
public readonly diffusionPipeline: DiffusionPipeline;
|
||||||
public readonly brushEffectDiffusionPipeline: DiffusionPipeline;
|
|
||||||
public readonly renderPipeline: RenderPipeline;
|
public readonly renderPipeline: RenderPipeline;
|
||||||
|
|
||||||
private readonly frameRenderer: SimulationFrameRenderer;
|
private readonly frameRenderer: SimulationFrameRenderer;
|
||||||
|
|
@ -74,7 +73,6 @@ export class GameLoopResources {
|
||||||
);
|
);
|
||||||
this.eraserTexturePipeline = new EraserTexturePipeline(this.device, this.commonState);
|
this.eraserTexturePipeline = new EraserTexturePipeline(this.device, this.commonState);
|
||||||
this.diffusionPipeline = new DiffusionPipeline(this.device);
|
this.diffusionPipeline = new DiffusionPipeline(this.device);
|
||||||
this.brushEffectDiffusionPipeline = new DiffusionPipeline(this.device);
|
|
||||||
this.renderPipeline = new RenderPipeline(context, this.device, this.commonState);
|
this.renderPipeline = new RenderPipeline(context, this.device, this.commonState);
|
||||||
|
|
||||||
this.frameRenderer = new SimulationFrameRenderer(this.device, this.textures, {
|
this.frameRenderer = new SimulationFrameRenderer(this.device, this.textures, {
|
||||||
|
|
@ -83,7 +81,6 @@ export class GameLoopResources {
|
||||||
eraserAgentPipeline: this.eraserAgentPipeline,
|
eraserAgentPipeline: this.eraserAgentPipeline,
|
||||||
eraserTexturePipeline: this.eraserTexturePipeline,
|
eraserTexturePipeline: this.eraserTexturePipeline,
|
||||||
diffusionPipeline: this.diffusionPipeline,
|
diffusionPipeline: this.diffusionPipeline,
|
||||||
brushEffectDiffusionPipeline: this.brushEffectDiffusionPipeline,
|
|
||||||
renderPipeline: this.renderPipeline,
|
renderPipeline: this.renderPipeline,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +91,7 @@ export class GameLoopResources {
|
||||||
|
|
||||||
public clearSimulation(): void {
|
public clearSimulation(): void {
|
||||||
this.textures.clear();
|
this.textures.clear();
|
||||||
|
this.frameRenderer.resetSourceMapActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFrameParameters({
|
public setFrameParameters({
|
||||||
|
|
@ -115,6 +113,7 @@ export class GameLoopResources {
|
||||||
this.agentPipeline.setParameters({
|
this.agentPipeline.setParameters({
|
||||||
...settings,
|
...settings,
|
||||||
deltaTime,
|
deltaTime,
|
||||||
|
time,
|
||||||
agentCount: activeAgentCount,
|
agentCount: activeAgentCount,
|
||||||
moveSpeed:
|
moveSpeed:
|
||||||
settings.moveSpeed *
|
settings.moveSpeed *
|
||||||
|
|
@ -138,6 +137,7 @@ export class GameLoopResources {
|
||||||
this.eraserAgentPipeline.setParameters({
|
this.eraserAgentPipeline.setParameters({
|
||||||
agentCount: activeAgentCount,
|
agentCount: activeAgentCount,
|
||||||
eraserMaskAlphaThreshold: settings.eraserMaskAlphaThreshold,
|
eraserMaskAlphaThreshold: settings.eraserMaskAlphaThreshold,
|
||||||
|
maskSize: canvasSize,
|
||||||
});
|
});
|
||||||
this.eraserTexturePipeline.setParameters({
|
this.eraserTexturePipeline.setParameters({
|
||||||
eraserSize: eraserPixelSize,
|
eraserSize: eraserPixelSize,
|
||||||
|
|
@ -147,7 +147,6 @@ export class GameLoopResources {
|
||||||
eraserClearBlue: settings.eraserClearBlue,
|
eraserClearBlue: settings.eraserClearBlue,
|
||||||
eraserClearAlpha: settings.eraserClearAlpha,
|
eraserClearAlpha: settings.eraserClearAlpha,
|
||||||
});
|
});
|
||||||
this.setBrushEffectDiffusionParameters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public executeFrame(
|
public executeFrame(
|
||||||
|
|
@ -164,20 +163,8 @@ export class GameLoopResources {
|
||||||
this.eraserAgentPipeline.destroy();
|
this.eraserAgentPipeline.destroy();
|
||||||
this.eraserTexturePipeline.destroy();
|
this.eraserTexturePipeline.destroy();
|
||||||
this.diffusionPipeline.destroy();
|
this.diffusionPipeline.destroy();
|
||||||
this.brushEffectDiffusionPipeline.destroy();
|
|
||||||
this.renderPipeline.destroy();
|
this.renderPipeline.destroy();
|
||||||
this.commonState.destroy();
|
this.commonState.destroy();
|
||||||
this.textures.destroy();
|
this.textures.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setBrushEffectDiffusionParameters(): void {
|
|
||||||
const framesToOneE = Math.max(
|
|
||||||
1,
|
|
||||||
settings.brushEffectDuration * appConfig.simulation.brushEffectFramesPerSecond
|
|
||||||
);
|
|
||||||
this.brushEffectDiffusionPipeline.setParameters({
|
|
||||||
...settings,
|
|
||||||
decayRateTrails: Math.exp(-1 / framesToOneE) * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default class GameLoop {
|
||||||
private readonly agentPopulation: AgentPopulation;
|
private readonly agentPopulation: AgentPopulation;
|
||||||
private readonly exportSnapshotRenderer: ExportSnapshotRenderer;
|
private readonly exportSnapshotRenderer: ExportSnapshotRenderer;
|
||||||
private readonly framePerformance = new FramePerformance();
|
private readonly framePerformance = new FramePerformance();
|
||||||
private readonly devStatsOverlay: DevStatsOverlay | null;
|
private devStatsOverlay: DevStatsOverlay | null = null;
|
||||||
private readonly toolbarContrastMonitor: ToolbarContrastMonitor;
|
private readonly toolbarContrastMonitor: ToolbarContrastMonitor;
|
||||||
private readonly seedValue = Math.floor(Math.random() * 0xffffffff);
|
private readonly seedValue = Math.floor(Math.random() * 0xffffffff);
|
||||||
private readonly seed = this.seedValue.toString(16);
|
private readonly seed = this.seedValue.toString(16);
|
||||||
|
|
@ -47,9 +47,6 @@ export default class GameLoop {
|
||||||
ui: GardenUi
|
ui: GardenUi
|
||||||
) {
|
) {
|
||||||
this.resize();
|
this.resize();
|
||||||
this.devStatsOverlay = import.meta.env.DEV
|
|
||||||
? new DevStatsOverlay(canvas.parentElement ?? document.body)
|
|
||||||
: null;
|
|
||||||
this.resources = new GameLoopResources(
|
this.resources = new GameLoopResources(
|
||||||
canvas,
|
canvas,
|
||||||
device,
|
device,
|
||||||
|
|
@ -103,8 +100,12 @@ export default class GameLoop {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', this.resizeListener);
|
window.addEventListener('resize', this.resizeListener);
|
||||||
this.pointerInput.attach();
|
|
||||||
this.eraserPreviewController.attach();
|
this.eraserPreviewController.attach();
|
||||||
|
this.syncDevStatsOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public attachPointerInput(): void {
|
||||||
|
this.pointerInput.attach();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEraseMode(isErasing: boolean): void {
|
public setEraseMode(isErasing: boolean): void {
|
||||||
|
|
@ -118,6 +119,7 @@ export default class GameLoop {
|
||||||
|
|
||||||
public onVibeChanged(): void {
|
public onVibeChanged(): void {
|
||||||
this.agentPopulation.onVibeChanged();
|
this.agentPopulation.onVibeChanged();
|
||||||
|
this.syncDevStatsOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAudioMuted(isMuted: boolean): void {
|
public setAudioMuted(isMuted: boolean): void {
|
||||||
|
|
@ -153,6 +155,7 @@ export default class GameLoop {
|
||||||
this.pointerInput.detach();
|
this.pointerInput.detach();
|
||||||
this.eraserPreviewController.detach();
|
this.eraserPreviewController.detach();
|
||||||
this.devStatsOverlay?.destroy();
|
this.devStatsOverlay?.destroy();
|
||||||
|
this.devStatsOverlay = null;
|
||||||
this.toolbarContrastMonitor.destroy();
|
this.toolbarContrastMonitor.destroy();
|
||||||
this.introPrompt.destroy();
|
this.introPrompt.destroy();
|
||||||
await this.agentPopulation.waitForCompaction();
|
await this.agentPopulation.waitForCompaction();
|
||||||
|
|
@ -220,6 +223,18 @@ export default class GameLoop {
|
||||||
requestAnimationFrame(this.render);
|
requestAnimationFrame(this.render);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private syncDevStatsOverlay(): void {
|
||||||
|
if (appConfig.tuningPane.showFpsOverlay) {
|
||||||
|
this.devStatsOverlay ??= new DevStatsOverlay(
|
||||||
|
this.canvas.parentElement ?? document.body
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.devStatsOverlay?.destroy();
|
||||||
|
this.devStatsOverlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
private updateAccentColor(color: RgbColor): void {
|
private updateAccentColor(color: RgbColor): void {
|
||||||
const accentColor = rgbColorToCss(color);
|
const accentColor = rgbColorToCss(color);
|
||||||
if (this.previousAccentColor === accentColor) {
|
if (this.previousAccentColor === accentColor) {
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,26 @@ interface SimulationFramePipelines {
|
||||||
eraserAgentPipeline: EraserAgentPipeline;
|
eraserAgentPipeline: EraserAgentPipeline;
|
||||||
eraserTexturePipeline: EraserTexturePipeline;
|
eraserTexturePipeline: EraserTexturePipeline;
|
||||||
diffusionPipeline: DiffusionPipeline;
|
diffusionPipeline: DiffusionPipeline;
|
||||||
brushEffectDiffusionPipeline: DiffusionPipeline;
|
|
||||||
renderPipeline: RenderPipeline;
|
renderPipeline: RenderPipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SimulationFrameRenderer {
|
export class SimulationFrameRenderer {
|
||||||
|
private static readonly SOURCE_ACTIVE_FRAMES_AFTER_WRITE = 600;
|
||||||
|
|
||||||
|
private sourceActiveFramesRemaining = 0;
|
||||||
|
private sourceMapsCleared = true;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly device: GPUDevice,
|
private readonly device: GPUDevice,
|
||||||
private readonly textures: SimulationTextures,
|
private readonly textures: SimulationTextures,
|
||||||
private readonly pipelines: SimulationFramePipelines
|
private readonly pipelines: SimulationFramePipelines
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public resetSourceMapActivity(): void {
|
||||||
|
this.sourceActiveFramesRemaining = 0;
|
||||||
|
this.sourceMapsCleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
public execute(
|
public execute(
|
||||||
isErasing: boolean,
|
isErasing: boolean,
|
||||||
canvasReadbackRequest?: CanvasReadbackRequest | null
|
canvasReadbackRequest?: CanvasReadbackRequest | null
|
||||||
|
|
@ -31,6 +40,7 @@ export class SimulationFrameRenderer {
|
||||||
const commandEncoder = this.device.createCommandEncoder();
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
|
||||||
this.textures.copyTrailMapAToB(commandEncoder);
|
this.textures.copyTrailMapAToB(commandEncoder);
|
||||||
|
let wroteSourceMap = false;
|
||||||
if (isErasing) {
|
if (isErasing) {
|
||||||
if (this.pipelines.eraserAgentPipeline.hasActiveMask()) {
|
if (this.pipelines.eraserAgentPipeline.hasActiveMask()) {
|
||||||
const eraserMask = this.textures.eraserMask.getTextureView();
|
const eraserMask = this.textures.eraserMask.getTextureView();
|
||||||
|
|
@ -38,18 +48,29 @@ export class SimulationFrameRenderer {
|
||||||
commandEncoder,
|
commandEncoder,
|
||||||
eraserMask,
|
eraserMask,
|
||||||
this.textures.sourceMapA.getTextureView(),
|
this.textures.sourceMapA.getTextureView(),
|
||||||
this.textures.influenceMapA.getTextureView(),
|
|
||||||
this.textures.trailMapB.getTextureView()
|
this.textures.trailMapB.getTextureView()
|
||||||
);
|
);
|
||||||
this.pipelines.eraserAgentPipeline.execute(commandEncoder, eraserMask);
|
this.pipelines.eraserAgentPipeline.execute(commandEncoder, eraserMask);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.pipelines.brushPipeline.executeMultiTarget(
|
wroteSourceMap = this.pipelines.brushPipeline.executeMultiTarget(
|
||||||
commandEncoder,
|
commandEncoder,
|
||||||
this.textures.sourceMapA.getTextureView(),
|
this.textures.sourceMapA.getTextureView()
|
||||||
this.textures.influenceMapA.getTextureView()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wroteSourceMap) {
|
||||||
|
this.sourceActiveFramesRemaining =
|
||||||
|
SimulationFrameRenderer.SOURCE_ACTIVE_FRAMES_AFTER_WRITE;
|
||||||
|
this.sourceMapsCleared = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSourceMap = this.sourceActiveFramesRemaining > 0;
|
||||||
|
if (!useSourceMap && !this.sourceMapsCleared) {
|
||||||
|
this.textures.clearSourceMaps(commandEncoder);
|
||||||
|
this.sourceMapsCleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.pipelines.agentPipeline.execute(
|
this.pipelines.agentPipeline.execute(
|
||||||
commandEncoder,
|
commandEncoder,
|
||||||
this.textures.trailMapA.getTextureView(),
|
this.textures.trailMapA.getTextureView(),
|
||||||
|
|
@ -64,28 +85,24 @@ export class SimulationFrameRenderer {
|
||||||
const canvasTexture = this.pipelines.renderPipeline.execute(
|
const canvasTexture = this.pipelines.renderPipeline.execute(
|
||||||
commandEncoder,
|
commandEncoder,
|
||||||
this.textures.trailMapA.getTextureView(),
|
this.textures.trailMapA.getTextureView(),
|
||||||
this.textures.sourceMapA.getTextureView()
|
this.textures.sourceMapA.getTextureView(),
|
||||||
|
useSourceMap
|
||||||
);
|
);
|
||||||
canvasReadbackRequest?.encode(commandEncoder, canvasTexture);
|
canvasReadbackRequest?.encode(commandEncoder, canvasTexture);
|
||||||
|
|
||||||
|
if (useSourceMap) {
|
||||||
|
this.pipelines.diffusionPipeline.execute(
|
||||||
|
commandEncoder,
|
||||||
|
this.textures.sourceMapA.getTextureView(),
|
||||||
|
this.textures.sourceMapB.getTextureView(),
|
||||||
|
this.textures.sourceMapB.getSize()
|
||||||
|
);
|
||||||
|
}
|
||||||
this.device.queue.submit([commandEncoder.finish()]);
|
this.device.queue.submit([commandEncoder.finish()]);
|
||||||
canvasReadbackRequest?.afterSubmit();
|
canvasReadbackRequest?.afterSubmit();
|
||||||
|
if (useSourceMap) {
|
||||||
const postRenderCommandEncoder = this.device.createCommandEncoder();
|
this.textures.swapSourceMaps();
|
||||||
this.pipelines.diffusionPipeline.execute(
|
this.sourceActiveFramesRemaining -= 1;
|
||||||
postRenderCommandEncoder,
|
}
|
||||||
this.textures.sourceMapA.getTextureView(),
|
|
||||||
this.textures.sourceMapB.getTextureView(),
|
|
||||||
this.textures.sourceMapB.getSize()
|
|
||||||
);
|
|
||||||
this.pipelines.brushEffectDiffusionPipeline.execute(
|
|
||||||
postRenderCommandEncoder,
|
|
||||||
this.textures.influenceMapA.getTextureView(),
|
|
||||||
this.textures.influenceMapB.getTextureView(),
|
|
||||||
this.textures.influenceMapB.getSize()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.device.queue.submit([postRenderCommandEncoder.finish()]);
|
|
||||||
this.textures.swapBrushEffectMaps();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ export class SimulationTextures {
|
||||||
public readonly trailMapB: ResizableTexture;
|
public readonly trailMapB: ResizableTexture;
|
||||||
public sourceMapA: ResizableTexture;
|
public sourceMapA: ResizableTexture;
|
||||||
public sourceMapB: ResizableTexture;
|
public sourceMapB: ResizableTexture;
|
||||||
public influenceMapA: ResizableTexture;
|
|
||||||
public influenceMapB: ResizableTexture;
|
|
||||||
public eraserMask: ResizableTexture;
|
public eraserMask: ResizableTexture;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
|
@ -20,8 +18,6 @@ export class SimulationTextures {
|
||||||
this.trailMapB = this.createTexture(canvasSize);
|
this.trailMapB = this.createTexture(canvasSize);
|
||||||
this.sourceMapA = this.createTexture(canvasSize);
|
this.sourceMapA = this.createTexture(canvasSize);
|
||||||
this.sourceMapB = this.createTexture(canvasSize);
|
this.sourceMapB = this.createTexture(canvasSize);
|
||||||
this.influenceMapA = this.createTexture(canvasSize);
|
|
||||||
this.influenceMapB = this.createTexture(canvasSize);
|
|
||||||
this.eraserMask = this.createEraserMask(canvasSize);
|
this.eraserMask = this.createEraserMask(canvasSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,8 +32,6 @@ export class SimulationTextures {
|
||||||
this.trailMapB.resize(nextSize);
|
this.trailMapB.resize(nextSize);
|
||||||
this.sourceMapA.resize(nextSize);
|
this.sourceMapA.resize(nextSize);
|
||||||
this.sourceMapB.resize(nextSize);
|
this.sourceMapB.resize(nextSize);
|
||||||
this.influenceMapA.resize(nextSize);
|
|
||||||
this.influenceMapB.resize(nextSize);
|
|
||||||
this.eraserMask.resize(nextSize);
|
this.eraserMask.resize(nextSize);
|
||||||
|
|
||||||
return scale;
|
return scale;
|
||||||
|
|
@ -50,8 +44,6 @@ export class SimulationTextures {
|
||||||
this.trailMapB,
|
this.trailMapB,
|
||||||
this.sourceMapA,
|
this.sourceMapA,
|
||||||
this.sourceMapB,
|
this.sourceMapB,
|
||||||
this.influenceMapA,
|
|
||||||
this.influenceMapB,
|
|
||||||
this.eraserMask,
|
this.eraserMask,
|
||||||
].forEach((texture) => {
|
].forEach((texture) => {
|
||||||
const passEncoder = commandEncoder.beginRenderPass({
|
const passEncoder = commandEncoder.beginRenderPass({
|
||||||
|
|
@ -79,9 +71,20 @@ export class SimulationTextures {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public swapBrushEffectMaps(): void {
|
public clearSourceMaps(commandEncoder: GPUCommandEncoder): void {
|
||||||
|
const passEncoder = commandEncoder.beginRenderPass({
|
||||||
|
colorAttachments: [this.sourceMapA, this.sourceMapB].map((texture) => ({
|
||||||
|
view: texture.getTextureView(),
|
||||||
|
clearValue: appConfig.simulation.clearColor,
|
||||||
|
loadOp: 'clear',
|
||||||
|
storeOp: 'store',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
passEncoder.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public swapSourceMaps(): void {
|
||||||
[this.sourceMapA, this.sourceMapB] = [this.sourceMapB, this.sourceMapA];
|
[this.sourceMapA, this.sourceMapB] = [this.sourceMapB, this.sourceMapA];
|
||||||
[this.influenceMapA, this.influenceMapB] = [this.influenceMapB, this.influenceMapA];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
|
|
@ -89,8 +92,6 @@ export class SimulationTextures {
|
||||||
this.trailMapB.destroy();
|
this.trailMapB.destroy();
|
||||||
this.sourceMapA.destroy();
|
this.sourceMapA.destroy();
|
||||||
this.sourceMapB.destroy();
|
this.sourceMapB.destroy();
|
||||||
this.influenceMapA.destroy();
|
|
||||||
this.influenceMapB.destroy();
|
|
||||||
this.eraserMask.destroy();
|
this.eraserMask.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const getRelativeLuminance = (red: number, green: number, blue: number): number
|
||||||
appConfig.toolbar.contrast.luminanceGreenWeight * getLinearChannel(green) +
|
appConfig.toolbar.contrast.luminanceGreenWeight * getLinearChannel(green) +
|
||||||
appConfig.toolbar.contrast.luminanceBlueWeight * getLinearChannel(blue);
|
appConfig.toolbar.contrast.luminanceBlueWeight * getLinearChannel(blue);
|
||||||
|
|
||||||
export const getToolbarContrastMetrics = (
|
const getToolbarContrastMetrics = (
|
||||||
pixels: Uint8Array,
|
pixels: Uint8Array,
|
||||||
sampleCount: number,
|
sampleCount: number,
|
||||||
isBgra: boolean
|
isBgra: boolean
|
||||||
|
|
|
||||||
29
src/index.ts
29
src/index.ts
|
|
@ -16,7 +16,7 @@ import { CollapsiblePanelAnimator } from './page/collapsible-panel-animator';
|
||||||
import { ConfigPane } from './page/config-pane';
|
import { ConfigPane } from './page/config-pane';
|
||||||
import { FullScreenHandler } from './page/full-screen-handler';
|
import { FullScreenHandler } from './page/full-screen-handler';
|
||||||
import { MenuHider } from './page/menu-hider';
|
import { MenuHider } from './page/menu-hider';
|
||||||
import { activeVibe, applyVibeSettings, resetSettings, settings } from './settings';
|
import { activeVibe, applyVibeSettings, settings } from './settings';
|
||||||
import { readBrowserStorage, writeBrowserStorage } from './utils/browser-storage';
|
import { readBrowserStorage, writeBrowserStorage } from './utils/browser-storage';
|
||||||
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
|
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
|
||||||
import { queryRequiredElement, queryRequiredElements } from './utils/dom';
|
import { queryRequiredElement, queryRequiredElements } from './utils/dom';
|
||||||
|
|
@ -148,7 +148,6 @@ const LOADING_MESSAGES = {
|
||||||
const VIBE_CHANGE_SOURCES = {
|
const VIBE_CHANGE_SOURCES = {
|
||||||
nextButton: 'next-button',
|
nextButton: 'next-button',
|
||||||
previousButton: 'previous-button',
|
previousButton: 'previous-button',
|
||||||
settings: 'settings',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const clampEraserSize = (value: number): number => {
|
const clampEraserSize = (value: number): number => {
|
||||||
|
|
@ -432,6 +431,7 @@ const main = async () => {
|
||||||
initAnalytics();
|
initAnalytics();
|
||||||
|
|
||||||
let shouldStop = false;
|
let shouldStop = false;
|
||||||
|
let hasStarted = false;
|
||||||
let game: GameLoop | null = null;
|
let game: GameLoop | null = null;
|
||||||
let configPane: ConfigPane | null = null;
|
let configPane: ConfigPane | null = null;
|
||||||
|
|
||||||
|
|
@ -477,6 +477,7 @@ const main = async () => {
|
||||||
|
|
||||||
const startAudioFromUserGesture = (event: Event) => {
|
const startAudioFromUserGesture = (event: Event) => {
|
||||||
if (
|
if (
|
||||||
|
!hasStarted ||
|
||||||
isAudioMuted ||
|
isAudioMuted ||
|
||||||
(event.target instanceof Node && elements.startButton.contains(event.target)) ||
|
(event.target instanceof Node && elements.startButton.contains(event.target)) ||
|
||||||
(event.target instanceof Node && elements.soundButton.contains(event.target))
|
(event.target instanceof Node && elements.soundButton.contains(event.target))
|
||||||
|
|
@ -652,28 +653,6 @@ const main = async () => {
|
||||||
},
|
},
|
||||||
onOpenChange: () => undefined,
|
onOpenChange: () => undefined,
|
||||||
onRuntimeChange: syncRuntimeUi,
|
onRuntimeChange: syncRuntimeUi,
|
||||||
onRuntimeReset: () => {
|
|
||||||
resetSettings();
|
|
||||||
game?.onVibeChanged();
|
|
||||||
syncRuntimeUi();
|
|
||||||
},
|
|
||||||
onRestart: () => game?.destroy(),
|
|
||||||
onVibeChange: (vibeId) => {
|
|
||||||
const vibe = VIBE_PRESETS.find((candidate) => candidate.id === vibeId);
|
|
||||||
if (!vibe) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activePreset = applyVibeSettings(vibe);
|
|
||||||
trackVibeChange({
|
|
||||||
vibeId: activePreset.id,
|
|
||||||
vibeName: activePreset.name,
|
|
||||||
source: VIBE_CHANGE_SOURCES.settings,
|
|
||||||
});
|
|
||||||
game?.onVibeChanged();
|
|
||||||
syncRuntimeUi();
|
|
||||||
game?.playVibeChangeAudio(false);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
infoPageHandler.onOpen = configPane.close.bind(configPane);
|
infoPageHandler.onOpen = configPane.close.bind(configPane);
|
||||||
await fontsReady;
|
await fontsReady;
|
||||||
|
|
@ -702,6 +681,7 @@ const main = async () => {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
elements.startButton.removeEventListener(DOM_EVENTS.click, onClick);
|
elements.startButton.removeEventListener(DOM_EVENTS.click, onClick);
|
||||||
|
hasStarted = true;
|
||||||
game?.startAudio(true);
|
game?.startAudio(true);
|
||||||
trackStart();
|
trackStart();
|
||||||
elements.splash.hidden = true;
|
elements.splash.hidden = true;
|
||||||
|
|
@ -731,6 +711,7 @@ const main = async () => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
game.attachPointerInput();
|
||||||
await game.start();
|
await game.start();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,32 @@
|
||||||
import type { BindingParams, FolderApi } from '@tweakpane/core';
|
import type { BindingParams, FolderApi } from '@tweakpane/core';
|
||||||
import { Pane } from 'tweakpane';
|
import { Pane } from 'tweakpane';
|
||||||
|
|
||||||
|
import type { GardenAudioVibeSettings } from '../audio/garden-audio-config';
|
||||||
import {
|
import {
|
||||||
appConfig,
|
appConfig,
|
||||||
type GardenRuntimeSettings,
|
type GardenRuntimeSettings,
|
||||||
type NumberControlConfig,
|
type NumberControlConfig,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { activeVibe, settings } from '../settings';
|
import { activeVibe, settings } from '../settings';
|
||||||
import { rgbColorToCss } from '../utils/rgb-color';
|
import {
|
||||||
import { isVibeId, VIBE_PRESETS, type VibeId } from '../vibes';
|
hexColorToRgbColor,
|
||||||
|
rgbColorToCss,
|
||||||
|
rgbColorToHex,
|
||||||
|
type RgbColor,
|
||||||
|
} from '../utils/rgb-color';
|
||||||
|
|
||||||
type PaneContainer = Pick<FolderApi, 'addBinding' | 'addButton' | 'addFolder'>;
|
type PaneContainer = Pick<FolderApi, 'addBinding' | 'addButton' | 'addFolder'>;
|
||||||
type ColorReactionKey = (typeof colorReactionRows)[number]['keys'][number];
|
type ColorReactionKey = (typeof colorReactionRows)[number]['keys'][number];
|
||||||
|
type RuntimeControlKey = keyof GardenRuntimeSettings & string;
|
||||||
|
type VibeColorKey = 'color1' | 'color2' | 'color3' | 'backgroundColor';
|
||||||
|
type VibeNumberKey = keyof GardenAudioVibeSettings;
|
||||||
|
|
||||||
|
interface PaneState extends GardenAudioVibeSettings {
|
||||||
|
backgroundColor: string;
|
||||||
|
color1: string;
|
||||||
|
color2: string;
|
||||||
|
color3: string;
|
||||||
|
}
|
||||||
|
|
||||||
const COLOR_REACTION_LABELS = ['1', '2', '3'] as const;
|
const COLOR_REACTION_LABELS = ['1', '2', '3'] as const;
|
||||||
|
|
||||||
|
|
@ -33,37 +48,54 @@ const colorReactionRows = [
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const colorReactionKeySet = new Set<string>(
|
const brushControlKeys = [
|
||||||
colorReactionRows.flatMap((row) => [...row.keys])
|
'brushSize',
|
||||||
);
|
'spawnPerPixel',
|
||||||
|
'brushSizeVariation',
|
||||||
|
'diffusionRateBrush',
|
||||||
|
] satisfies Array<RuntimeControlKey>;
|
||||||
|
|
||||||
const isColorReactionKey = (key: string): key is ColorReactionKey =>
|
const agentControlKeys = [
|
||||||
colorReactionKeySet.has(key);
|
'sensorOffsetDistance',
|
||||||
|
'moveSpeed',
|
||||||
|
'turnSpeed',
|
||||||
|
'individualTrailWeight',
|
||||||
|
'decayRateTrails',
|
||||||
|
] satisfies Array<RuntimeControlKey>;
|
||||||
|
|
||||||
|
const lookControlKeys = [
|
||||||
|
'clarity',
|
||||||
|
'backgroundGrainStrength',
|
||||||
|
] satisfies Array<RuntimeControlKey>;
|
||||||
|
|
||||||
|
const performanceControlKeys = [
|
||||||
|
'maxAgentCount',
|
||||||
|
'internalRenderAreaMegapixels',
|
||||||
|
] satisfies Array<RuntimeControlKey>;
|
||||||
|
|
||||||
|
const MUSIC_CONTROLS: ReadonlyArray<{
|
||||||
|
key: VibeNumberKey;
|
||||||
|
label: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
}> = [
|
||||||
|
{ key: 'idleIntensity', label: 'idle intensity', min: 0, max: 0.8, step: 0.01 },
|
||||||
|
{ key: 'bpm', label: 'bpm', min: 48, max: 150, step: 1 },
|
||||||
|
{ key: 'rampUpIntensity', label: 'ramp up intensity', min: 0, max: 2, step: 0.01 },
|
||||||
|
{ key: 'rampUpTime', label: 'ramp up time', min: 0.01, max: 0.4, step: 0.01 },
|
||||||
|
{ key: 'noteLength', label: 'note length', min: 0.1, max: 1.8, step: 0.01 },
|
||||||
|
{ key: 'notePitchOffset', label: 'higher / lower notes', min: -12, max: 12, step: 1 },
|
||||||
|
{ key: 'brightness', label: 'brightness', min: 0.5, max: 1.5, step: 0.01 },
|
||||||
|
];
|
||||||
|
|
||||||
interface ConfigPaneOptions {
|
interface ConfigPaneOptions {
|
||||||
onConfigChange: () => void;
|
onConfigChange: () => void;
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
onRestart: () => void;
|
|
||||||
onRuntimeChange: () => void;
|
onRuntimeChange: () => void;
|
||||||
onRuntimeReset: () => void;
|
|
||||||
onVibeChange: (vibeId: VibeId) => void;
|
|
||||||
settingsButton: HTMLButtonElement;
|
settingsButton: HTMLButtonElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
typeof value === 'object' && value !== null;
|
|
||||||
|
|
||||||
const isBindablePrimitive = (value: unknown): value is boolean | number | string =>
|
|
||||||
['boolean', 'number', 'string'].includes(typeof value);
|
|
||||||
|
|
||||||
const toLabel = (value: string): string =>
|
|
||||||
value
|
|
||||||
.replace(/\[(\d+)\]/g, ' $1')
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/[-_]/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
const normalizeNumber = (value: number, config: NumberControlConfig): number => {
|
const normalizeNumber = (value: number, config: NumberControlConfig): number => {
|
||||||
if (config.options) {
|
if (config.options) {
|
||||||
const optionValues = Object.values(config.options);
|
const optionValues = Object.values(config.options);
|
||||||
|
|
@ -81,12 +113,9 @@ const normalizeNumber = (value: number, config: NumberControlConfig): number =>
|
||||||
return config.integer ? Math.round(clampedValue) : clampedValue;
|
return config.integer ? Math.round(clampedValue) : clampedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNumberBindingParams = (
|
const getNumberBindingParams = (config: NumberControlConfig): BindingParams => {
|
||||||
key: keyof GardenRuntimeSettings & string,
|
|
||||||
config: NumberControlConfig
|
|
||||||
): BindingParams => {
|
|
||||||
const params: BindingParams = {
|
const params: BindingParams = {
|
||||||
label: config.label ?? toLabel(key),
|
label: config.label,
|
||||||
options: config.options,
|
options: config.options,
|
||||||
step: config.step,
|
step: config.step,
|
||||||
};
|
};
|
||||||
|
|
@ -107,8 +136,12 @@ export class ConfigPane {
|
||||||
colorIndex: number;
|
colorIndex: number;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
}> = [];
|
}> = [];
|
||||||
private readonly state: { activeVibeId: VibeId } = {
|
private readonly state: PaneState = {
|
||||||
activeVibeId: activeVibe.id,
|
backgroundColor: rgbColorToHex(activeVibe.backgroundColor),
|
||||||
|
color1: rgbColorToHex(activeVibe.colors[0]),
|
||||||
|
color2: rgbColorToHex(activeVibe.colors[1]),
|
||||||
|
color3: rgbColorToHex(activeVibe.colors[2]),
|
||||||
|
...activeVibe.audio,
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(private readonly options: ConfigPaneOptions) {
|
public constructor(private readonly options: ConfigPaneOptions) {
|
||||||
|
|
@ -142,12 +175,7 @@ export class ConfigPane {
|
||||||
|
|
||||||
this.options.settingsButton.addEventListener('click', this.toggle);
|
this.options.settingsButton.addEventListener('click', this.toggle);
|
||||||
|
|
||||||
const tabs = this.pane.addTab({
|
this.setUpTuningPane(this.pane);
|
||||||
pages: [{ title: 'Runtime' }, { title: 'Config' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setUpRuntimeTab(tabs.pages[0]);
|
|
||||||
this.setUpConfigTab(tabs.pages[1]);
|
|
||||||
this.syncOpenState();
|
this.syncOpenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +184,7 @@ export class ConfigPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public refresh(): void {
|
public refresh(): void {
|
||||||
this.state.activeVibeId = activeVibe.id;
|
this.syncVibeState();
|
||||||
this.pane.refresh();
|
this.pane.refresh();
|
||||||
this.syncColorReactionMatrix();
|
this.syncColorReactionMatrix();
|
||||||
this.syncOpenState();
|
this.syncOpenState();
|
||||||
|
|
@ -176,73 +204,116 @@ export class ConfigPane {
|
||||||
this.syncOpenState();
|
this.syncOpenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUpRuntimeTab(container: PaneContainer): void {
|
private setUpTuningPane(container: PaneContainer): void {
|
||||||
|
this.setUpVibeSection(container);
|
||||||
|
this.addRuntimeSection(container, 'Brush', brushControlKeys, true);
|
||||||
|
this.addRuntimeSection(container, 'Agents', agentControlKeys, true);
|
||||||
|
this.addColorReactionMatrix(container);
|
||||||
|
this.addRuntimeSection(container, 'Look', lookControlKeys, true);
|
||||||
|
const performanceFolder = this.addRuntimeSection(
|
||||||
|
container,
|
||||||
|
'Performance',
|
||||||
|
performanceControlKeys,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this.addFpsOverlayBinding(performanceFolder);
|
||||||
|
this.setUpMusicSection(container);
|
||||||
|
this.syncColorReactionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setUpVibeSection(container: PaneContainer): void {
|
||||||
|
const folder = container.addFolder({
|
||||||
|
title: 'Vibe',
|
||||||
|
expanded: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addColorBinding(folder, 'color1', 'colour 1', (color) => {
|
||||||
|
activeVibe.colors[0] = color;
|
||||||
|
});
|
||||||
|
this.addColorBinding(folder, 'color2', 'colour 2', (color) => {
|
||||||
|
activeVibe.colors[1] = color;
|
||||||
|
});
|
||||||
|
this.addColorBinding(folder, 'color3', 'colour 3', (color) => {
|
||||||
|
activeVibe.colors[2] = color;
|
||||||
|
});
|
||||||
|
this.addColorBinding(folder, 'backgroundColor', 'overlay / background', (color) => {
|
||||||
|
activeVibe.backgroundColor = color;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
folder
|
||||||
|
.addButton({ title: 'Copy vibe preset' })
|
||||||
|
.on('click', () => void this.copyVibePresetToClipboard());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addColorBinding(
|
||||||
|
container: PaneContainer,
|
||||||
|
key: VibeColorKey,
|
||||||
|
label: string,
|
||||||
|
updateColor: (color: RgbColor) => void
|
||||||
|
): void {
|
||||||
container
|
container
|
||||||
.addBinding(this.state, 'activeVibeId', {
|
.addBinding(this.state, key, {
|
||||||
label: 'active vibe',
|
label,
|
||||||
options: Object.fromEntries(
|
view: 'color',
|
||||||
VIBE_PRESETS.map((vibe) => [vibe.name, vibe.id])
|
} as BindingParams)
|
||||||
) as Record<string, VibeId>,
|
|
||||||
})
|
|
||||||
.on('change', ({ value }) => {
|
.on('change', ({ value }) => {
|
||||||
if (!isVibeId(value)) {
|
const color = hexColorToRgbColor(String(value));
|
||||||
this.refresh();
|
if (!color) {
|
||||||
|
this.syncVibeState();
|
||||||
|
this.pane.refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.options.onVibeChange(value);
|
|
||||||
this.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
container.addButton({ title: 'Reset runtime settings' }).on('click', () => {
|
updateColor(color);
|
||||||
this.options.onRuntimeReset();
|
this.syncColorReactionMatrix();
|
||||||
this.refresh();
|
this.options.onConfigChange();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRuntimeSection(
|
||||||
|
container: PaneContainer,
|
||||||
|
title: string,
|
||||||
|
keys: ReadonlyArray<RuntimeControlKey>,
|
||||||
|
expanded: boolean
|
||||||
|
): PaneContainer {
|
||||||
|
const folder = container.addFolder({ title, expanded });
|
||||||
|
keys.forEach((key) => this.addRuntimeBinding(folder, key));
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRuntimeBinding(container: PaneContainer, key: RuntimeControlKey): void {
|
||||||
|
const config = appConfig.runtimeSettings.controls[key];
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings[key] = normalizeNumber(settings[key], config);
|
||||||
|
|
||||||
container
|
container
|
||||||
.addButton({
|
.addBinding(settings, key, getNumberBindingParams(config))
|
||||||
title: 'Restart simulation',
|
.on('change', () => {
|
||||||
})
|
const nextValue = normalizeNumber(settings[key], config);
|
||||||
.on('click', () => this.options.onRestart());
|
if (nextValue !== settings[key]) {
|
||||||
|
settings[key] = nextValue;
|
||||||
const folders = new Map<string, PaneContainer>();
|
this.pane.refresh();
|
||||||
let hasAddedColorReactionMatrix = false;
|
|
||||||
Object.entries(appConfig.runtimeSettings.controls).forEach(([key, config]) => {
|
|
||||||
const settingKey = key as keyof GardenRuntimeSettings & string;
|
|
||||||
settings[settingKey] = normalizeNumber(settings[settingKey], config);
|
|
||||||
|
|
||||||
if (isColorReactionKey(key)) {
|
|
||||||
if (!hasAddedColorReactionMatrix) {
|
|
||||||
this.addColorReactionMatrix(container);
|
|
||||||
hasAddedColorReactionMatrix = true;
|
|
||||||
}
|
}
|
||||||
return;
|
this.options.onRuntimeChange();
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const folder =
|
private addFpsOverlayBinding(container: PaneContainer): void {
|
||||||
folders.get(config.folder) ??
|
container
|
||||||
container.addFolder({
|
.addBinding(appConfig.tuningPane, 'showFpsOverlay', {
|
||||||
title: config.folder,
|
label: 'FPS overlay',
|
||||||
expanded: config.folder !== 'Runtime',
|
})
|
||||||
});
|
.on('change', () => this.options.onConfigChange());
|
||||||
folders.set(config.folder, folder);
|
|
||||||
|
|
||||||
folder
|
|
||||||
.addBinding(settings, settingKey, getNumberBindingParams(settingKey, config))
|
|
||||||
.on('change', () => {
|
|
||||||
const nextValue = normalizeNumber(settings[settingKey], config);
|
|
||||||
if (nextValue !== settings[settingKey]) {
|
|
||||||
settings[settingKey] = nextValue;
|
|
||||||
this.pane.refresh();
|
|
||||||
}
|
|
||||||
this.options.onRuntimeChange();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.syncColorReactionMatrix();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addColorReactionMatrix(container: PaneContainer): void {
|
private addColorReactionMatrix(container: PaneContainer): void {
|
||||||
const folder = container.addFolder({
|
const folder = container.addFolder({
|
||||||
title: 'Color Reactions',
|
title: 'Follow / Ignore / Avoid',
|
||||||
expanded: true,
|
expanded: true,
|
||||||
});
|
});
|
||||||
folder.element.classList.add('color-reaction-folder');
|
folder.element.classList.add('color-reaction-folder');
|
||||||
|
|
@ -319,6 +390,10 @@ export class ConfigPane {
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = appConfig.runtimeSettings.controls[key];
|
const config = appConfig.runtimeSettings.controls[key];
|
||||||
|
if (!config) {
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(config.options ?? {}).forEach(([label, value]) => {
|
Object.entries(config.options ?? {}).forEach(([label, value]) => {
|
||||||
const option = doc.createElement('option');
|
const option = doc.createElement('option');
|
||||||
option.value = String(value);
|
option.value = String(value);
|
||||||
|
|
@ -341,6 +416,10 @@ export class ConfigPane {
|
||||||
private syncColorReactionMatrix(): void {
|
private syncColorReactionMatrix(): void {
|
||||||
this.colorReactionSelects.forEach((select, key) => {
|
this.colorReactionSelects.forEach((select, key) => {
|
||||||
const config = appConfig.runtimeSettings.controls[key];
|
const config = appConfig.runtimeSettings.controls[key];
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
settings[key] = normalizeNumber(settings[key], config);
|
settings[key] = normalizeNumber(settings[key], config);
|
||||||
select.value = String(settings[key]);
|
select.value = String(settings[key]);
|
||||||
});
|
});
|
||||||
|
|
@ -350,96 +429,66 @@ export class ConfigPane {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setUpConfigTab(container: PaneContainer): void {
|
private setUpMusicSection(container: PaneContainer): void {
|
||||||
this.addObjectBindings(
|
const folder = container.addFolder({ title: 'Music', expanded: true });
|
||||||
container,
|
MUSIC_CONTROLS.forEach(({ key, label, min, max, step }) => {
|
||||||
appConfig as unknown as Record<string, unknown>,
|
this.addVibeNumberBinding(folder, key, { folder: 'Music', label, min, max, step });
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private addObjectBindings(
|
|
||||||
container: PaneContainer,
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
path: Array<string>
|
|
||||||
): void {
|
|
||||||
Object.entries(source).forEach(([key, value]) => {
|
|
||||||
if (isBindablePrimitive(value)) {
|
|
||||||
this.addPrimitiveBinding(container, source, key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const folder = container.addFolder({
|
|
||||||
title: toLabel(`${key}[]`),
|
|
||||||
expanded: path.length < appConfig.tuningPane.expandedDepth,
|
|
||||||
});
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
if (isBindablePrimitive(item)) {
|
|
||||||
this.addPrimitiveBinding(
|
|
||||||
folder,
|
|
||||||
value as unknown as Record<string, unknown>,
|
|
||||||
`${index}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(item)) {
|
|
||||||
this.addObjectBindings(
|
|
||||||
folder.addFolder({
|
|
||||||
title: `[${index}]`,
|
|
||||||
expanded: false,
|
|
||||||
}),
|
|
||||||
item,
|
|
||||||
[...path, key, String(index)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
|
||||||
this.addObjectBindings(
|
|
||||||
container.addFolder({
|
|
||||||
title: toLabel(key),
|
|
||||||
expanded: path.length < appConfig.tuningPane.expandedDepth,
|
|
||||||
}),
|
|
||||||
value,
|
|
||||||
[...path, key]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private addPrimitiveBinding(
|
private addVibeNumberBinding(
|
||||||
container: PaneContainer,
|
container: PaneContainer,
|
||||||
source: Record<string, unknown>,
|
key: VibeNumberKey,
|
||||||
key: string
|
config: NumberControlConfig
|
||||||
): void {
|
): void {
|
||||||
const params: BindingParams = {
|
this.state[key] = normalizeNumber(this.state[key], config);
|
||||||
label: toLabel(key),
|
|
||||||
...(key === 'quality' ? { options: { major: 'major', minor: 'minor' } } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
container
|
container
|
||||||
.addBinding(source, key, params)
|
.addBinding(this.state, key, getNumberBindingParams(config))
|
||||||
.on('change', () => this.options.onConfigChange());
|
.on('change', () => {
|
||||||
|
const nextValue = normalizeNumber(this.state[key], config);
|
||||||
|
if (nextValue !== this.state[key]) {
|
||||||
|
this.state[key] = nextValue;
|
||||||
|
this.pane.refresh();
|
||||||
|
}
|
||||||
|
activeVibe.audio[key] = nextValue;
|
||||||
|
this.options.onConfigChange();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncButton(): void {
|
private async copyVibePresetToClipboard(): Promise<void> {
|
||||||
this.options.settingsButton.classList.toggle('active', this.isOpen);
|
const settingKeys = Object.keys(activeVibe.settings) as Array<
|
||||||
this.options.settingsButton.setAttribute('aria-expanded', String(this.isOpen));
|
keyof typeof activeVibe.settings
|
||||||
this.options.settingsButton.setAttribute(
|
>;
|
||||||
'aria-label',
|
const preset = {
|
||||||
this.isOpen ? 'Hide config overlay' : 'Show config overlay'
|
name: `${activeVibe.name} Copy`,
|
||||||
);
|
colors: activeVibe.colors,
|
||||||
this.options.settingsButton.title = this.isOpen
|
backgroundColor: activeVibe.backgroundColor,
|
||||||
? 'Hide config overlay'
|
settings: Object.fromEntries(settingKeys.map((key) => [key, settings[key]])),
|
||||||
: 'Show config overlay';
|
audio: activeVibe.audio,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify(preset, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not copy vibe preset to clipboard.', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncVibeState(): void {
|
||||||
|
this.state.color1 = rgbColorToHex(activeVibe.colors[0]);
|
||||||
|
this.state.color2 = rgbColorToHex(activeVibe.colors[1]);
|
||||||
|
this.state.color3 = rgbColorToHex(activeVibe.colors[2]);
|
||||||
|
this.state.backgroundColor = rgbColorToHex(activeVibe.backgroundColor);
|
||||||
|
Object.assign(this.state, activeVibe.audio);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncOpenState(): void {
|
private syncOpenState(): void {
|
||||||
this.syncButton();
|
const { settingsButton } = this.options;
|
||||||
|
const label = this.isOpen ? 'Hide config overlay' : 'Show config overlay';
|
||||||
|
settingsButton.classList.toggle('active', this.isOpen);
|
||||||
|
settingsButton.setAttribute('aria-expanded', String(this.isOpen));
|
||||||
|
settingsButton.setAttribute('aria-label', label);
|
||||||
|
settingsButton.title = label;
|
||||||
this.options.onOpenChange?.(this.isOpen);
|
this.options.onOpenChange?.(this.isOpen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,19 @@
|
||||||
export const AGENT_WORKGROUP_SIZE = 64;
|
const AGENT_WORKGROUP_SIZE = 64;
|
||||||
export const AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION = 65_535;
|
const AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION = 65_535;
|
||||||
export const AGENT_MAX_DISPATCHABLE_COUNT = 0xffffffff;
|
export const AGENT_MAX_DISPATCHABLE_COUNT =
|
||||||
|
AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION * AGENT_WORKGROUP_SIZE;
|
||||||
|
|
||||||
export const getAgentDispatchWorkgroups = (agentCount: number): [number, number] => {
|
const getAgentDispatchWorkgroups = (agentCount: number): [number, number] => {
|
||||||
if (!Number.isFinite(agentCount) || agentCount <= 0) {
|
if (!Number.isFinite(agentCount) || agentCount <= 0) {
|
||||||
throw new Error('Agent count must be a positive finite number');
|
throw new Error('Agent count must be a positive finite number');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workgroupCount = Math.ceil(agentCount / AGENT_WORKGROUP_SIZE);
|
const workgroupCount = Math.ceil(agentCount / AGENT_WORKGROUP_SIZE);
|
||||||
if (workgroupCount <= AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
|
if (workgroupCount > AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
|
||||||
return [workgroupCount, 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const workgroupX = Math.min(
|
|
||||||
Math.ceil(Math.sqrt(workgroupCount)),
|
|
||||||
AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION
|
|
||||||
);
|
|
||||||
const workgroupY = Math.ceil(workgroupCount / workgroupX);
|
|
||||||
|
|
||||||
if (workgroupY > AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
|
|
||||||
throw new Error('Agent count exceeds dispatchable workgroup range');
|
throw new Error('Agent count exceeds dispatchable workgroup range');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [workgroupX, workgroupY];
|
return [workgroupCount, 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dispatchAgentWorkgroups = (
|
export const dispatchAgentWorkgroups = (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ struct Counters {
|
||||||
aliveAgentCount: atomic<u32>,
|
aliveAgentCount: atomic<u32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearCompactedTailStride = 4u;
|
||||||
|
|
||||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||||
@group(1) @binding(2) var<storage, read_write> counters: Counters;
|
@group(1) @binding(2) var<storage, read_write> counters: Counters;
|
||||||
@group(1) @binding(3) var<storage, read_write> compactedAgents: array<Agent>;
|
@group(1) @binding(3) var<storage, read_write> compactedAgents: array<Agent>;
|
||||||
|
|
@ -20,10 +22,9 @@ var<workgroup> clearAliveAgentCount: u32;
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn main(
|
fn main(
|
||||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
@builtin(global_invocation_id) global_id: vec3<u32>,
|
||||||
@builtin(local_invocation_id) local_id: vec3<u32>,
|
@builtin(local_invocation_id) local_id: vec3<u32>
|
||||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
|
||||||
) {
|
) {
|
||||||
let id = get_id(global_id, num_workgroups);
|
let id = get_id(global_id);
|
||||||
|
|
||||||
if local_id.x == 0u {
|
if local_id.x == 0u {
|
||||||
atomicStore(&workgroupAliveCount, 0u);
|
atomicStore(&workgroupAliveCount, 0u);
|
||||||
|
|
@ -63,10 +64,9 @@ fn main(
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn clearCompactedTail(
|
fn clearCompactedTail(
|
||||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
@builtin(global_invocation_id) global_id: vec3<u32>,
|
||||||
@builtin(local_invocation_id) local_id: vec3<u32>,
|
@builtin(local_invocation_id) local_id: vec3<u32>
|
||||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
|
||||||
) {
|
) {
|
||||||
let id = get_id(global_id, num_workgroups);
|
let id = get_id(global_id);
|
||||||
|
|
||||||
if local_id.x == 0u {
|
if local_id.x == 0u {
|
||||||
clearAliveAgentCount = atomicLoad(&counters.aliveAgentCount);
|
clearAliveAgentCount = atomicLoad(&counters.aliveAgentCount);
|
||||||
|
|
@ -74,11 +74,11 @@ fn clearCompactedTail(
|
||||||
|
|
||||||
workgroupBarrier();
|
workgroupBarrier();
|
||||||
|
|
||||||
if id >= settings.agentCount {
|
let firstClearId = clearAliveAgentCount + id * clearCompactedTailStride;
|
||||||
return;
|
for (var offset = 0u; offset < clearCompactedTailStride; offset += 1u) {
|
||||||
}
|
let clearId = firstClearId + offset;
|
||||||
|
if clearId < settings.agentCount {
|
||||||
if id >= clearAliveAgentCount {
|
compactedAgents[clearId].colorIndex = -1.0;
|
||||||
compactedAgents[id].colorIndex = -1.0;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
import { smartCompile } from '../../../utils/graphics/smart-compile';
|
import { smartCompile } from '../../../utils/graphics/smart-compile';
|
||||||
import {
|
import { AGENT_MAX_DISPATCHABLE_COUNT, dispatchAgentWorkgroups } from '../agent-dispatch';
|
||||||
AGENT_MAX_DISPATCHABLE_COUNT,
|
|
||||||
dispatchAgentWorkgroups,
|
|
||||||
} from '../agent-dispatch';
|
|
||||||
import compactionShader from './agent-compaction.wgsl?raw';
|
import compactionShader from './agent-compaction.wgsl?raw';
|
||||||
import resizeShader from './agent-resize.wgsl?raw';
|
import resizeShader from './agent-resize.wgsl?raw';
|
||||||
import agentSchema from './agent-schema.wgsl?raw';
|
import agentSchema from './agent-schema.wgsl?raw';
|
||||||
|
|
||||||
export const AGENT_FLOAT_COUNT = 8;
|
export const AGENT_FLOAT_COUNT = 8;
|
||||||
export const AGENT_SIZE_IN_BYTES =
|
const AGENT_SIZE_IN_BYTES = AGENT_FLOAT_COUNT * Float32Array.BYTES_PER_ELEMENT;
|
||||||
AGENT_FLOAT_COUNT * Float32Array.BYTES_PER_ELEMENT;
|
|
||||||
|
|
||||||
export class AgentGenerationPipeline {
|
export class AgentGenerationPipeline {
|
||||||
private static readonly UNIFORM_COUNT = 4;
|
private static readonly UNIFORM_COUNT = 4;
|
||||||
private static readonly COUNTER_COUNT = 1;
|
private static readonly COUNTER_COUNT = 1;
|
||||||
|
private static readonly CLEAR_COMPACTED_TAIL_STRIDE = 4;
|
||||||
|
private static readonly ALLOCATION_GROWTH_FACTOR = 1.25;
|
||||||
|
|
||||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||||
private readonly uniforms: GPUBuffer;
|
private readonly uniforms: GPUBuffer;
|
||||||
|
|
@ -33,21 +31,14 @@ export class AgentGenerationPipeline {
|
||||||
private allocatedMaxAgentCount: number;
|
private allocatedMaxAgentCount: number;
|
||||||
private readonly countersBuffer: GPUBuffer;
|
private readonly countersBuffer: GPUBuffer;
|
||||||
private readonly countersStagingBuffer: GPUBuffer;
|
private readonly countersStagingBuffer: GPUBuffer;
|
||||||
private readonly counterClearValues = new Uint32Array(
|
|
||||||
AgentGenerationPipeline.COUNTER_COUNT
|
|
||||||
);
|
|
||||||
private readonly agentCountUniformValues = new Uint32Array(
|
private readonly agentCountUniformValues = new Uint32Array(
|
||||||
AgentGenerationPipeline.UNIFORM_COUNT
|
AgentGenerationPipeline.UNIFORM_COUNT
|
||||||
);
|
);
|
||||||
private readonly resizeUniformBuffer = new ArrayBuffer(
|
private readonly resizeUniformBuffer = new ArrayBuffer(
|
||||||
AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT
|
AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT
|
||||||
);
|
);
|
||||||
private readonly resizeUniformFloatValues = new Float32Array(
|
private readonly resizeUniformFloatValues = new Float32Array(this.resizeUniformBuffer);
|
||||||
this.resizeUniformBuffer
|
private readonly resizeUniformUintValues = new Uint32Array(this.resizeUniformBuffer);
|
||||||
);
|
|
||||||
private readonly resizeUniformUintValues = new Uint32Array(
|
|
||||||
this.resizeUniformBuffer
|
|
||||||
);
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly device: GPUDevice,
|
private readonly device: GPUDevice,
|
||||||
|
|
@ -173,11 +164,19 @@ export class AgentGenerationPipeline {
|
||||||
requestedMaxAgentCount: number,
|
requestedMaxAgentCount: number,
|
||||||
activeAgentCount: number
|
activeAgentCount: number
|
||||||
): number {
|
): number {
|
||||||
const nextMaxAgentCount = this.clampMaxAgentCount(requestedMaxAgentCount);
|
const requestedClampedMaxAgentCount = this.clampMaxAgentCount(requestedMaxAgentCount);
|
||||||
if (nextMaxAgentCount <= this.allocatedMaxAgentCount) {
|
if (requestedClampedMaxAgentCount <= this.allocatedMaxAgentCount) {
|
||||||
return this.allocatedMaxAgentCount;
|
return this.allocatedMaxAgentCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextMaxAgentCount = this.clampMaxAgentCount(
|
||||||
|
Math.max(
|
||||||
|
requestedClampedMaxAgentCount,
|
||||||
|
Math.ceil(
|
||||||
|
this.allocatedMaxAgentCount * AgentGenerationPipeline.ALLOCATION_GROWTH_FACTOR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
const previousActiveAgentsBuffer = this.activeAgentsBuffer;
|
const previousActiveAgentsBuffer = this.activeAgentsBuffer;
|
||||||
const previousMaxAgentCount = this.allocatedMaxAgentCount;
|
const previousMaxAgentCount = this.allocatedMaxAgentCount;
|
||||||
this.allocatedMaxAgentCount = nextMaxAgentCount;
|
this.allocatedMaxAgentCount = nextMaxAgentCount;
|
||||||
|
|
@ -270,16 +269,19 @@ export class AgentGenerationPipeline {
|
||||||
this.inactiveAgentsBuffer = this.createAgentsBuffer();
|
this.inactiveAgentsBuffer = this.createAgentsBuffer();
|
||||||
|
|
||||||
this.agentCountUniformValues[0] = agentCount;
|
this.agentCountUniformValues[0] = agentCount;
|
||||||
this.device.queue.writeBuffer(this.countersBuffer, 0, this.counterClearValues);
|
|
||||||
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
|
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
|
||||||
|
|
||||||
const commandEncoder = this.device.createCommandEncoder();
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
commandEncoder.clearBuffer(this.countersBuffer, 0, Uint32Array.BYTES_PER_ELEMENT);
|
||||||
const passEncoder = commandEncoder.beginComputePass();
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
passEncoder.setPipeline(this.compactionPipeline);
|
passEncoder.setPipeline(this.compactionPipeline);
|
||||||
passEncoder.setBindGroup(1, this.getBindGroup());
|
passEncoder.setBindGroup(1, this.getBindGroup());
|
||||||
dispatchAgentWorkgroups(passEncoder, agentCount);
|
dispatchAgentWorkgroups(passEncoder, agentCount);
|
||||||
passEncoder.setPipeline(this.clearCompactedTailPipeline);
|
passEncoder.setPipeline(this.clearCompactedTailPipeline);
|
||||||
dispatchAgentWorkgroups(passEncoder, agentCount);
|
dispatchAgentWorkgroups(
|
||||||
|
passEncoder,
|
||||||
|
Math.ceil(agentCount / AgentGenerationPipeline.CLEAR_COMPACTED_TAIL_STRIDE)
|
||||||
|
);
|
||||||
passEncoder.end();
|
passEncoder.end();
|
||||||
|
|
||||||
commandEncoder.copyBufferToBuffer(
|
commandEncoder.copyBufferToBuffer(
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@ struct ResizeSettings {
|
||||||
|
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn main(
|
fn main(
|
||||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
@builtin(global_invocation_id) global_id: vec3<u32>
|
||||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
|
||||||
) {
|
) {
|
||||||
let id = get_id(global_id, num_workgroups);
|
let id = get_id(global_id);
|
||||||
|
|
||||||
if id >= resizeSettings.agentCount {
|
if id >= resizeSettings.agentCount {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ struct Agent {
|
||||||
|
|
||||||
const agentWorkgroupSize = 64u;
|
const agentWorkgroupSize = 64u;
|
||||||
|
|
||||||
fn get_id(global_id: vec3<u32>, num_workgroups: vec3<u32>) -> u32 {
|
fn get_id(global_id: vec3<u32>) -> u32 {
|
||||||
return global_id.x + global_id.y * num_workgroups.x * agentWorkgroupSize;
|
return global_id.x;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,12 @@ export class AgentPipeline {
|
||||||
introNearMoveMultiplier,
|
introNearMoveMultiplier,
|
||||||
introStepStopDistance,
|
introStepStopDistance,
|
||||||
randomTimeScale,
|
randomTimeScale,
|
||||||
|
time,
|
||||||
agentCount,
|
agentCount,
|
||||||
introProgress,
|
introProgress,
|
||||||
}: AgentSettings & {
|
}: AgentSettings & {
|
||||||
deltaTime: number;
|
deltaTime: number;
|
||||||
|
time: number;
|
||||||
agentCount: number;
|
agentCount: number;
|
||||||
introProgress?: number;
|
introProgress?: number;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -117,7 +119,7 @@ export class AgentPipeline {
|
||||||
this.uniformValues[26] = introFarMoveMultiplier;
|
this.uniformValues[26] = introFarMoveMultiplier;
|
||||||
this.uniformValues[27] = introNearMoveMultiplier;
|
this.uniformValues[27] = introNearMoveMultiplier;
|
||||||
this.uniformValues[28] = introStepStopDistance;
|
this.uniformValues[28] = introStepStopDistance;
|
||||||
this.uniformValues[29] = randomTimeScale;
|
this.uniformUintValues[29] = Math.max(0, Math.floor(time * randomTimeScale)) >>> 0;
|
||||||
writeFloat32BufferIfChanged(
|
writeFloat32BufferIfChanged(
|
||||||
this.device,
|
this.device,
|
||||||
this.uniforms,
|
this.uniforms,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ struct Settings {
|
||||||
introFarMoveMultiplier: f32,
|
introFarMoveMultiplier: f32,
|
||||||
introNearMoveMultiplier: f32,
|
introNearMoveMultiplier: f32,
|
||||||
introStepStopDistance: f32,
|
introStepStopDistance: f32,
|
||||||
randomTimeScale: f32,
|
randomTimeSeed: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||||
|
|
@ -37,10 +37,9 @@ struct Settings {
|
||||||
|
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn main(
|
fn main(
|
||||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
@builtin(global_invocation_id) global_id: vec3<u32>
|
||||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
|
||||||
) {
|
) {
|
||||||
let id = get_id(global_id, num_workgroups);
|
let id = get_id(global_id);
|
||||||
|
|
||||||
if id >= settings.agentCount {
|
if id >= settings.agentCount {
|
||||||
return;
|
return;
|
||||||
|
|
@ -65,7 +64,8 @@ fn main(
|
||||||
|
|
||||||
let channelMask = get_channel_mask(colorIndex);
|
let channelMask = get_channel_mask(colorIndex);
|
||||||
let reactionMask = get_reaction_mask(colorIndex);
|
let reactionMask = get_reaction_mask(colorIndex);
|
||||||
let randomSeed = random_seed(id, state.time);
|
let randomSeed = random_seed(id);
|
||||||
|
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
|
||||||
var rotation = 0.0;
|
var rotation = 0.0;
|
||||||
var step = vec2<f32>(0.0, 0.0);
|
var step = vec2<f32>(0.0, 0.0);
|
||||||
|
|
||||||
|
|
@ -108,16 +108,18 @@ fn main(
|
||||||
let randomTurn = random_float(randomSeed);
|
let randomTurn = random_float(randomSeed);
|
||||||
let direction = vec2(cos(angle), sin(angle));
|
let direction = vec2(cos(angle), sin(angle));
|
||||||
|
|
||||||
let forwardSensor = sensor_position(position, direction, settings.sensorOffset);
|
let forwardSensor = sensor_position(position, direction, settings.sensorOffset, maxPosition);
|
||||||
let leftSensor = sensor_position(
|
let leftSensor = sensor_position(
|
||||||
position,
|
position,
|
||||||
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
|
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
|
||||||
settings.sensorOffset
|
settings.sensorOffset,
|
||||||
|
maxPosition
|
||||||
);
|
);
|
||||||
let rightSensor = sensor_position(
|
let rightSensor = sensor_position(
|
||||||
position,
|
position,
|
||||||
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
|
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
|
||||||
settings.sensorOffset
|
settings.sensorOffset,
|
||||||
|
maxPosition
|
||||||
);
|
);
|
||||||
|
|
||||||
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
|
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
|
||||||
|
|
@ -138,7 +140,6 @@ fn main(
|
||||||
step = direction * settings.moveRate;
|
step = direction * settings.moveRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
|
|
||||||
let nextPosition = clamp(position + step, vec2<f32>(0, 0), maxPosition);
|
let nextPosition = clamp(position + step, vec2<f32>(0, 0), maxPosition);
|
||||||
if nextPosition.x == 0 || nextPosition.x == maxPosition.x || nextPosition.y == 0 || nextPosition.y == maxPosition.y {
|
if nextPosition.x == 0 || nextPosition.x == maxPosition.x || nextPosition.y == 0 || nextPosition.y == maxPosition.y {
|
||||||
rotation = 3.14159265359 + random_float(randomSeed + 22695477u) - 0.5;
|
rotation = 3.14159265359 + random_float(randomSeed + 22695477u) - 0.5;
|
||||||
|
|
@ -155,11 +156,16 @@ fn main(
|
||||||
agents[id].position = nextPosition;
|
agents[id].position = nextPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sensor_position(agentPosition: vec2<f32>, direction: vec2<f32>, sensorOffset: f32) -> vec2<i32> {
|
fn sensor_position(
|
||||||
|
agentPosition: vec2<f32>,
|
||||||
|
direction: vec2<f32>,
|
||||||
|
sensorOffset: f32,
|
||||||
|
maxPosition: vec2<f32>
|
||||||
|
) -> vec2<i32> {
|
||||||
return vec2<i32>(clamp(
|
return vec2<i32>(clamp(
|
||||||
agentPosition + direction * sensorOffset,
|
agentPosition + direction * sensorOffset,
|
||||||
vec2<f32>(0, 0),
|
vec2<f32>(0, 0),
|
||||||
state.size - vec2<f32>(1, 1)
|
maxPosition
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,9 +218,8 @@ fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 {
|
||||||
return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle));
|
return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_seed(id: u32, time: f32) -> u32 {
|
fn random_seed(id: u32) -> u32 {
|
||||||
let timeSeed = u32(time * settings.randomTimeScale);
|
return id * 747796405u + settings.randomTimeSeed * 2891336453u;
|
||||||
return id * 747796405u + timeSeed * 2891336453u;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_float(seed: u32) -> f32 {
|
fn random_float(seed: u32) -> f32 {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const getSafePixelRatio = (pixelRatio: number | undefined): number =>
|
||||||
? pixelRatio
|
? pixelRatio
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
export const setBrushUniformValues = (
|
const setBrushUniformValues = (
|
||||||
target: Float32Array,
|
target: Float32Array,
|
||||||
{
|
{
|
||||||
brushSize,
|
brushSize,
|
||||||
|
|
@ -105,7 +105,7 @@ export class BrushPipeline {
|
||||||
});
|
});
|
||||||
|
|
||||||
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
|
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
|
||||||
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 2);
|
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 1);
|
||||||
|
|
||||||
this.uniforms = this.device.createBuffer({
|
this.uniforms = this.device.createBuffer({
|
||||||
size: BrushPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
size: BrushPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||||
|
|
@ -209,12 +209,10 @@ export class BrushPipeline {
|
||||||
|
|
||||||
public executeMultiTarget(
|
public executeMultiTarget(
|
||||||
commandEncoder: GPUCommandEncoder,
|
commandEncoder: GPUCommandEncoder,
|
||||||
sourceMapOut: GPUTextureView,
|
sourceMapOut: GPUTextureView
|
||||||
influenceMapOut: GPUTextureView
|
): boolean {
|
||||||
): void {
|
return this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
|
||||||
this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
|
|
||||||
sourceMapOut,
|
sourceMapOut,
|
||||||
influenceMapOut,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,9 +220,9 @@ export class BrushPipeline {
|
||||||
commandEncoder: GPUCommandEncoder,
|
commandEncoder: GPUCommandEncoder,
|
||||||
pipeline: GPURenderPipeline,
|
pipeline: GPURenderPipeline,
|
||||||
textureViews: Array<GPUTextureView>
|
textureViews: Array<GPUTextureView>
|
||||||
): void {
|
): boolean {
|
||||||
if (this.lineCount === 0) {
|
if (this.lineCount === 0) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||||
|
|
@ -242,6 +240,7 @@ export class BrushPipeline {
|
||||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||||
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
|
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
|
||||||
passEncoder.end();
|
passEncoder.end();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ struct VertexOutput {
|
||||||
|
|
||||||
struct BrushTargets {
|
struct BrushTargets {
|
||||||
@location(0) source: vec4<f32>,
|
@location(0) source: vec4<f32>,
|
||||||
@location(1) influence: vec4<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
|
|
@ -53,6 +52,22 @@ fn fragmentMrt(
|
||||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
||||||
) -> BrushTargets {
|
) -> BrushTargets {
|
||||||
|
let strength = brushStrength(screenPosition, start, direction, inverseLengthSquared);
|
||||||
|
|
||||||
|
if(strength < settings.brushDiscardThreshold) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = brushOutput(strength);
|
||||||
|
return BrushTargets(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn brushStrength(
|
||||||
|
screenPosition: vec2<f32>,
|
||||||
|
start: vec2<f32>,
|
||||||
|
direction: vec2<f32>,
|
||||||
|
inverseLengthSquared: f32
|
||||||
|
) -> f32 {
|
||||||
let distanceSquared = distanceSquaredFromLine(
|
let distanceSquared = distanceSquaredFromLine(
|
||||||
screenPosition,
|
screenPosition,
|
||||||
start,
|
start,
|
||||||
|
|
@ -60,24 +75,9 @@ fn fragmentMrt(
|
||||||
inverseLengthSquared
|
inverseLengthSquared
|
||||||
);
|
);
|
||||||
if distanceSquared > settings.brushGeometryRadiusSquared {
|
if distanceSquared > settings.brushGeometryRadiusSquared {
|
||||||
discard;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let strength = brushStrength(screenPosition, distanceSquared);
|
|
||||||
|
|
||||||
if(strength < settings.brushDiscardThreshold) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
let color = brushOutput(strength);
|
|
||||||
return BrushTargets(color, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn brushStrength(
|
|
||||||
screenPosition: vec2<f32>,
|
|
||||||
distanceSquared: f32
|
|
||||||
) -> f32 {
|
|
||||||
let distance = sqrt(distanceSquared);
|
|
||||||
let coarseNoise = textureSampleLevel(
|
let coarseNoise = textureSampleLevel(
|
||||||
noise,
|
noise,
|
||||||
noiseSampler,
|
noiseSampler,
|
||||||
|
|
@ -85,7 +85,7 @@ fn brushStrength(
|
||||||
0.0
|
0.0
|
||||||
).r;
|
).r;
|
||||||
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation;
|
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation;
|
||||||
let edge = 1.0 - step(radius, distance);
|
let edge = 1.0 - step(radius * radius, distanceSquared);
|
||||||
if edge * max(settings.brushGrainMinStrength, settings.brushGrainMaxStrength) < settings.brushDiscardThreshold {
|
if edge * max(settings.brushGrainMinStrength, settings.brushGrainMaxStrength) < settings.brushDiscardThreshold {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
@ -112,10 +112,6 @@ fn distanceSquaredFromLine(
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
let pa = position - start;
|
let pa = position - start;
|
||||||
|
|
||||||
if inverseLengthSquared <= 0.0 {
|
|
||||||
return dot(pa, pa);
|
|
||||||
}
|
|
||||||
|
|
||||||
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0, 1);
|
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0, 1);
|
||||||
let nearestOffset = pa - direction * q;
|
let nearestOffset = pa - direction * q;
|
||||||
return dot(nearestOffset, nearestOffset);
|
return dot(nearestOffset, nearestOffset);
|
||||||
|
|
@ -140,14 +136,13 @@ fn segment_vertex_position(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
||||||
if index == 0u {
|
let corners = array<vec2<f32>, 6>(
|
||||||
return vec2<f32>(-1.0, 1.0);
|
vec2<f32>(-1.0, 1.0),
|
||||||
}
|
vec2<f32>(-1.0, -1.0),
|
||||||
if index == 1u || index == 3u {
|
vec2<f32>(1.0, 1.0),
|
||||||
return vec2<f32>(-1.0, -1.0);
|
vec2<f32>(-1.0, -1.0),
|
||||||
}
|
vec2<f32>(1.0, 1.0),
|
||||||
if index == 2u || index == 4u {
|
vec2<f32>(1.0, -1.0),
|
||||||
return vec2<f32>(1.0, 1.0);
|
);
|
||||||
}
|
return corners[index];
|
||||||
return vec2<f32>(1.0, -1.0);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const TILE_TEXEL_COUNT = TILE_SIZE_X * TILE_SIZE_Y;
|
||||||
@group(0) @binding(2) var trailMapOut: texture_storage_2d<rgba16float, write>;
|
@group(0) @binding(2) var trailMapOut: texture_storage_2d<rgba16float, write>;
|
||||||
|
|
||||||
var<workgroup> tile: array<vec4<f32>, 324>;
|
var<workgroup> tile: array<vec4<f32>, 324>;
|
||||||
|
var<workgroup> tileTrailStrength: array<f32, 324>;
|
||||||
|
|
||||||
@compute @workgroup_size(16, 16)
|
@compute @workgroup_size(16, 16)
|
||||||
fn main(
|
fn main(
|
||||||
|
|
@ -28,31 +29,32 @@ fn main(
|
||||||
@builtin(workgroup_id) workgroup_id: vec3<u32>
|
@builtin(workgroup_id) workgroup_id: vec3<u32>
|
||||||
) {
|
) {
|
||||||
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
|
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
|
||||||
|
let textureSizeU32 = vec2<u32>(textureSize);
|
||||||
let localLinearIndex = local_id.y * WORKGROUP_SIZE_X + local_id.x;
|
let localLinearIndex = local_id.y * WORKGROUP_SIZE_X + local_id.x;
|
||||||
var tileIndex = localLinearIndex;
|
let workgroupOrigin = workgroup_id.xy * vec2<u32>(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y);
|
||||||
|
let isInteriorTile =
|
||||||
loop {
|
workgroupOrigin.x > 0u &&
|
||||||
if tileIndex >= TILE_TEXEL_COUNT {
|
workgroupOrigin.y > 0u &&
|
||||||
break;
|
workgroupOrigin.x + WORKGROUP_SIZE_X < textureSizeU32.x &&
|
||||||
}
|
workgroupOrigin.y + WORKGROUP_SIZE_Y < textureSizeU32.y;
|
||||||
|
|
||||||
|
for (var tileIndex = localLinearIndex; tileIndex < TILE_TEXEL_COUNT; tileIndex += WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y) {
|
||||||
let tilePosition = vec2<u32>(tileIndex % TILE_SIZE_X, tileIndex / TILE_SIZE_X);
|
let tilePosition = vec2<u32>(tileIndex % TILE_SIZE_X, tileIndex / TILE_SIZE_X);
|
||||||
let sourcePixelU32 =
|
let unclampedSourcePixel = vec2<i32>(workgroupOrigin + tilePosition) - vec2<i32>(1, 1);
|
||||||
workgroup_id.xy * vec2<u32>(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y) +
|
var sourcePixel = unclampedSourcePixel;
|
||||||
tilePosition;
|
if !isInteriorTile {
|
||||||
let sourcePixel = clamp(
|
sourcePixel = clamp(unclampedSourcePixel, vec2<i32>(0, 0), textureSize - vec2<i32>(1, 1));
|
||||||
vec2<i32>(i32(sourcePixelU32.x), i32(sourcePixelU32.y)) - vec2<i32>(1, 1),
|
}
|
||||||
vec2<i32>(0, 0),
|
let texel = textureLoad(trailMap, sourcePixel, 0);
|
||||||
textureSize - vec2<i32>(1, 1)
|
tile[tileIndex] = texel;
|
||||||
);
|
tileTrailStrength[tileIndex] = length(texel.rgb);
|
||||||
tile[tileIndex] = textureLoad(trailMap, sourcePixel, 0);
|
|
||||||
tileIndex += WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workgroupBarrier();
|
workgroupBarrier();
|
||||||
|
|
||||||
let pixel = vec2<i32>(i32(global_id.x), i32(global_id.y));
|
let pixel = vec2<i32>(i32(global_id.x), i32(global_id.y));
|
||||||
if pixel.x >= textureSize.x || pixel.y >= textureSize.y {
|
let inBounds = pixel.x < textureSize.x && pixel.y < textureSize.y;
|
||||||
|
if !inBounds {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,11 +112,12 @@ fn propagate(
|
||||||
brushWeight: f32
|
brushWeight: f32
|
||||||
) -> vec4<f32> {
|
) -> vec4<f32> {
|
||||||
let neighbourIndex = i32(centerTileIndex) + offsetY * i32(TILE_SIZE_X) + offsetX;
|
let neighbourIndex = i32(centerTileIndex) + offsetY * i32(TILE_SIZE_X) + offsetX;
|
||||||
let neighbour = tile[u32(neighbourIndex)];
|
let neighbourTileIndex = u32(neighbourIndex);
|
||||||
|
let neighbour = tile[neighbourTileIndex];
|
||||||
let difference = clamp(neighbour - currentColor, vec4(0), vec4(1));
|
let difference = clamp(neighbour - currentColor, vec4(0), vec4(1));
|
||||||
|
|
||||||
return vec4(
|
return vec4(
|
||||||
vec3(length(neighbour.rgb) * trailWeight),
|
vec3(tileTrailStrength[neighbourTileIndex] * trailWeight),
|
||||||
neighbour.a * brushWeight
|
neighbour.a * brushWeight
|
||||||
) * difference;
|
) * difference;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,14 @@ type DiffusionUniformSettings = Pick<
|
||||||
| 'brushDecayAlphaOffset'
|
| 'brushDecayAlphaOffset'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
|
const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
|
||||||
1 /
|
1 /
|
||||||
(Number.isFinite(diffusionRate) &&
|
(Number.isFinite(diffusionRate) &&
|
||||||
diffusionRate > appConfig.pipelines.diffusion.minDiffusionRate
|
diffusionRate > appConfig.pipelines.diffusion.minDiffusionRate
|
||||||
? diffusionRate
|
? diffusionRate
|
||||||
: appConfig.pipelines.diffusion.minDiffusionRate);
|
: appConfig.pipelines.diffusion.minDiffusionRate);
|
||||||
|
|
||||||
export const setDiffusionUniformValues = (
|
const setDiffusionUniformValues = (
|
||||||
target: Float32Array,
|
target: Float32Array,
|
||||||
{
|
{
|
||||||
diffusionRateTrails,
|
diffusionRateTrails,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createCachedFloat32BufferWrite,
|
createCachedFloat32BufferWrite,
|
||||||
writeFloat32BufferIfChanged,
|
writeFloat32BufferIfChanged,
|
||||||
|
|
@ -86,9 +88,11 @@ export class EraserAgentPipeline {
|
||||||
public setParameters({
|
public setParameters({
|
||||||
agentCount,
|
agentCount,
|
||||||
eraserMaskAlphaThreshold,
|
eraserMaskAlphaThreshold,
|
||||||
|
maskSize,
|
||||||
}: {
|
}: {
|
||||||
agentCount: number;
|
agentCount: number;
|
||||||
eraserMaskAlphaThreshold: number;
|
eraserMaskAlphaThreshold: number;
|
||||||
|
maskSize: vec2;
|
||||||
}): void {
|
}): void {
|
||||||
this.agentCount = agentCount;
|
this.agentCount = agentCount;
|
||||||
this.activeSegmentCount = this.pendingSegmentCount;
|
this.activeSegmentCount = this.pendingSegmentCount;
|
||||||
|
|
@ -96,8 +100,8 @@ export class EraserAgentPipeline {
|
||||||
|
|
||||||
this.uniformUintValues[0] = Math.max(0, Math.floor(agentCount));
|
this.uniformUintValues[0] = Math.max(0, Math.floor(agentCount));
|
||||||
this.uniformValues[1] = eraserMaskAlphaThreshold;
|
this.uniformValues[1] = eraserMaskAlphaThreshold;
|
||||||
this.uniformValues[2] = 0;
|
this.uniformUintValues[2] = Math.max(0, Math.floor(maskSize[0]));
|
||||||
this.uniformValues[3] = 0;
|
this.uniformUintValues[3] = Math.max(0, Math.floor(maskSize[1]));
|
||||||
writeFloat32BufferIfChanged(
|
writeFloat32BufferIfChanged(
|
||||||
this.device,
|
this.device,
|
||||||
this.uniforms,
|
this.uniforms,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
struct Settings {
|
struct Settings {
|
||||||
agentCount: u32,
|
agentCount: u32,
|
||||||
eraserMaskAlphaThreshold: f32,
|
eraserMaskAlphaThreshold: f32,
|
||||||
padding1: f32,
|
maskWidth: u32,
|
||||||
padding2: f32,
|
maskHeight: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||||
|
|
@ -10,10 +10,9 @@ struct Settings {
|
||||||
|
|
||||||
@compute @workgroup_size(64)
|
@compute @workgroup_size(64)
|
||||||
fn main(
|
fn main(
|
||||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
@builtin(global_invocation_id) global_id: vec3<u32>
|
||||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
|
||||||
) {
|
) {
|
||||||
let id = get_id(global_id, num_workgroups);
|
let id = get_id(global_id);
|
||||||
|
|
||||||
if id >= settings.agentCount {
|
if id >= settings.agentCount {
|
||||||
return;
|
return;
|
||||||
|
|
@ -24,7 +23,7 @@ fn main(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let maskSize = vec2<i32>(textureDimensions(eraserMask));
|
let maskSize = vec2<i32>(i32(settings.maskWidth), i32(settings.maskHeight));
|
||||||
let maskPosition = clamp(
|
let maskPosition = clamp(
|
||||||
vec2<i32>(agents[id].position),
|
vec2<i32>(agents[id].position),
|
||||||
vec2<i32>(0, 0),
|
vec2<i32>(0, 0),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export class EraserTexturePipeline {
|
||||||
'r8unorm',
|
'r8unorm',
|
||||||
'rgba16float',
|
'rgba16float',
|
||||||
'rgba16float',
|
'rgba16float',
|
||||||
'rgba16float',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.uniforms = this.device.createBuffer({
|
this.uniforms = this.device.createBuffer({
|
||||||
|
|
@ -169,7 +168,6 @@ export class EraserTexturePipeline {
|
||||||
commandEncoder: GPUCommandEncoder,
|
commandEncoder: GPUCommandEncoder,
|
||||||
eraserMaskOut: GPUTextureView,
|
eraserMaskOut: GPUTextureView,
|
||||||
sourceMapOut: GPUTextureView,
|
sourceMapOut: GPUTextureView,
|
||||||
influenceMapOut: GPUTextureView,
|
|
||||||
trailMapOut: GPUTextureView
|
trailMapOut: GPUTextureView
|
||||||
): void {
|
): void {
|
||||||
if (this.lineCount === 0) {
|
if (this.lineCount === 0) {
|
||||||
|
|
@ -200,11 +198,6 @@ export class EraserTexturePipeline {
|
||||||
loadOp: 'load',
|
loadOp: 'load',
|
||||||
storeOp: 'store',
|
storeOp: 'store',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
view: influenceMapOut,
|
|
||||||
loadOp: 'load',
|
|
||||||
storeOp: 'store',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
view: trailMapOut,
|
view: trailMapOut,
|
||||||
loadOp: 'load',
|
loadOp: 'load',
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,10 @@ struct VertexOutput {
|
||||||
@location(3) @interpolate(flat) inverseLengthSquared: f32,
|
@location(3) @interpolate(flat) inverseLengthSquared: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EraserTextureTargets {
|
|
||||||
@location(0) source: vec4<f32>,
|
|
||||||
@location(1) influence: vec4<f32>,
|
|
||||||
@location(2) trail: vec4<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EraserCombinedTargets {
|
struct EraserCombinedTargets {
|
||||||
@location(0) mask: vec4<f32>,
|
@location(0) mask: vec4<f32>,
|
||||||
@location(1) source: vec4<f32>,
|
@location(1) source: vec4<f32>,
|
||||||
@location(2) influence: vec4<f32>,
|
@location(2) trail: vec4<f32>,
|
||||||
@location(3) trail: vec4<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
|
|
@ -50,35 +43,6 @@ fn vertex(
|
||||||
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, direction, inverseLengthSquared);
|
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, direction, inverseLengthSquared);
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fragment(
|
|
||||||
@location(0) screenPosition: vec2<f32>,
|
|
||||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
|
||||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
|
||||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
|
||||||
) -> @location(0) vec4<f32> {
|
|
||||||
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getEraserClearValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fragmentMrt(
|
|
||||||
@location(0) screenPosition: vec2<f32>,
|
|
||||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
|
||||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
|
||||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
|
||||||
) -> EraserTextureTargets {
|
|
||||||
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleared = getEraserClearValue();
|
|
||||||
return EraserTextureTargets(cleared, cleared, cleared);
|
|
||||||
}
|
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fragmentCombined(
|
fn fragmentCombined(
|
||||||
@location(0) screenPosition: vec2<f32>,
|
@location(0) screenPosition: vec2<f32>,
|
||||||
|
|
@ -91,7 +55,7 @@ fn fragmentCombined(
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleared = getEraserClearValue();
|
let cleared = getEraserClearValue();
|
||||||
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared, cleared);
|
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getEraserMaskValue() -> vec4<f32> {
|
fn getEraserMaskValue() -> vec4<f32> {
|
||||||
|
|
@ -124,10 +88,6 @@ fn distanceSquaredFromLine(
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
let pa = position - start;
|
let pa = position - start;
|
||||||
|
|
||||||
if inverseLengthSquared <= 0.0 {
|
|
||||||
return dot(pa, pa);
|
|
||||||
}
|
|
||||||
|
|
||||||
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0.0, 1.0);
|
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0.0, 1.0);
|
||||||
let nearestOffset = pa - direction * q;
|
let nearestOffset = pa - direction * q;
|
||||||
return dot(nearestOffset, nearestOffset);
|
return dot(nearestOffset, nearestOffset);
|
||||||
|
|
@ -152,14 +112,13 @@ fn segment_vertex_position(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
||||||
if index == 0u {
|
let corners = array<vec2<f32>, 6>(
|
||||||
return vec2<f32>(-1.0, 1.0);
|
vec2<f32>(-1.0, 1.0),
|
||||||
}
|
vec2<f32>(-1.0, -1.0),
|
||||||
if index == 1u || index == 3u {
|
vec2<f32>(1.0, 1.0),
|
||||||
return vec2<f32>(-1.0, -1.0);
|
vec2<f32>(-1.0, -1.0),
|
||||||
}
|
vec2<f32>(1.0, 1.0),
|
||||||
if index == 2u || index == 4u {
|
vec2<f32>(1.0, -1.0),
|
||||||
return vec2<f32>(1.0, 1.0);
|
);
|
||||||
}
|
return corners[index];
|
||||||
return vec2<f32>(1.0, -1.0);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export class RenderPipeline {
|
||||||
|
|
||||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||||
private readonly pipeline: GPURenderPipeline;
|
private readonly pipeline: GPURenderPipeline;
|
||||||
|
private readonly noSourcePipeline: GPURenderPipeline;
|
||||||
private readonly uniforms: GPUBuffer;
|
private readonly uniforms: GPUBuffer;
|
||||||
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
|
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
|
||||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||||
|
|
@ -36,6 +37,7 @@ export class RenderPipeline {
|
||||||
|
|
||||||
const format = navigator.gpu.getPreferredCanvasFormat();
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
||||||
this.pipeline = this.createPipeline(format, vertex, 'fragment');
|
this.pipeline = this.createPipeline(format, vertex, 'fragment');
|
||||||
|
this.noSourcePipeline = this.createPipeline(format, vertex, 'fragmentNoSource');
|
||||||
|
|
||||||
this.uniforms = this.device.createBuffer({
|
this.uniforms = this.device.createBuffer({
|
||||||
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||||
|
|
@ -112,7 +114,8 @@ export class RenderPipeline {
|
||||||
public execute(
|
public execute(
|
||||||
commandEncoder: GPUCommandEncoder,
|
commandEncoder: GPUCommandEncoder,
|
||||||
colorTexture: GPUTextureView,
|
colorTexture: GPUTextureView,
|
||||||
sourceTexture: GPUTextureView
|
sourceTexture: GPUTextureView,
|
||||||
|
useSourceTexture = true
|
||||||
): GPUTexture {
|
): GPUTexture {
|
||||||
const bindGroup = this.getBindGroup(colorTexture, sourceTexture);
|
const bindGroup = this.getBindGroup(colorTexture, sourceTexture);
|
||||||
const canvasTexture = this.context.getCurrentTexture();
|
const canvasTexture = this.context.getCurrentTexture();
|
||||||
|
|
@ -128,7 +131,7 @@ export class RenderPipeline {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||||
passEncoder.setPipeline(this.pipeline);
|
passEncoder.setPipeline(useSourceTexture ? this.pipeline : this.noSourcePipeline);
|
||||||
this.commonState.execute(passEncoder);
|
this.commonState.execute(passEncoder);
|
||||||
passEncoder.setBindGroup(1, bindGroup);
|
passEncoder.setBindGroup(1, bindGroup);
|
||||||
passEncoder.draw(3, 1);
|
passEncoder.draw(3, 1);
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,18 @@ fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
||||||
return renderColor(traces, sources, pixel);
|
return renderColor(traces, sources, pixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragmentNoSource(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
||||||
|
let pixel = vec2<i32>(position.xy);
|
||||||
|
let traces = textureLoad(trailMap, pixel, 0);
|
||||||
|
return renderColor(traces, vec4<f32>(0.0), pixel);
|
||||||
|
}
|
||||||
|
|
||||||
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
|
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
|
||||||
let background = getTexturedBackground(pixel);
|
let background = getTexturedBackground(pixel);
|
||||||
if max(max(max(traces.r, traces.g), traces.b), max(max(sources.r, sources.g), sources.b)) <= 0.0 {
|
let tracesMax = maxComponent(traces.rgb);
|
||||||
|
let sourcesMax = maxComponent(sources.rgb);
|
||||||
|
if max(tracesMax, sourcesMax) <= 0.0 {
|
||||||
return vec4(background, 1);
|
return vec4(background, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,13 +47,13 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
|
||||||
clarity(traces.g),
|
clarity(traces.g),
|
||||||
clarity(traces.b)
|
clarity(traces.b)
|
||||||
);
|
);
|
||||||
if max(max(sources.r, sources.g), sources.b) <= 0.0 {
|
if sourcesMax <= 0.0 {
|
||||||
let traceColor =
|
let traceColor =
|
||||||
traceStrengths.r * settings.colorA
|
traceStrengths.r * settings.colorA
|
||||||
+ traceStrengths.g * settings.colorB
|
+ traceStrengths.g * settings.colorB
|
||||||
+ traceStrengths.b * settings.colorC;
|
+ traceStrengths.b * settings.colorC;
|
||||||
let normalizedTraceColor = normalizeColorIntensity(traceColor);
|
let normalizedTraceColor = normalizeColorIntensity(traceColor);
|
||||||
let traceStrength = max(max(traceStrengths.r, traceStrengths.g), traceStrengths.b);
|
let traceStrength = maxComponent(traceStrengths);
|
||||||
return vec4(mix(background, clamp(normalizedTraceColor, vec3(0), vec3(1)), traceStrength), 1);
|
return vec4(mix(background, clamp(normalizedTraceColor, vec3(0), vec3(1)), traceStrength), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +73,7 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
|
||||||
+ sourceStrengths.g * settings.colorB
|
+ sourceStrengths.g * settings.colorB
|
||||||
+ sourceStrengths.b * settings.colorC;
|
+ sourceStrengths.b * settings.colorC;
|
||||||
let normalizedBrushColor = normalizeColorIntensity(brushColor);
|
let normalizedBrushColor = normalizeColorIntensity(brushColor);
|
||||||
let brushStrength = max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b);
|
let brushStrength = maxComponent(sourceStrengths);
|
||||||
let brushVisibility = clamp(
|
let brushVisibility = clamp(
|
||||||
brushStrength * (
|
brushStrength * (
|
||||||
settings.brushColorBase +
|
settings.brushColorBase +
|
||||||
|
|
@ -75,20 +84,27 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
|
||||||
);
|
);
|
||||||
let color = max(normalizedTraceColor, normalizedBrushColor);
|
let color = max(normalizedTraceColor, normalizedBrushColor);
|
||||||
|
|
||||||
let strength = max(max(max(strengths.r, strengths.g), strengths.b), brushVisibility);
|
let strength = max(maxComponent(strengths), brushVisibility);
|
||||||
return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1);
|
return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maxComponent(v: vec3<f32>) -> f32 {
|
||||||
|
return max(max(v.r, v.g), v.b);
|
||||||
|
}
|
||||||
|
|
||||||
fn clarity(strength: f32) -> f32 {
|
fn clarity(strength: f32) -> f32 {
|
||||||
return pow(clamp(strength, 0, 1), settings.clarity);
|
return pow(clamp(strength, 0, 1), settings.clarity);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
|
fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
|
||||||
let brightestChannel = max(max(color.r, color.g), color.b);
|
let brightestChannel = maxComponent(color);
|
||||||
return color / max(settings.traceNormalizationFloor, brightestChannel);
|
return color / max(settings.traceNormalizationFloor, brightestChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
|
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
|
||||||
|
if settings.backgroundGrainStrength == 0.0 {
|
||||||
|
return clamp(settings.backgroundColor, vec3(0), vec3(1));
|
||||||
|
}
|
||||||
let noiseCoord = vec2<i32>(vec2<u32>(pixel) & vec2<u32>(NOISE_TEXTURE_MASK));
|
let noiseCoord = vec2<i32>(vec2<u32>(pixel) & vec2<u32>(NOISE_TEXTURE_MASK));
|
||||||
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;
|
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,6 @@ export const settings: GardenRuntimeSettings = {
|
||||||
...buildSettings(activeVibe),
|
...buildSettings(activeVibe),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetSettings = (): GardenRuntimeSettings => {
|
|
||||||
Object.assign(settings, buildSettings(activeVibe));
|
|
||||||
return settings;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const applyVibeSettings = (vibe: VibePreset) => {
|
export const applyVibeSettings = (vibe: VibePreset) => {
|
||||||
activeVibe = vibe;
|
activeVibe = vibe;
|
||||||
Object.assign(settings, {
|
Object.assign(settings, {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const initializeContext = ({
|
||||||
device: device,
|
device: device,
|
||||||
format: gpu.getPreferredCanvasFormat(),
|
format: gpu.getPreferredCanvasFormat(),
|
||||||
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
||||||
alphaMode: 'premultiplied',
|
alphaMode: 'opaque',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new RuntimeError(
|
throw new RuntimeError(
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,9 @@ export const generateNoise = ({
|
||||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||||
return vec4(
|
return vec4(
|
||||||
random_with_seed(uv, ${appConfig.pipelines.common.noiseChannelSeeds[0]}),
|
random_with_seed(uv, ${appConfig.pipelines.common.noiseChannelSeeds[0]}),
|
||||||
random_with_seed(uv, ${appConfig.pipelines.common.noiseChannelSeeds[1]}),
|
0.0,
|
||||||
random_with_seed(uv, ${appConfig.pipelines.common.noiseChannelSeeds[2]}),
|
0.0,
|
||||||
random_with_seed(uv, ${appConfig.pipelines.common.noiseChannelSeeds[3]}),
|
1.0,
|
||||||
);
|
);
|
||||||
}`
|
}`
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,32 @@ const clampRgbChannel = (value: number): number =>
|
||||||
export const rgbColorToCss = ([red, green, blue]: RgbColor): string =>
|
export const rgbColorToCss = ([red, green, blue]: RgbColor): string =>
|
||||||
`rgb(${clampRgbChannel(red)}, ${clampRgbChannel(green)}, ${clampRgbChannel(blue)})`;
|
`rgb(${clampRgbChannel(red)}, ${clampRgbChannel(green)}, ${clampRgbChannel(blue)})`;
|
||||||
|
|
||||||
|
export const rgbColorToHex = ([red, green, blue]: RgbColor): string =>
|
||||||
|
`#${[red, green, blue]
|
||||||
|
.map((channel) => clampRgbChannel(channel).toString(16).padStart(2, '0'))
|
||||||
|
.join('')}`;
|
||||||
|
|
||||||
|
export const hexColorToRgbColor = (value: string): RgbColor | null => {
|
||||||
|
const match = value.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shorthandOrHex = match[1];
|
||||||
|
const hex =
|
||||||
|
shorthandOrHex.length === 3
|
||||||
|
? shorthandOrHex
|
||||||
|
.split('')
|
||||||
|
.map((channel) => `${channel}${channel}`)
|
||||||
|
.join('')
|
||||||
|
: shorthandOrHex;
|
||||||
|
|
||||||
|
return [
|
||||||
|
Number.parseInt(hex.slice(0, 2), 16),
|
||||||
|
Number.parseInt(hex.slice(2, 4), 16),
|
||||||
|
Number.parseInt(hex.slice(4, 6), 16),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export const rgbChannelToUnit = (value: number): number =>
|
export const rgbChannelToUnit = (value: number): number =>
|
||||||
Math.min(1, Math.max(0, toFiniteRgbChannel(value) / RGB_CHANNEL_MAX));
|
Math.min(1, Math.max(0, toFiniteRgbChannel(value) / RGB_CHANNEL_MAX));
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export type { VibePreset } from './config';
|
||||||
export const VIBE_PRESETS: Array<VibePreset> = appConfig.vibes.presets;
|
export const VIBE_PRESETS: Array<VibePreset> = appConfig.vibes.presets;
|
||||||
const VIBE_IDS = new Set<VibeId>(VIBE_PRESETS.map((vibe) => vibe.id));
|
const VIBE_IDS = new Set<VibeId>(VIBE_PRESETS.map((vibe) => vibe.id));
|
||||||
|
|
||||||
export const isVibeId = (value: unknown): value is VibeId =>
|
const isVibeId = (value: unknown): value is VibeId =>
|
||||||
typeof value === 'string' && VIBE_IDS.has(value as VibeId);
|
typeof value === 'string' && VIBE_IDS.has(value as VibeId);
|
||||||
|
|
||||||
export const getInitialVibe = (): VibePreset => {
|
export const getInitialVibe = (): VibePreset => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue