not sure
This commit is contained in:
parent
560398fefb
commit
80ed37298b
73 changed files with 3619 additions and 1295 deletions
|
|
@ -15,7 +15,6 @@ struct Counters {
|
|||
|
||||
var<workgroup> workgroupAliveCount: atomic<u32>;
|
||||
var<workgroup> workgroupCompactedOffset: u32;
|
||||
var<workgroup> workgroupCopyCount: u32;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
|
|
@ -31,9 +30,12 @@ fn main(
|
|||
workgroupBarrier();
|
||||
|
||||
var localCompactedIndex = 0u;
|
||||
var agent = Agent(vec2<f32>(0.0, 0.0), 0.0, -1.0, vec2<f32>(-1.0, -1.0), 0.0, 0.0);
|
||||
var isAlive = false;
|
||||
if id < settings.agentCount {
|
||||
let agent = agents[id];
|
||||
if agent.colorIndex >= 0.0 {
|
||||
agent = agents[id];
|
||||
isAlive = agent.colorIndex >= 0.0;
|
||||
if isAlive {
|
||||
localCompactedIndex = atomicAdd(&workgroupAliveCount, 1u);
|
||||
}
|
||||
}
|
||||
|
|
@ -51,30 +53,7 @@ fn main(
|
|||
|
||||
workgroupBarrier();
|
||||
|
||||
if id < settings.agentCount {
|
||||
let agent = agents[id];
|
||||
if agent.colorIndex >= 0.0 {
|
||||
compactedAgents[workgroupCompactedOffset + localCompactedIndex] = agent;
|
||||
}
|
||||
if isAlive {
|
||||
compactedAgents[workgroupCompactedOffset + localCompactedIndex] = agent;
|
||||
}
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn copyCompactedAgents(
|
||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
||||
@builtin(local_invocation_id) local_id: vec3<u32>
|
||||
) {
|
||||
let id = get_id(global_id);
|
||||
|
||||
if local_id.x == 0u {
|
||||
workgroupCopyCount = atomicLoad(&counters.aliveAgentCount);
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
if id >= workgroupCopyCount {
|
||||
return;
|
||||
}
|
||||
|
||||
agents[id] = compactedAgents[id];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,17 +177,14 @@ const createPipeline = (compactedCount: number) => {
|
|||
};
|
||||
|
||||
describe('AgentGenerationPipeline compaction', () => {
|
||||
it('copies compacted agents back with compute instead of a full agent buffer copy', async () => {
|
||||
it('swaps compacted agents into the active buffer without a copy-back dispatch', async () => {
|
||||
const agentCount = 10;
|
||||
const { device, pipeline } = createPipeline(3);
|
||||
|
||||
await expect(pipeline.compactAgents(agentCount)).resolves.toBe(3);
|
||||
|
||||
expect(device.createdComputeEntryPoints).toContain('copyCompactedAgents');
|
||||
expect(device.dispatchCalls.map((call) => call.entryPoint)).toEqual([
|
||||
'main',
|
||||
'copyCompactedAgents',
|
||||
]);
|
||||
expect(device.createdComputeEntryPoints).not.toContain('copyCompactedAgents');
|
||||
expect(device.dispatchCalls.map((call) => call.entryPoint)).toEqual(['main']);
|
||||
expect(device.copyCalls.map((call) => call.size)).toEqual([
|
||||
Uint32Array.BYTES_PER_ELEMENT,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -14,14 +14,16 @@ export class AgentGenerationPipeline {
|
|||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly bindGroup: GPUBindGroup;
|
||||
private readonly bindGroupsByActiveBuffer = new WeakMap<
|
||||
GPUBuffer,
|
||||
WeakMap<GPUBuffer, GPUBindGroup>
|
||||
>();
|
||||
|
||||
private readonly resizePipeline: GPUComputePipeline;
|
||||
private readonly compactionPipeline: GPUComputePipeline;
|
||||
private readonly compactedAgentsCopyPipeline: GPUComputePipeline;
|
||||
|
||||
public readonly agentsBuffer: GPUBuffer;
|
||||
private readonly compactedAgentsBuffer: GPUBuffer;
|
||||
private activeAgentsBuffer: GPUBuffer;
|
||||
private inactiveAgentsBuffer: GPUBuffer;
|
||||
private readonly countersBuffer: GPUBuffer;
|
||||
private readonly countersStagingBuffer: GPUBuffer;
|
||||
private readonly counterClearValues = new Uint32Array(
|
||||
|
|
@ -69,15 +71,8 @@ export class AgentGenerationPipeline {
|
|||
],
|
||||
});
|
||||
|
||||
this.agentsBuffer = this.device.createBuffer({
|
||||
size: this.maxAgentCount * AGENT_SIZE_IN_BYTES,
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
this.compactedAgentsBuffer = this.device.createBuffer({
|
||||
size: this.maxAgentCount * AGENT_SIZE_IN_BYTES,
|
||||
usage: GPUBufferUsage.STORAGE,
|
||||
});
|
||||
this.activeAgentsBuffer = this.createAgentsBuffer();
|
||||
this.inactiveAgentsBuffer = this.createAgentsBuffer();
|
||||
|
||||
this.countersBuffer = this.device.createBuffer({
|
||||
size: AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT,
|
||||
|
|
@ -94,36 +89,6 @@ export class AgentGenerationPipeline {
|
|||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: {
|
||||
buffer: this.uniforms,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: {
|
||||
buffer: this.agentsBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: {
|
||||
buffer: this.countersBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: {
|
||||
buffer: this.compactedAgentsBuffer,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.resizePipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
|
||||
|
|
@ -145,15 +110,16 @@ export class AgentGenerationPipeline {
|
|||
entryPoint: 'main',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.compactedAgentsCopyPipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
|
||||
}),
|
||||
compute: {
|
||||
module: compactionModule,
|
||||
entryPoint: 'copyCompactedAgents',
|
||||
},
|
||||
public get agentsBuffer(): GPUBuffer {
|
||||
return this.activeAgentsBuffer;
|
||||
}
|
||||
|
||||
private createAgentsBuffer(): GPUBuffer {
|
||||
return this.device.createBuffer({
|
||||
size: this.maxAgentCount * AGENT_SIZE_IN_BYTES,
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +129,10 @@ export class AgentGenerationPipeline {
|
|||
? this.maxAgentCountUpperLimit
|
||||
: Number.POSITIVE_INFINITY,
|
||||
Math.floor(this.device.limits.maxBufferSize / AGENT_SIZE_IN_BYTES) - 1,
|
||||
Math.floor(
|
||||
((this.device.limits as GPUSupportedLimits).maxStorageBufferBindingSize ??
|
||||
this.device.limits.maxBufferSize) / AGENT_SIZE_IN_BYTES
|
||||
) - 1,
|
||||
this.device.limits.maxComputeWorkgroupsPerDimension *
|
||||
AgentGenerationPipeline.WORKGROUP_SIZE
|
||||
);
|
||||
|
|
@ -170,7 +140,7 @@ export class AgentGenerationPipeline {
|
|||
|
||||
public writeAgents(agentOffset: number, data: Float32Array): void {
|
||||
this.device.queue.writeBuffer(
|
||||
this.agentsBuffer,
|
||||
this.activeAgentsBuffer,
|
||||
agentOffset * AGENT_SIZE_IN_BYTES,
|
||||
data
|
||||
);
|
||||
|
|
@ -190,7 +160,7 @@ export class AgentGenerationPipeline {
|
|||
const commandEncoder = this.device.createCommandEncoder();
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
passEncoder.setPipeline(this.resizePipeline);
|
||||
passEncoder.setBindGroup(1, this.bindGroup);
|
||||
passEncoder.setBindGroup(1, this.getBindGroup());
|
||||
passEncoder.dispatchWorkgroups(
|
||||
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
|
||||
);
|
||||
|
|
@ -204,8 +174,6 @@ export class AgentGenerationPipeline {
|
|||
return 0;
|
||||
}
|
||||
|
||||
this.counterClearValues.fill(0);
|
||||
this.agentCountUniformValues.fill(0);
|
||||
this.agentCountUniformValues[0] = agentCount;
|
||||
this.device.queue.writeBuffer(this.countersBuffer, 0, this.counterClearValues);
|
||||
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
|
||||
|
|
@ -213,20 +181,12 @@ export class AgentGenerationPipeline {
|
|||
const commandEncoder = this.device.createCommandEncoder();
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
passEncoder.setPipeline(this.compactionPipeline);
|
||||
passEncoder.setBindGroup(1, this.bindGroup);
|
||||
passEncoder.setBindGroup(1, this.getBindGroup());
|
||||
passEncoder.dispatchWorkgroups(
|
||||
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
|
||||
);
|
||||
passEncoder.end();
|
||||
|
||||
const copyPassEncoder = commandEncoder.beginComputePass();
|
||||
copyPassEncoder.setPipeline(this.compactedAgentsCopyPipeline);
|
||||
copyPassEncoder.setBindGroup(1, this.bindGroup);
|
||||
copyPassEncoder.dispatchWorkgroups(
|
||||
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
|
||||
);
|
||||
copyPassEncoder.end();
|
||||
|
||||
commandEncoder.copyBufferToBuffer(
|
||||
this.countersBuffer,
|
||||
0,
|
||||
|
|
@ -239,18 +199,74 @@ export class AgentGenerationPipeline {
|
|||
|
||||
await this.countersStagingBuffer.mapAsync(GPUMapMode.READ);
|
||||
const compactedCount = new Uint32Array(
|
||||
this.countersStagingBuffer.getMappedRange().slice(0, Uint32Array.BYTES_PER_ELEMENT)
|
||||
this.countersStagingBuffer.getMappedRange(),
|
||||
0,
|
||||
1
|
||||
)[0];
|
||||
this.countersStagingBuffer.unmap();
|
||||
this.swapAgentBuffers();
|
||||
|
||||
return compactedCount;
|
||||
}
|
||||
|
||||
private getBindGroup(): GPUBindGroup {
|
||||
let inactiveCache = this.bindGroupsByActiveBuffer.get(this.activeAgentsBuffer);
|
||||
if (!inactiveCache) {
|
||||
inactiveCache = new WeakMap<GPUBuffer, GPUBindGroup>();
|
||||
this.bindGroupsByActiveBuffer.set(this.activeAgentsBuffer, inactiveCache);
|
||||
}
|
||||
|
||||
const cached = inactiveCache.get(this.inactiveAgentsBuffer);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: {
|
||||
buffer: this.uniforms,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: {
|
||||
buffer: this.activeAgentsBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: {
|
||||
buffer: this.countersBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: {
|
||||
buffer: this.inactiveAgentsBuffer,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
inactiveCache.set(this.inactiveAgentsBuffer, bindGroup);
|
||||
return bindGroup;
|
||||
}
|
||||
|
||||
private swapAgentBuffers(): void {
|
||||
[this.activeAgentsBuffer, this.inactiveAgentsBuffer] = [
|
||||
this.inactiveAgentsBuffer,
|
||||
this.activeAgentsBuffer,
|
||||
];
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.uniforms.destroy();
|
||||
this.countersBuffer.destroy();
|
||||
this.countersStagingBuffer.destroy();
|
||||
this.compactedAgentsBuffer.destroy();
|
||||
this.agentsBuffer.destroy();
|
||||
this.inactiveAgentsBuffer.destroy();
|
||||
this.activeAgentsBuffer.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,6 @@ fn main(
|
|||
return;
|
||||
}
|
||||
|
||||
var agent = agents[id];
|
||||
agent.position *= resizeSettings.scale;
|
||||
|
||||
if agent.targetPosition.x >= 0.0 && agent.targetPosition.y >= 0.0 {
|
||||
agent.targetPosition *= resizeSettings.scale;
|
||||
}
|
||||
|
||||
agents[id] = agent;
|
||||
agents[id].position = agents[id].position * resizeSettings.scale;
|
||||
agents[id].targetPosition = agents[id].targetPosition * resizeSettings.scale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,13 +67,12 @@ describe('Agent TS/WGSL contract', () => {
|
|||
expect(compactionShader).toContain('if id < settings.agentCount');
|
||||
});
|
||||
|
||||
it('keeps compaction copy-back bounded by the compacted count', () => {
|
||||
expect(compactionShader).toContain('fn copyCompactedAgents');
|
||||
it('keeps compaction as a ping-pong write without copy-back shader work', () => {
|
||||
expect(compactionShader).not.toContain('fn copyCompactedAgents');
|
||||
expect(compactionShader).not.toContain('agents[id] = compactedAgents[id];');
|
||||
expect(compactionShader).toContain(
|
||||
'workgroupCopyCount = atomicLoad(&counters.aliveAgentCount);'
|
||||
'compactedAgents[workgroupCompactedOffset + localCompactedIndex] = agent;'
|
||||
);
|
||||
expect(compactionShader).toContain('if id >= workgroupCopyCount');
|
||||
expect(compactionShader).toContain('agents[id] = compactedAgents[id];');
|
||||
});
|
||||
|
||||
it('uses workgroup-local counting before allocating global compacted ranges', () => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import shader from './agent.wgsl?raw';
|
|||
|
||||
export class AgentPipeline {
|
||||
private static readonly WORKGROUP_SIZE = 64;
|
||||
private static readonly UNIFORM_COUNT = 17;
|
||||
private static readonly UNIFORM_COUNT = 33;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly pipeline: GPUComputePipeline;
|
||||
|
|
@ -20,9 +20,12 @@ export class AgentPipeline {
|
|||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
AgentPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly bindGroupsByTexture = new WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
private readonly bindGroupsByAgentsBuffer = new WeakMap<
|
||||
GPUBuffer,
|
||||
WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
>
|
||||
>();
|
||||
|
||||
private agentCount = 0;
|
||||
|
|
@ -30,7 +33,7 @@ export class AgentPipeline {
|
|||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
private readonly commonState: CommonState,
|
||||
private readonly agentsBuffer: GPUBuffer // doesn't get destroyed
|
||||
private readonly getAgentsBuffer: () => GPUBuffer // doesn't get destroyed
|
||||
) {
|
||||
this.bindGroupLayout = device.createBindGroupLayout(AgentPipeline.bindGroupLayout);
|
||||
|
||||
|
|
@ -67,6 +70,21 @@ export class AgentPipeline {
|
|||
color3ToColor1,
|
||||
color3ToColor2,
|
||||
color3ToColor3,
|
||||
sourceAttractionWeight,
|
||||
sourceSlowMoveRate,
|
||||
sourceTrailWeightMultiplier,
|
||||
forwardRotationScale,
|
||||
introNearDistanceInner,
|
||||
introNearDistanceMin,
|
||||
introNearSensorOffsetMultiplier,
|
||||
introTargetAngleBlend,
|
||||
introProgressCutoff,
|
||||
introTurnRateMultiplier,
|
||||
introRandomTurnMultiplier,
|
||||
introFarMoveMultiplier,
|
||||
introNearMoveMultiplier,
|
||||
introStepStopDistance,
|
||||
randomTimeScale,
|
||||
agentCount,
|
||||
introProgress,
|
||||
}: AgentSettings & {
|
||||
|
|
@ -77,21 +95,38 @@ export class AgentPipeline {
|
|||
this.agentCount = agentCount;
|
||||
this.uniformValues[0] = moveSpeed * deltaTime;
|
||||
this.uniformValues[1] = turnSpeed * deltaTime;
|
||||
this.uniformValues[2] = (sensorOffsetAngle * Math.PI) / 180;
|
||||
this.uniformValues[3] = sensorOffsetDistance;
|
||||
this.uniformValues[4] = turnWhenLost;
|
||||
this.uniformValues[5] = individualTrailWeight;
|
||||
this.uniformValues[6] = agentCount;
|
||||
this.uniformValues[7] = introProgress ?? 1;
|
||||
this.uniformValues[8] = color1ToColor1;
|
||||
this.uniformValues[9] = color1ToColor2;
|
||||
this.uniformValues[10] = color1ToColor3;
|
||||
this.uniformValues[11] = color2ToColor1;
|
||||
this.uniformValues[12] = color2ToColor2;
|
||||
this.uniformValues[13] = color2ToColor3;
|
||||
this.uniformValues[14] = color3ToColor1;
|
||||
this.uniformValues[15] = color3ToColor2;
|
||||
this.uniformValues[16] = color3ToColor3;
|
||||
const sensorAngle = (sensorOffsetAngle * Math.PI) / 180;
|
||||
this.uniformValues[2] = Math.sin(sensorAngle);
|
||||
this.uniformValues[3] = Math.cos(sensorAngle);
|
||||
this.uniformValues[4] = sensorOffsetDistance;
|
||||
this.uniformValues[5] = turnWhenLost;
|
||||
this.uniformValues[6] = individualTrailWeight;
|
||||
this.uniformValues[7] = agentCount;
|
||||
this.uniformValues[8] = introProgress ?? 1;
|
||||
this.uniformValues[9] = color1ToColor1;
|
||||
this.uniformValues[10] = color1ToColor2;
|
||||
this.uniformValues[11] = color1ToColor3;
|
||||
this.uniformValues[12] = color2ToColor1;
|
||||
this.uniformValues[13] = color2ToColor2;
|
||||
this.uniformValues[14] = color2ToColor3;
|
||||
this.uniformValues[15] = color3ToColor1;
|
||||
this.uniformValues[16] = color3ToColor2;
|
||||
this.uniformValues[17] = color3ToColor3;
|
||||
this.uniformValues[18] = sourceAttractionWeight;
|
||||
this.uniformValues[19] = sourceSlowMoveRate;
|
||||
this.uniformValues[20] = sourceTrailWeightMultiplier;
|
||||
this.uniformValues[21] = forwardRotationScale;
|
||||
this.uniformValues[22] = introNearDistanceInner;
|
||||
this.uniformValues[23] = introNearDistanceMin;
|
||||
this.uniformValues[24] = introNearSensorOffsetMultiplier;
|
||||
this.uniformValues[25] = introTargetAngleBlend;
|
||||
this.uniformValues[26] = introProgressCutoff;
|
||||
this.uniformValues[27] = introTurnRateMultiplier;
|
||||
this.uniformValues[28] = introRandomTurnMultiplier;
|
||||
this.uniformValues[29] = introFarMoveMultiplier;
|
||||
this.uniformValues[30] = introNearMoveMultiplier;
|
||||
this.uniformValues[31] = introStepStopDistance;
|
||||
this.uniformValues[32] = randomTimeScale;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
|
|
@ -127,10 +162,20 @@ export class AgentPipeline {
|
|||
trailMapOut: GPUTextureView,
|
||||
sourceMap: GPUTextureView
|
||||
): GPUBindGroup {
|
||||
let outputCache = this.bindGroupsByTexture.get(trailMapIn);
|
||||
const agentsBuffer = this.getAgentsBuffer();
|
||||
let textureCache = this.bindGroupsByAgentsBuffer.get(agentsBuffer);
|
||||
if (!textureCache) {
|
||||
textureCache = new WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
>();
|
||||
this.bindGroupsByAgentsBuffer.set(agentsBuffer, textureCache);
|
||||
}
|
||||
|
||||
let outputCache = textureCache.get(trailMapIn);
|
||||
if (!outputCache) {
|
||||
outputCache = new WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>();
|
||||
this.bindGroupsByTexture.set(trailMapIn, outputCache);
|
||||
textureCache.set(trailMapIn, outputCache);
|
||||
}
|
||||
|
||||
let sourceCache = outputCache.get(trailMapOut);
|
||||
|
|
@ -156,7 +201,7 @@ export class AgentPipeline {
|
|||
{
|
||||
binding: 1,
|
||||
resource: {
|
||||
buffer: this.agentsBuffer,
|
||||
buffer: agentsBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,4 +14,19 @@ export interface AgentSettings {
|
|||
sensorOffsetDistance: number;
|
||||
turnWhenLost: number;
|
||||
individualTrailWeight: number;
|
||||
sourceAttractionWeight: number;
|
||||
sourceSlowMoveRate: number;
|
||||
sourceTrailWeightMultiplier: number;
|
||||
forwardRotationScale: number;
|
||||
introNearDistanceMin: number;
|
||||
introNearSensorOffsetMultiplier: number;
|
||||
introTargetAngleBlend: number;
|
||||
introProgressCutoff: number;
|
||||
introNearDistanceInner: number;
|
||||
introTurnRateMultiplier: number;
|
||||
introRandomTurnMultiplier: number;
|
||||
introFarMoveMultiplier: number;
|
||||
introNearMoveMultiplier: number;
|
||||
introStepStopDistance: number;
|
||||
randomTimeScale: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
struct Settings {
|
||||
moveRate: f32,
|
||||
turnRate: f32,
|
||||
sensorAngle: f32,
|
||||
sensorAngleSin: f32,
|
||||
sensorAngleCos: f32,
|
||||
sensorOffset: f32,
|
||||
turnWhenLost: f32,
|
||||
individualTrailWeight: f32,
|
||||
|
|
@ -16,6 +17,21 @@ struct Settings {
|
|||
color3ToColor1: f32,
|
||||
color3ToColor2: f32,
|
||||
color3ToColor3: f32,
|
||||
sourceAttractionWeight: f32,
|
||||
sourceSlowMoveRate: f32,
|
||||
sourceTrailWeightMultiplier: f32,
|
||||
forwardRotationScale: f32,
|
||||
introNearDistanceInner: f32,
|
||||
introNearDistanceMin: f32,
|
||||
introNearSensorOffsetMultiplier: f32,
|
||||
introTargetAngleBlend: f32,
|
||||
introProgressCutoff: f32,
|
||||
introTurnRateMultiplier: f32,
|
||||
introRandomTurnMultiplier: f32,
|
||||
introFarMoveMultiplier: f32,
|
||||
introNearMoveMultiplier: f32,
|
||||
introStepStopDistance: f32,
|
||||
randomTimeScale: f32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
|
|
@ -33,25 +49,38 @@ fn main(
|
|||
return;
|
||||
}
|
||||
|
||||
var agent = agents[id];
|
||||
if agent.colorIndex < 0.0 || agent.colorIndex >= 2.5 {
|
||||
let colorIndex = agents[id].colorIndex;
|
||||
if colorIndex < 0.0 || colorIndex >= 2.5 {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasIntroTarget =
|
||||
settings.introProgress < 0.999 &&
|
||||
agent.targetPosition.x >= 0.0 &&
|
||||
agent.targetPosition.y >= 0.0;
|
||||
|
||||
if hasIntroTarget && settings.introProgress < agent.introDelay {
|
||||
return;
|
||||
var position = agents[id].position;
|
||||
var angle = agents[id].angle;
|
||||
var targetPosition = vec2<f32>(-1.0, -1.0);
|
||||
var hasIntroTarget = false;
|
||||
if settings.introProgress < settings.introProgressCutoff {
|
||||
targetPosition = agents[id].targetPosition;
|
||||
hasIntroTarget = targetPosition.x >= 0.0 && targetPosition.y >= 0.0;
|
||||
if hasIntroTarget && settings.introProgress < agents[id].introDelay {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let random = random_vec4(id, state.time);
|
||||
let randomSeed = random_seed(id, state.time);
|
||||
let randomTurn = random_float(randomSeed);
|
||||
let direction = vec2(cos(angle), sin(angle));
|
||||
|
||||
let forwardSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, 0);
|
||||
let leftSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle);
|
||||
let rightSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, -settings.sensorAngle);
|
||||
let forwardSensor = sensor_position(position, direction, settings.sensorOffset);
|
||||
let leftSensor = sensor_position(
|
||||
position,
|
||||
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
|
||||
settings.sensorOffset
|
||||
);
|
||||
let rightSensor = sensor_position(
|
||||
position,
|
||||
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
|
||||
settings.sensorOffset
|
||||
);
|
||||
|
||||
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
|
||||
let trailLeft = textureLoad(trailMapIn, leftSensor, 0);
|
||||
|
|
@ -60,8 +89,8 @@ fn main(
|
|||
let sourceLeftSample = textureLoad(sourceMap, leftSensor, 0);
|
||||
let sourceRightSample = textureLoad(sourceMap, rightSensor, 0);
|
||||
|
||||
let channelMask = get_channel_mask(agent.colorIndex);
|
||||
let reactionMask = get_reaction_mask(agent.colorIndex);
|
||||
let channelMask = get_channel_mask(colorIndex);
|
||||
let reactionMask = get_reaction_mask(colorIndex);
|
||||
|
||||
let trailForwardWeight = dot(trailForward.rgb, reactionMask);
|
||||
let trailLeftWeight = dot(trailLeft.rgb, reactionMask);
|
||||
|
|
@ -71,76 +100,105 @@ fn main(
|
|||
let sourceLeftWeight = dot(sourceLeftSample.rgb, reactionMask);
|
||||
let sourceRightWeight = dot(sourceRightSample.rgb, reactionMask);
|
||||
|
||||
let weightForward = trailForwardWeight + sourceForwardWeight * 24.0;
|
||||
let weightLeft = trailLeftWeight + sourceLeftWeight * 24.0;
|
||||
let weightRight = trailRightWeight + sourceRightWeight * 24.0;
|
||||
let weightForward =
|
||||
trailForwardWeight + sourceForwardWeight * settings.sourceAttractionWeight;
|
||||
let weightLeft = trailLeftWeight + sourceLeftWeight * settings.sourceAttractionWeight;
|
||||
let weightRight =
|
||||
trailRightWeight + sourceRightWeight * settings.sourceAttractionWeight;
|
||||
|
||||
var rotation = (random.r - 0.5) * settings.turnWhenLost;
|
||||
var rotation = (randomTurn - 0.5) * settings.turnWhenLost;
|
||||
if weightForward >= weightLeft && weightForward >= weightRight {
|
||||
rotation = rotation * 0.25;
|
||||
rotation = rotation * settings.forwardRotationScale;
|
||||
} else {
|
||||
rotation += sign(weightLeft - weightRight) * settings.turnRate;
|
||||
}
|
||||
|
||||
let sourceAtAgent = textureLoad(sourceMap, vec2<i32>(agent.position), 0);
|
||||
let sourceAtAgent = textureLoad(sourceMap, vec2<i32>(position), 0);
|
||||
let positiveReactionMask = max(reactionMask, vec3<f32>(0.0));
|
||||
let sourceAtAgentStrength = clamp(dot(sourceAtAgent.rgb, positiveReactionMask), 0.0, 1.0);
|
||||
var moveRate = settings.moveRate * mix(1.0, 0.08, sourceAtAgentStrength);
|
||||
var moveRate = settings.moveRate * mix(1.0, settings.sourceSlowMoveRate, sourceAtAgentStrength);
|
||||
var introTargetOffset = vec2<f32>(0.0, 0.0);
|
||||
var introTargetDistance = 0.0;
|
||||
|
||||
if hasIntroTarget {
|
||||
introTargetOffset = agent.targetPosition - agent.position;
|
||||
introTargetOffset = targetPosition - position;
|
||||
introTargetDistance = length(introTargetOffset);
|
||||
let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x);
|
||||
let nearTitle = 1.0 - smoothstep(4.0, max(28.0, settings.sensorOffset * 0.75), introTargetDistance);
|
||||
let desiredAngle = mix(targetAngle, agent.targetAngle, nearTitle * 0.2);
|
||||
let introTurn = angle_delta(agent.angle, desiredAngle);
|
||||
let nearTitle = 1.0 - smoothstep(
|
||||
settings.introNearDistanceInner,
|
||||
max(
|
||||
settings.introNearDistanceMin,
|
||||
settings.sensorOffset * settings.introNearSensorOffsetMultiplier
|
||||
),
|
||||
introTargetDistance
|
||||
);
|
||||
let desiredAngle = mix(
|
||||
targetAngle,
|
||||
agents[id].targetAngle,
|
||||
nearTitle * settings.introTargetAngleBlend
|
||||
);
|
||||
let introTurn = angle_delta(angle, desiredAngle);
|
||||
|
||||
rotation = clamp(introTurn, -settings.turnRate * 3.4, settings.turnRate * 3.4)
|
||||
+ (random.g - 0.5) * settings.turnWhenLost * 0.18;
|
||||
moveRate = min(settings.moveRate * mix(2.65, 0.01, nearTitle), introTargetDistance);
|
||||
rotation = clamp(
|
||||
introTurn,
|
||||
-settings.turnRate * settings.introTurnRateMultiplier,
|
||||
settings.turnRate * settings.introTurnRateMultiplier
|
||||
)
|
||||
+ (random_float(randomSeed + 1013904223u) - 0.5) *
|
||||
settings.turnWhenLost *
|
||||
settings.introRandomTurnMultiplier;
|
||||
moveRate = min(
|
||||
settings.moveRate *
|
||||
mix(settings.introFarMoveMultiplier, settings.introNearMoveMultiplier, nearTitle),
|
||||
introTargetDistance
|
||||
);
|
||||
}
|
||||
|
||||
var step = vec2(cos(agent.angle), sin(agent.angle)) * moveRate;
|
||||
var step = direction * moveRate;
|
||||
if hasIntroTarget {
|
||||
step = vec2<f32>(0.0, 0.0);
|
||||
if introTargetDistance > 0.5 {
|
||||
if introTargetDistance > settings.introStepStopDistance {
|
||||
step = introTargetOffset / introTargetDistance * moveRate;
|
||||
}
|
||||
}
|
||||
|
||||
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
|
||||
let nextPosition = clamp(agent.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 {
|
||||
rotation = 3.14159265359 + random.a - 0.5;
|
||||
rotation = 3.14159265359 + random_float(randomSeed + 22695477u) - 0.5;
|
||||
}
|
||||
|
||||
let sourceBelow = textureLoad(sourceMap, vec2<i32>(nextPosition), 0);
|
||||
let sourceBelowStrength = clamp(dot(sourceBelow.rgb, positiveReactionMask), 0.0, 1.0);
|
||||
let trailWeight = settings.individualTrailWeight * (1.0 + sourceBelowStrength * 16.0);
|
||||
let trailWeight =
|
||||
settings.individualTrailWeight *
|
||||
(1.0 + sourceBelowStrength * settings.sourceTrailWeightMultiplier);
|
||||
var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0);
|
||||
trailBelow = vec4<f32>(
|
||||
trailBelow.rgb + channelMask * trailWeight,
|
||||
max(trailBelow.a, 0.0)
|
||||
);
|
||||
|
||||
agent.angle += rotation;
|
||||
agent.position = nextPosition;
|
||||
|
||||
textureStore(trailMapOut, vec2<i32>(nextPosition), trailBelow);
|
||||
agents[id] = agent;
|
||||
agents[id].angle = angle + rotation;
|
||||
agents[id].position = nextPosition;
|
||||
}
|
||||
|
||||
fn sensor_position(agentPosition: vec2<f32>, agentAngle: f32, sensorOffset: f32, sensorOffsetAngle: f32) -> vec2<i32> {
|
||||
let sensorAngle = agentAngle + sensorOffsetAngle;
|
||||
fn sensor_position(agentPosition: vec2<f32>, direction: vec2<f32>, sensorOffset: f32) -> vec2<i32> {
|
||||
return vec2<i32>(clamp(
|
||||
agentPosition + vec2(cos(sensorAngle), sin(sensorAngle)) * sensorOffset,
|
||||
agentPosition + direction * sensorOffset,
|
||||
vec2<f32>(0, 0),
|
||||
state.size - vec2<f32>(1, 1)
|
||||
));
|
||||
}
|
||||
|
||||
fn rotate_direction(direction: vec2<f32>, angleSin: f32, angleCos: f32) -> vec2<f32> {
|
||||
return vec2<f32>(
|
||||
direction.x * angleCos - direction.y * angleSin,
|
||||
direction.x * angleSin + direction.y * angleCos
|
||||
);
|
||||
}
|
||||
|
||||
fn get_channel_mask(colorIndex: f32) -> vec3<f32> {
|
||||
if colorIndex < 0.5 {
|
||||
return vec3<f32>(1, 0, 0);
|
||||
|
|
@ -183,15 +241,9 @@ fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 {
|
|||
return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle));
|
||||
}
|
||||
|
||||
fn random_vec4(id: u32, time: f32) -> vec4<f32> {
|
||||
let timeSeed = u32(time * 0.34816);
|
||||
let seed = id * 747796405u + timeSeed * 2891336453u;
|
||||
return vec4<f32>(
|
||||
random_float(seed),
|
||||
random_float(seed + 1013904223u),
|
||||
random_float(seed + 1664525u),
|
||||
random_float(seed + 22695477u)
|
||||
);
|
||||
fn random_seed(id: u32, time: f32) -> u32 {
|
||||
let timeSeed = u32(time * settings.randomTimeScale);
|
||||
return id * 747796405u + timeSeed * 2891336453u;
|
||||
}
|
||||
|
||||
fn random_float(seed: u32) -> f32 {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ interface LineSegment {
|
|||
}
|
||||
|
||||
export class BrushPipeline {
|
||||
private static readonly UNIFORM_COUNT = 8;
|
||||
private static readonly UNIFORM_COUNT = 16;
|
||||
private static readonly MAX_LINE_COUNT = appConfig.pipelines.brush.maxLineCount;
|
||||
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
|
||||
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 6;
|
||||
private static readonly FEATHER_RADIUS_RATIO = 0.22;
|
||||
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 4;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly bindGroup: GPUBindGroup;
|
||||
|
|
@ -91,22 +90,40 @@ export class BrushPipeline {
|
|||
public setParameters({
|
||||
brushSize,
|
||||
brushSizeVariation,
|
||||
brushAlpha,
|
||||
brushFeatherRatio,
|
||||
brushMinimumFeather,
|
||||
brushDiscardThreshold,
|
||||
brushCoarseNoiseScale,
|
||||
brushGrainNoiseScale,
|
||||
brushGrainNoiseOffsetX,
|
||||
brushGrainNoiseOffsetY,
|
||||
brushGrainMinStrength,
|
||||
brushGrainMaxStrength,
|
||||
selectedColorIndex,
|
||||
}: BrushSettings & { selectedColorIndex: number }) {
|
||||
const brushRadius = brushSize / 2;
|
||||
const brushRadiusVariation = Math.floor(brushRadius * brushSizeVariation);
|
||||
const brushFeather = Math.max(1, brushRadius * BrushPipeline.FEATHER_RADIUS_RATIO);
|
||||
const brushFeather = Math.max(brushMinimumFeather, brushRadius * brushFeatherRatio);
|
||||
const brushGeometryRadius =
|
||||
brushRadius + Math.max(0, brushRadiusVariation) + brushFeather;
|
||||
|
||||
this.uniformValues[0] = brushRadius;
|
||||
this.uniformValues[1] = brushRadiusVariation;
|
||||
this.uniformValues[2] = 0;
|
||||
this.uniformValues[3] = 0;
|
||||
this.uniformValues[2] = brushFeatherRatio;
|
||||
this.uniformValues[3] = brushMinimumFeather;
|
||||
this.uniformValues[4] = selectedColorIndex === 0 ? 1 : 0;
|
||||
this.uniformValues[5] = selectedColorIndex === 1 ? 1 : 0;
|
||||
this.uniformValues[6] = selectedColorIndex === 2 ? 1 : 0;
|
||||
this.uniformValues[7] = 1;
|
||||
this.uniformValues[7] = brushAlpha;
|
||||
this.uniformValues[8] = brushCoarseNoiseScale;
|
||||
this.uniformValues[9] = brushGrainNoiseScale;
|
||||
this.uniformValues[10] = brushGrainNoiseOffsetX;
|
||||
this.uniformValues[11] = brushGrainNoiseOffsetY;
|
||||
this.uniformValues[12] = brushDiscardThreshold;
|
||||
this.uniformValues[13] = brushGrainMinStrength;
|
||||
this.uniformValues[14] = brushGrainMaxStrength;
|
||||
this.uniformValues[15] = brushGeometryRadius;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
|
|
@ -133,8 +150,7 @@ export class BrushPipeline {
|
|||
this.vertexUploadData,
|
||||
floatOffset,
|
||||
segment.from,
|
||||
segment.to,
|
||||
brushGeometryRadius
|
||||
segment.to
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -167,84 +183,8 @@ export class BrushPipeline {
|
|||
target: Float32Array,
|
||||
offset: number,
|
||||
from: vec2,
|
||||
to: vec2,
|
||||
width: number
|
||||
): number {
|
||||
const dx = to[0] - from[0];
|
||||
const dy = to[1] - from[1];
|
||||
const length = Math.hypot(dx, dy);
|
||||
const directionX = length > 0 ? dx / length : 1;
|
||||
const directionY = length > 0 ? dy / length : 0;
|
||||
const scaledDirectionX = directionX * width;
|
||||
const scaledDirectionY = directionY * width;
|
||||
const perpendicularX = directionY * width;
|
||||
const perpendicularY = -directionX * width;
|
||||
|
||||
const startX = from[0] - scaledDirectionX;
|
||||
const startY = from[1] - scaledDirectionY;
|
||||
const endX = to[0] + scaledDirectionX;
|
||||
const endY = to[1] + scaledDirectionY;
|
||||
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX + perpendicularX,
|
||||
startY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
return this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX - perpendicularX,
|
||||
endY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
}
|
||||
|
||||
private writeVertex(
|
||||
target: Float32Array,
|
||||
offset: number,
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
from: vec2,
|
||||
to: vec2
|
||||
): number {
|
||||
target[offset++] = screenX;
|
||||
target[offset++] = screenY;
|
||||
target[offset++] = from[0];
|
||||
target[offset++] = from[1];
|
||||
target[offset++] = to[0];
|
||||
|
|
@ -285,7 +225,7 @@ export class BrushPipeline {
|
|||
this.commonState.execute(passEncoder);
|
||||
passEncoder.setBindGroup(1, this.bindGroup);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT * this.lineCount, 1);
|
||||
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +248,9 @@ export class BrushPipeline {
|
|||
entryPoint: 'vertex',
|
||||
buffers: [
|
||||
{
|
||||
arrayStride: Float32Array.BYTES_PER_ELEMENT * 6,
|
||||
arrayStride:
|
||||
Float32Array.BYTES_PER_ELEMENT * BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT,
|
||||
stepMode: 'instance',
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 0,
|
||||
|
|
@ -320,11 +262,6 @@ export class BrushPipeline {
|
|||
format: 'float32x2',
|
||||
offset: Float32Array.BYTES_PER_ELEMENT * 2,
|
||||
},
|
||||
{
|
||||
shaderLocation: 2,
|
||||
format: 'float32x2',
|
||||
offset: Float32Array.BYTES_PER_ELEMENT * 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -366,7 +303,7 @@ export class BrushPipeline {
|
|||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: {
|
||||
type: 'uniform',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
export interface BrushSettings {
|
||||
brushSize: number;
|
||||
brushSizeVariation: number;
|
||||
brushAlpha: number;
|
||||
brushFeatherRatio: number;
|
||||
brushMinimumFeather: number;
|
||||
brushDiscardThreshold: number;
|
||||
brushCoarseNoiseScale: number;
|
||||
brushGrainNoiseScale: number;
|
||||
brushGrainNoiseOffsetX: number;
|
||||
brushGrainNoiseOffsetY: number;
|
||||
brushGrainMinStrength: number;
|
||||
brushGrainMaxStrength: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
struct Settings {
|
||||
brushSize: f32,
|
||||
brushSizeVariation: f32,
|
||||
padding0: f32,
|
||||
padding1: f32,
|
||||
brushFeatherRatio: f32,
|
||||
brushMinimumFeather: f32,
|
||||
brushValue: vec4<f32>,
|
||||
brushCoarseNoiseScale: f32,
|
||||
brushGrainNoiseScale: f32,
|
||||
brushGrainNoiseOffsetX: f32,
|
||||
brushGrainNoiseOffsetY: f32,
|
||||
brushDiscardThreshold: f32,
|
||||
brushGrainMinStrength: f32,
|
||||
brushGrainMaxStrength: f32,
|
||||
brushGeometryRadius: f32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
|
|
@ -11,8 +19,8 @@ struct Settings {
|
|||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) start: vec2<f32>,
|
||||
@location(2) end: vec2<f32>
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
}
|
||||
|
||||
struct BrushTargets {
|
||||
|
|
@ -22,10 +30,11 @@ struct BrushTargets {
|
|||
|
||||
@vertex
|
||||
fn vertex(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@builtin(vertex_index) vertexIndex: u32,
|
||||
@location(0) start: vec2<f32>,
|
||||
@location(1) end: vec2<f32>
|
||||
) -> VertexOutput {
|
||||
let screenPosition = segment_vertex_position(vertexIndex, start, end, settings.brushGeometryRadius);
|
||||
let uv = screenPosition / state.size;
|
||||
let position = vec2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
||||
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, end);
|
||||
|
|
@ -34,12 +43,17 @@ fn vertex(
|
|||
@fragment
|
||||
fn fragmentMrt(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) start: vec2<f32>,
|
||||
@location(2) end: vec2<f32>
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
) -> BrushTargets {
|
||||
let strength = brushStrength(screenPosition, start, end);
|
||||
let distanceSquared = distanceSquaredFromLine(screenPosition, start, end);
|
||||
if distanceSquared > settings.brushGeometryRadius * settings.brushGeometryRadius {
|
||||
discard;
|
||||
}
|
||||
|
||||
if(strength < 0.02) {
|
||||
let strength = brushStrength(screenPosition, start, end, distanceSquared);
|
||||
|
||||
if(strength < settings.brushDiscardThreshold) {
|
||||
discard;
|
||||
}
|
||||
|
||||
|
|
@ -47,33 +61,77 @@ fn fragmentMrt(
|
|||
return BrushTargets(color, color);
|
||||
}
|
||||
|
||||
fn brushStrength(screenPosition: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
|
||||
let distance = distanceFromLine(screenPosition, start, end);
|
||||
let coarseNoise = textureSample(noise, noiseSampler, fract(screenPosition / 160.0)).r;
|
||||
fn brushStrength(
|
||||
screenPosition: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
end: vec2<f32>,
|
||||
distanceSquared: f32
|
||||
) -> f32 {
|
||||
let distance = sqrt(distanceSquared);
|
||||
let coarseNoise = textureSample(
|
||||
noise,
|
||||
noiseSampler,
|
||||
fract(screenPosition / settings.brushCoarseNoiseScale)
|
||||
).r;
|
||||
let grainNoise = textureSample(
|
||||
noise,
|
||||
noiseSampler,
|
||||
fract(screenPosition / 22.0 + vec2(0.31, 0.67))
|
||||
fract(
|
||||
screenPosition / settings.brushGrainNoiseScale +
|
||||
vec2(settings.brushGrainNoiseOffsetX, settings.brushGrainNoiseOffsetY)
|
||||
)
|
||||
).r;
|
||||
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation * 2.0;
|
||||
let feather = max(1.0, settings.brushSize * 0.22);
|
||||
let feather = max(settings.brushMinimumFeather, settings.brushSize * settings.brushFeatherRatio);
|
||||
let edge = 1.0 - smoothstep(radius - feather, radius + feather, distance);
|
||||
return edge * mix(0.45, 1.0, grainNoise);
|
||||
return edge * mix(settings.brushGrainMinStrength, settings.brushGrainMaxStrength, grainNoise);
|
||||
}
|
||||
|
||||
fn brushOutput(strength: f32) -> vec4<f32> {
|
||||
return vec4(settings.brushValue.rgb * strength, settings.brushValue.a * strength);
|
||||
}
|
||||
|
||||
fn distanceFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
|
||||
fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
|
||||
let pa = position - start;
|
||||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
|
||||
if denominator <= 0.0001 {
|
||||
return length(pa);
|
||||
return dot(pa, pa);
|
||||
}
|
||||
|
||||
let q = clamp(dot(pa, direction) / denominator, 0, 1);
|
||||
return length(pa - direction * q);
|
||||
let nearestOffset = pa - direction * q;
|
||||
return dot(nearestOffset, nearestOffset);
|
||||
}
|
||||
|
||||
fn segment_vertex_position(
|
||||
vertexIndex: u32,
|
||||
start: vec2<f32>,
|
||||
end: vec2<f32>,
|
||||
radius: f32
|
||||
) -> vec2<f32> {
|
||||
let directionVector = end - start;
|
||||
let segmentLength = length(directionVector);
|
||||
var direction = vec2<f32>(1.0, 0.0);
|
||||
if segmentLength > 0.0 {
|
||||
direction = directionVector / segmentLength;
|
||||
}
|
||||
let perpendicular = vec2<f32>(direction.y, -direction.x);
|
||||
let corner = segment_vertex_corner(vertexIndex % 6u);
|
||||
let center = mix(start, end, (corner.x + 1.0) * 0.5);
|
||||
return center + direction * corner.x * radius + perpendicular * corner.y * radius;
|
||||
}
|
||||
|
||||
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
||||
if index == 0u {
|
||||
return vec2<f32>(-1.0, 1.0);
|
||||
}
|
||||
if index == 1u || index == 3u {
|
||||
return vec2<f32>(-1.0, -1.0);
|
||||
}
|
||||
if index == 2u || index == 4u {
|
||||
return vec2<f32>(1.0, 1.0);
|
||||
}
|
||||
return vec2<f32>(1.0, -1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { appConfig } from '../../config';
|
||||
import {
|
||||
createCachedFloat32BufferWrite,
|
||||
writeFloat32BufferIfChanged,
|
||||
|
|
@ -8,7 +9,6 @@ import { generateNoise } from '../../utils/graphics/noise';
|
|||
|
||||
export class CommonState {
|
||||
private static readonly UNIFORM_COUNT = 4;
|
||||
private static readonly NOISE_TEXTURE_SIZE = 2048;
|
||||
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(CommonState.UNIFORM_COUNT);
|
||||
|
|
@ -40,8 +40,8 @@ export class CommonState {
|
|||
|
||||
this.noise = generateNoise({
|
||||
device,
|
||||
width: CommonState.NOISE_TEXTURE_SIZE,
|
||||
height: CommonState.NOISE_TEXTURE_SIZE,
|
||||
width: appConfig.pipelines.common.noiseTextureSize,
|
||||
height: appConfig.pipelines.common.noiseTextureSize,
|
||||
});
|
||||
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import {
|
||||
createCachedFloat32BufferWrite,
|
||||
writeFloat32BufferIfChanged,
|
||||
} from '../../utils/graphics/cached-buffer-write';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import shader from './copy.wgsl?raw';
|
||||
|
||||
export class CopyPipeline {
|
||||
private static readonly UNIFORM_COUNT = 2;
|
||||
private static readonly DEFAULT_SCALE = vec2.fromValues(1, 1);
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(CopyPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
CopyPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly sampler: GPUSampler;
|
||||
|
||||
private readonly vertexBuffer: GPUBuffer;
|
||||
|
||||
private readonly bindGroupsByInput = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
|
||||
public constructor(private readonly device: GPUDevice) {
|
||||
this.bindGroupLayout = device.createBindGroupLayout(CopyPipeline.bindGroupLayout);
|
||||
|
||||
this.uniforms = this.device.createBuffer({
|
||||
size: CopyPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
this.sampler = this.device.createSampler({
|
||||
magFilter: 'linear',
|
||||
minFilter: 'linear',
|
||||
});
|
||||
|
||||
this.vertexBuffer = device.createBuffer({
|
||||
size: 2 * 4 * Float32Array.BYTES_PER_ELEMENT, // 4 x vec2<f32>
|
||||
usage: GPUBufferUsage.VERTEX,
|
||||
mappedAtCreation: true,
|
||||
});
|
||||
// prettier-ignore
|
||||
const vertexData = [
|
||||
// U V
|
||||
0.0, 1.0,
|
||||
1.0, 1.0,
|
||||
0.0, 0.0,
|
||||
1.0, 0.0,
|
||||
];
|
||||
new Float32Array(this.vertexBuffer.getMappedRange()).set(vertexData);
|
||||
this.vertexBuffer.unmap();
|
||||
|
||||
const shaderModule = smartCompile(device, shader);
|
||||
this.pipeline = device.createRenderPipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: 'vertex',
|
||||
buffers: [
|
||||
{
|
||||
arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT,
|
||||
stepMode: 'vertex',
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 0,
|
||||
offset: 0,
|
||||
format: 'float32x2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public execute(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
trailMapIn: GPUTextureView,
|
||||
trailMapOut: GPUTextureView,
|
||||
scale: vec2 = CopyPipeline.DEFAULT_SCALE
|
||||
) {
|
||||
this.uniformValues[0] = scale[0];
|
||||
this.uniformValues[1] = scale[1];
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
this.uniformValues,
|
||||
this.uniformCache
|
||||
);
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: trailMapOut,
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const bindGroup = this.getBindGroup(trailMapIn);
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
passEncoder.setBindGroup(0, bindGroup);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.vertexBuffer.destroy();
|
||||
this.uniforms.destroy();
|
||||
}
|
||||
|
||||
private getBindGroup(trailMapIn: GPUTextureView): GPUBindGroup {
|
||||
const cached = this.bindGroupsByInput.get(trailMapIn);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: {
|
||||
buffer: this.uniforms,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: this.sampler,
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: trailMapIn,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.bindGroupsByInput.set(trailMapIn, bindGroup);
|
||||
return bindGroup;
|
||||
}
|
||||
|
||||
private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor {
|
||||
return {
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.VERTEX,
|
||||
buffer: {
|
||||
type: 'uniform',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
sampler: {
|
||||
type: 'filtering',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
texture: {
|
||||
sampleType: 'float',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(@location(0) uv: vec2<f32>) -> VertexOutput {
|
||||
let ndc = uv * sourceScaler * vec2(2) - vec2(1);
|
||||
return VertexOutput(vec4(ndc.x, -ndc.y, 0, 1), uv);
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> sourceScaler: vec2<f32>;
|
||||
@group(0) @binding(1) var Sampler: sampler;
|
||||
@group(0) @binding(2) var original: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
return textureSample(original, Sampler, uv);
|
||||
}
|
||||
|
|
@ -3,56 +3,96 @@ struct Settings {
|
|||
decayRateTrails: f32,
|
||||
inverseDiffusionRateBrush: f32,
|
||||
decayRateBrush: f32,
|
||||
diffusionNeighborDivisor: f32,
|
||||
brushDecayAlphaOffset: f32,
|
||||
padding0: f32,
|
||||
padding1: f32,
|
||||
};
|
||||
|
||||
const WORKGROUP_SIZE_X = 16u;
|
||||
const WORKGROUP_SIZE_Y = 16u;
|
||||
const TILE_SIZE_X = WORKGROUP_SIZE_X + 2u;
|
||||
const TILE_SIZE_Y = WORKGROUP_SIZE_Y + 2u;
|
||||
const TILE_TEXEL_COUNT = TILE_SIZE_X * TILE_SIZE_Y;
|
||||
|
||||
@group(0) @binding(0) var<uniform> settings: Settings;
|
||||
@group(0) @binding(2) var trailMap: texture_2d<f32>;
|
||||
@group(0) @binding(1) var trailMap: texture_2d<f32>;
|
||||
@group(0) @binding(2) var trailMapOut: texture_storage_2d<rgba16float, write>;
|
||||
|
||||
var<workgroup> tile: array<vec4<f32>, 324>;
|
||||
|
||||
@fragment
|
||||
fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn main(
|
||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
||||
@builtin(local_invocation_id) local_id: vec3<u32>,
|
||||
@builtin(workgroup_id) workgroup_id: vec3<u32>
|
||||
) {
|
||||
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
|
||||
let pixel = clamp(vec2<i32>(position.xy), vec2<i32>(0), textureSize - vec2<i32>(1));
|
||||
var current = textureLoad(trailMap, pixel, 0);
|
||||
let localLinearIndex = local_id.y * WORKGROUP_SIZE_X + local_id.x;
|
||||
var tileIndex = localLinearIndex;
|
||||
|
||||
loop {
|
||||
if tileIndex >= TILE_TEXEL_COUNT {
|
||||
break;
|
||||
}
|
||||
|
||||
let tilePosition = vec2<u32>(tileIndex % TILE_SIZE_X, tileIndex / TILE_SIZE_X);
|
||||
let sourcePixelU32 =
|
||||
workgroup_id.xy * vec2<u32>(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y) +
|
||||
tilePosition;
|
||||
let sourcePixel = clamp(
|
||||
vec2<i32>(i32(sourcePixelU32.x), i32(sourcePixelU32.y)) - vec2<i32>(1, 1),
|
||||
vec2<i32>(0, 0),
|
||||
textureSize - vec2<i32>(1, 1)
|
||||
);
|
||||
tile[tileIndex] = textureLoad(trailMap, sourcePixel, 0);
|
||||
tileIndex += WORKGROUP_SIZE_X * WORKGROUP_SIZE_Y;
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
let pixel = vec2<i32>(i32(global_id.x), i32(global_id.y));
|
||||
if pixel.x >= textureSize.x || pixel.y >= textureSize.y {
|
||||
return;
|
||||
}
|
||||
|
||||
let centerTilePosition = local_id.xy + vec2<u32>(1u, 1u);
|
||||
let centerTileIndex = centerTilePosition.y * TILE_SIZE_X + centerTilePosition.x;
|
||||
var current = tile[centerTileIndex];
|
||||
let random = random_from_pixel(pixel);
|
||||
let trailWeight = diffusion_weight(random, settings.inverseDiffusionRateTrails);
|
||||
let brushWeight = diffusion_weight(random, settings.inverseDiffusionRateBrush);
|
||||
|
||||
current += (
|
||||
propagate(pixel, textureSize, vec2<i32>(-1, -1), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(-1, 1), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(1, -1), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(1, 1), current, trailWeight, brushWeight)
|
||||
propagate(centerTileIndex, -1, -1, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, -1, 1, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, 1, -1, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, 1, 1, current, trailWeight, brushWeight)
|
||||
|
||||
+ propagate(pixel, textureSize, vec2<i32>(-1, 0), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(0, -1), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(1, 0), current, trailWeight, brushWeight)
|
||||
+ propagate(pixel, textureSize, vec2<i32>(0, 1), current, trailWeight, brushWeight)
|
||||
) / 8;
|
||||
+ propagate(centerTileIndex, -1, 0, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, 0, -1, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, 1, 0, current, trailWeight, brushWeight)
|
||||
+ propagate(centerTileIndex, 0, 1, current, trailWeight, brushWeight)
|
||||
) / max(1.0, settings.diffusionNeighborDivisor);
|
||||
|
||||
let decayed = clamp(vec4(
|
||||
current.rgb * settings.decayRateTrails,
|
||||
max(0, current.a + (current.a - 1.001) * settings.decayRateBrush)
|
||||
max(0, current.a + (current.a - settings.brushDecayAlphaOffset) * settings.decayRateBrush)
|
||||
), vec4(0), vec4(1));
|
||||
|
||||
return decayed;
|
||||
|
||||
textureStore(trailMapOut, pixel, decayed);
|
||||
}
|
||||
|
||||
|
||||
fn propagate(
|
||||
pixel: vec2<i32>,
|
||||
textureSize: vec2<i32>,
|
||||
offset: vec2<i32>,
|
||||
centerTileIndex: u32,
|
||||
offsetX: i32,
|
||||
offsetY: i32,
|
||||
currentColor: vec4<f32>,
|
||||
trailWeight: f32,
|
||||
brushWeight: f32
|
||||
) -> vec4<f32> {
|
||||
let neighbour = textureLoad(
|
||||
trailMap,
|
||||
clamp(pixel + offset, vec2<i32>(0), textureSize - vec2<i32>(1)),
|
||||
0
|
||||
);
|
||||
let neighbourIndex = i32(centerTileIndex) + offsetY * i32(TILE_SIZE_X) + offsetX;
|
||||
let neighbour = tile[u32(neighbourIndex)];
|
||||
let difference = clamp(neighbour - currentColor, vec4(0), vec4(1));
|
||||
|
||||
return vec4(
|
||||
|
|
@ -72,9 +112,6 @@ fn random_from_pixel(pixel: vec2<i32>) -> f32 {
|
|||
|
||||
fn diffusion_weight(random: f32, inverseRate: f32) -> f32 {
|
||||
let r = clamp(random, 0.0, 1.0);
|
||||
let r2 = r * r;
|
||||
let r4 = r2 * r2;
|
||||
let r8 = r4 * r4;
|
||||
|
||||
if inverseRate < 1.0 {
|
||||
let rootApproximation = r / max(0.5 + r * 0.5, 0.0001);
|
||||
|
|
@ -85,14 +122,17 @@ fn diffusion_weight(random: f32, inverseRate: f32) -> f32 {
|
|||
);
|
||||
}
|
||||
|
||||
let r2 = r * r;
|
||||
if inverseRate < 2.0 {
|
||||
return mix(r, r2, inverseRate - 1.0);
|
||||
}
|
||||
|
||||
let r4 = r2 * r2;
|
||||
if inverseRate < 4.0 {
|
||||
return mix(r2, r4, (inverseRate - 2.0) * 0.5);
|
||||
}
|
||||
|
||||
let r8 = r4 * r4;
|
||||
if inverseRate < 8.0 {
|
||||
return mix(r4, r8, (inverseRate - 4.0) * 0.25);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ import {
|
|||
|
||||
describe('diffusion pipeline parameters', () => {
|
||||
it('keeps zero diffusion rates finite before writing shader uniforms', () => {
|
||||
const uniformValues = new Float32Array(4);
|
||||
const uniformValues = new Float32Array(8);
|
||||
|
||||
setDiffusionUniformValues(uniformValues, {
|
||||
brushDecayAlphaOffset: 1.001,
|
||||
decayRateBrush: 900,
|
||||
decayRateTrails: 970,
|
||||
diffusionDecayRateDivisor: 1000,
|
||||
diffusionNeighborDivisor: 8,
|
||||
diffusionRateBrush: 0,
|
||||
diffusionRateTrails: 0,
|
||||
});
|
||||
|
|
@ -28,7 +31,9 @@ describe('diffusion pipeline parameters', () => {
|
|||
expect(getSafeInverseDiffusionRate(0.25)).toBe(4);
|
||||
});
|
||||
|
||||
it('keeps the diffusion shader on the low-cost trail sampling path', () => {
|
||||
it('keeps the diffusion shader on the tiled compute sampling path', () => {
|
||||
expect(shader).toContain('@compute @workgroup_size(16, 16)');
|
||||
expect(shader).toContain('var<workgroup> tile');
|
||||
expect(shader).toContain('textureLoad');
|
||||
expect(shader).not.toContain('textureSample');
|
||||
expect(shader).not.toContain('pow(');
|
||||
|
|
@ -37,7 +42,8 @@ describe('diffusion pipeline parameters', () => {
|
|||
|
||||
it('keeps shader resource groups aligned with the simplified pipeline layout', () => {
|
||||
expect(shader).toContain('@group(0) @binding(0) var<uniform> settings');
|
||||
expect(shader).toContain('@group(0) @binding(2) var trailMap');
|
||||
expect(shader).toContain('@group(0) @binding(1) var trailMap');
|
||||
expect(shader).toContain('@group(0) @binding(2) var trailMapOut');
|
||||
expect(shader).not.toContain('@group(1)');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { appConfig } from '../../config';
|
||||
import {
|
||||
createCachedFloat32BufferWrite,
|
||||
writeFloat32BufferIfChanged,
|
||||
} from '../../utils/graphics/cached-buffer-write';
|
||||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad';
|
||||
import { getWorkgroupCount } from '../../utils/graphics/get-workgroup-count';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import shader from './diffuse.wgsl?raw';
|
||||
import { DiffusionSettings } from './diffusion-settings';
|
||||
|
||||
const MIN_DIFFUSION_RATE = appConfig.pipelines.diffusion.minDiffusionRate;
|
||||
|
||||
type DiffusionUniformSettings = Pick<
|
||||
DiffusionSettings,
|
||||
'diffusionRateTrails' | 'decayRateTrails' | 'diffusionRateBrush' | 'decayRateBrush'
|
||||
| 'diffusionRateTrails'
|
||||
| 'decayRateTrails'
|
||||
| 'diffusionRateBrush'
|
||||
| 'decayRateBrush'
|
||||
| 'diffusionDecayRateDivisor'
|
||||
| 'diffusionNeighborDivisor'
|
||||
| 'brushDecayAlphaOffset'
|
||||
>;
|
||||
|
||||
export const getSafeInverseDiffusionRate = (diffusionRate: number): number =>
|
||||
1 /
|
||||
(Number.isFinite(diffusionRate) && diffusionRate > MIN_DIFFUSION_RATE
|
||||
(Number.isFinite(diffusionRate) &&
|
||||
diffusionRate > appConfig.pipelines.diffusion.minDiffusionRate
|
||||
? diffusionRate
|
||||
: MIN_DIFFUSION_RATE);
|
||||
: appConfig.pipelines.diffusion.minDiffusionRate);
|
||||
|
||||
export const setDiffusionUniformValues = (
|
||||
target: Float32Array,
|
||||
|
|
@ -28,52 +35,48 @@ export const setDiffusionUniformValues = (
|
|||
decayRateTrails,
|
||||
diffusionRateBrush,
|
||||
decayRateBrush,
|
||||
diffusionDecayRateDivisor,
|
||||
diffusionNeighborDivisor,
|
||||
brushDecayAlphaOffset,
|
||||
}: DiffusionUniformSettings
|
||||
): void => {
|
||||
const decayDivisor = Math.max(Number.EPSILON, diffusionDecayRateDivisor);
|
||||
target[0] = getSafeInverseDiffusionRate(diffusionRateTrails);
|
||||
target[1] = decayRateTrails / 1000;
|
||||
target[1] = decayRateTrails / decayDivisor;
|
||||
target[2] = getSafeInverseDiffusionRate(diffusionRateBrush);
|
||||
target[3] = decayRateBrush / 1000;
|
||||
target[3] = decayRateBrush / decayDivisor;
|
||||
target[4] = diffusionNeighborDivisor;
|
||||
target[5] = brushDecayAlphaOffset;
|
||||
};
|
||||
|
||||
export class DiffusionPipeline {
|
||||
private static readonly UNIFORM_COUNT = 4;
|
||||
private static readonly WORKGROUP_SIZE = 16;
|
||||
private static readonly UNIFORM_COUNT = 8;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly pipeline: GPUComputePipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(DiffusionPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
DiffusionPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly vertexBuffer: GPUBuffer;
|
||||
|
||||
private readonly bindGroupsByInput = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
private readonly bindGroupsByInput = new WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, GPUBindGroup>
|
||||
>();
|
||||
|
||||
public constructor(private readonly device: GPUDevice) {
|
||||
this.bindGroupLayout = device.createBindGroupLayout(
|
||||
DiffusionPipeline.bindGroupLayout
|
||||
);
|
||||
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
this.vertexBuffer = buffer;
|
||||
|
||||
this.pipeline = device.createRenderPipeline({
|
||||
this.pipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
vertex,
|
||||
fragment: {
|
||||
compute: {
|
||||
module: smartCompile(device, shader),
|
||||
entryPoint: 'fragment',
|
||||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
},
|
||||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
entryPoint: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -88,12 +91,18 @@ export class DiffusionPipeline {
|
|||
decayRateTrails,
|
||||
diffusionRateBrush,
|
||||
decayRateBrush,
|
||||
diffusionDecayRateDivisor,
|
||||
diffusionNeighborDivisor,
|
||||
brushDecayAlphaOffset,
|
||||
}: DiffusionSettings) {
|
||||
setDiffusionUniformValues(this.uniformValues, {
|
||||
diffusionRateTrails,
|
||||
decayRateTrails,
|
||||
diffusionRateBrush,
|
||||
decayRateBrush,
|
||||
diffusionDecayRateDivisor,
|
||||
diffusionNeighborDivisor,
|
||||
brushDecayAlphaOffset,
|
||||
});
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
|
|
@ -106,31 +115,32 @@ export class DiffusionPipeline {
|
|||
public execute(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
trailMapIn: GPUTextureView,
|
||||
trailMapOut: GPUTextureView
|
||||
trailMapOut: GPUTextureView,
|
||||
size: vec2
|
||||
) {
|
||||
const bindGroup = this.getBindGroup(trailMapIn);
|
||||
const bindGroup = this.getBindGroup(trailMapIn, trailMapOut);
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
view: trailMapOut,
|
||||
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.setBindGroup(0, bindGroup);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.dispatchWorkgroups(
|
||||
getWorkgroupCount(size[0], DiffusionPipeline.WORKGROUP_SIZE),
|
||||
getWorkgroupCount(size[1], DiffusionPipeline.WORKGROUP_SIZE)
|
||||
);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
private getBindGroup(trailMapIn: GPUTextureView): GPUBindGroup {
|
||||
const cached = this.bindGroupsByInput.get(trailMapIn);
|
||||
private getBindGroup(
|
||||
trailMapIn: GPUTextureView,
|
||||
trailMapOut: GPUTextureView
|
||||
): GPUBindGroup {
|
||||
let outputCache = this.bindGroupsByInput.get(trailMapIn);
|
||||
if (!outputCache) {
|
||||
outputCache = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
this.bindGroupsByInput.set(trailMapIn, outputCache);
|
||||
}
|
||||
|
||||
const cached = outputCache.get(trailMapOut);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
|
@ -145,18 +155,21 @@ export class DiffusionPipeline {
|
|||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
binding: 1,
|
||||
resource: trailMapIn,
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: trailMapOut,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.bindGroupsByInput.set(trailMapIn, bindGroup);
|
||||
outputCache.set(trailMapOut, bindGroup);
|
||||
return bindGroup;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.vertexBuffer.destroy();
|
||||
this.uniforms.destroy();
|
||||
}
|
||||
|
||||
|
|
@ -165,18 +178,26 @@ export class DiffusionPipeline {
|
|||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
visibility: GPUShaderStage.COMPUTE,
|
||||
buffer: {
|
||||
type: 'uniform',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
binding: 1,
|
||||
visibility: GPUShaderStage.COMPUTE,
|
||||
texture: {
|
||||
sampleType: 'float',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: GPUShaderStage.COMPUTE,
|
||||
storageTexture: {
|
||||
access: 'write-only',
|
||||
format: 'rgba16float',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,7 @@ export interface DiffusionSettings {
|
|||
decayRateTrails: number;
|
||||
diffusionRateBrush: number;
|
||||
decayRateBrush: number;
|
||||
diffusionDecayRateDivisor: number;
|
||||
diffusionNeighborDivisor: number;
|
||||
brushDecayAlphaOffset: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export class EraserAgentPipeline {
|
|||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
EraserAgentPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly bindGroupsByMask = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
private readonly bindGroupsByAgentsBuffer = new WeakMap<
|
||||
GPUBuffer,
|
||||
WeakMap<GPUTextureView, GPUBindGroup>
|
||||
>();
|
||||
|
||||
private pendingSegmentCount = 0;
|
||||
private activeSegmentCount = 0;
|
||||
|
|
@ -26,7 +29,7 @@ export class EraserAgentPipeline {
|
|||
|
||||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
private readonly agentsBuffer: GPUBuffer
|
||||
private readonly getAgentsBuffer: () => GPUBuffer
|
||||
) {
|
||||
const emptyBindGroupLayout = device.createBindGroupLayout({ entries: [] });
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
|
|
@ -80,13 +83,19 @@ export class EraserAgentPipeline {
|
|||
this.activeSegmentCount = 0;
|
||||
}
|
||||
|
||||
public setParameters({ agentCount }: { agentCount: number }): void {
|
||||
public setParameters({
|
||||
agentCount,
|
||||
eraserMaskAlphaThreshold,
|
||||
}: {
|
||||
agentCount: number;
|
||||
eraserMaskAlphaThreshold: number;
|
||||
}): void {
|
||||
this.agentCount = agentCount;
|
||||
this.activeSegmentCount = this.pendingSegmentCount;
|
||||
this.pendingSegmentCount = 0;
|
||||
|
||||
this.uniformValues[0] = agentCount;
|
||||
this.uniformValues[1] = 0;
|
||||
this.uniformValues[1] = eraserMaskAlphaThreshold;
|
||||
this.uniformValues[2] = 0;
|
||||
this.uniformValues[3] = 0;
|
||||
writeFloat32BufferIfChanged(
|
||||
|
|
@ -120,7 +129,14 @@ export class EraserAgentPipeline {
|
|||
}
|
||||
|
||||
private getBindGroup(eraserMask: GPUTextureView): GPUBindGroup {
|
||||
const cached = this.bindGroupsByMask.get(eraserMask);
|
||||
const agentsBuffer = this.getAgentsBuffer();
|
||||
let maskCache = this.bindGroupsByAgentsBuffer.get(agentsBuffer);
|
||||
if (!maskCache) {
|
||||
maskCache = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
this.bindGroupsByAgentsBuffer.set(agentsBuffer, maskCache);
|
||||
}
|
||||
|
||||
const cached = maskCache.get(eraserMask);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
|
@ -137,7 +153,7 @@ export class EraserAgentPipeline {
|
|||
{
|
||||
binding: 1,
|
||||
resource: {
|
||||
buffer: this.agentsBuffer,
|
||||
buffer: agentsBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -147,7 +163,7 @@ export class EraserAgentPipeline {
|
|||
],
|
||||
});
|
||||
|
||||
this.bindGroupsByMask.set(eraserMask, bindGroup);
|
||||
maskCache.set(eraserMask, bindGroup);
|
||||
return bindGroup;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
struct Settings {
|
||||
agentCount: f32,
|
||||
padding0: f32,
|
||||
eraserMaskAlphaThreshold: f32,
|
||||
padding1: f32,
|
||||
padding2: f32,
|
||||
};
|
||||
|
|
@ -18,21 +18,20 @@ fn main(
|
|||
return;
|
||||
}
|
||||
|
||||
var agent = agents[id];
|
||||
if agent.colorIndex < 0.0 {
|
||||
let colorIndex = agents[id].colorIndex;
|
||||
if colorIndex < 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let maskSize = vec2<i32>(textureDimensions(eraserMask));
|
||||
let maskPosition = clamp(
|
||||
vec2<i32>(agent.position),
|
||||
vec2<i32>(agents[id].position),
|
||||
vec2<i32>(0, 0),
|
||||
maskSize - vec2<i32>(1, 1)
|
||||
);
|
||||
let maskSample = textureLoad(eraserMask, maskPosition, 0);
|
||||
|
||||
if maskSample.a < 0.5 {
|
||||
agent.colorIndex = -1.0;
|
||||
agents[id] = agent;
|
||||
if maskSample.a < settings.eraserMaskAlphaThreshold {
|
||||
agents[id].colorIndex = -1.0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,14 @@ interface LineSegment {
|
|||
}
|
||||
|
||||
export class EraserTexturePipeline {
|
||||
private static readonly UNIFORM_COUNT = 4;
|
||||
private static readonly UNIFORM_COUNT = 8;
|
||||
private static readonly MAX_LINE_COUNT = appConfig.pipelines.eraser.maxTextureLineCount;
|
||||
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
|
||||
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 6;
|
||||
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 4;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly bindGroup: GPUBindGroup;
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly multiTargetPipeline: GPURenderPipeline;
|
||||
private readonly combinedPipeline: GPURenderPipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(EraserTexturePipeline.UNIFORM_COUNT);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
|
|
@ -47,7 +46,7 @@ export class EraserTexturePipeline {
|
|||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||
buffer: {
|
||||
type: 'uniform',
|
||||
},
|
||||
|
|
@ -65,8 +64,7 @@ export class EraserTexturePipeline {
|
|||
});
|
||||
|
||||
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
|
||||
this.pipeline = this.createPipeline(shaderModule, 'fragment', 1);
|
||||
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 3);
|
||||
this.combinedPipeline = this.createPipeline(shaderModule, 'fragmentCombined', 4);
|
||||
|
||||
this.uniforms = this.device.createBuffer({
|
||||
size: EraserTexturePipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||
|
|
@ -98,13 +96,31 @@ export class EraserTexturePipeline {
|
|||
this.actualSegments.length = 0;
|
||||
}
|
||||
|
||||
public setParameters({ eraserSize }: { eraserSize: number }): void {
|
||||
public setParameters({
|
||||
eraserSize,
|
||||
eraserLineDistanceEpsilon,
|
||||
eraserClearRed,
|
||||
eraserClearGreen,
|
||||
eraserClearBlue,
|
||||
eraserClearAlpha,
|
||||
}: {
|
||||
eraserSize: number;
|
||||
eraserLineDistanceEpsilon: number;
|
||||
eraserClearRed: number;
|
||||
eraserClearGreen: number;
|
||||
eraserClearBlue: number;
|
||||
eraserClearAlpha: number;
|
||||
}): void {
|
||||
const eraserRadius = eraserSize / 2;
|
||||
|
||||
this.uniformValues[0] = eraserRadius * eraserRadius;
|
||||
this.uniformValues[1] = 0;
|
||||
this.uniformValues[2] = 0;
|
||||
this.uniformValues[3] = 0;
|
||||
this.uniformValues[1] = eraserLineDistanceEpsilon;
|
||||
this.uniformValues[2] = eraserClearRed;
|
||||
this.uniformValues[3] = eraserClearGreen;
|
||||
this.uniformValues[4] = eraserClearBlue;
|
||||
this.uniformValues[5] = eraserClearAlpha;
|
||||
this.uniformValues[6] = 0;
|
||||
this.uniformValues[7] = 0;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
|
|
@ -131,8 +147,7 @@ export class EraserTexturePipeline {
|
|||
this.vertexUploadData,
|
||||
floatOffset,
|
||||
segment.from,
|
||||
segment.to,
|
||||
eraserRadius
|
||||
segment.to
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -145,46 +160,60 @@ export class EraserTexturePipeline {
|
|||
);
|
||||
}
|
||||
|
||||
public execute(commandEncoder: GPUCommandEncoder, textureOut: GPUTextureView): void {
|
||||
this.executeWithPipeline(commandEncoder, this.pipeline, [textureOut]);
|
||||
}
|
||||
|
||||
public executeMultiTarget(
|
||||
public executeCombined(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
eraserMaskOut: GPUTextureView,
|
||||
sourceMapOut: GPUTextureView,
|
||||
influenceMapOut: GPUTextureView,
|
||||
trailMapOut: GPUTextureView
|
||||
): void {
|
||||
this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
|
||||
sourceMapOut,
|
||||
influenceMapOut,
|
||||
trailMapOut,
|
||||
]);
|
||||
}
|
||||
|
||||
private executeWithPipeline(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
pipeline: GPURenderPipeline,
|
||||
textureViews: Array<GPUTextureView>
|
||||
): void {
|
||||
if (this.lineCount === 0) {
|
||||
const passEncoder = commandEncoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: eraserMaskOut,
|
||||
clearValue: { r: 1, g: 1, b: 1, a: 1 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
});
|
||||
passEncoder.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: textureViews.map<GPURenderPassColorAttachment>((view) => ({
|
||||
view,
|
||||
loadOp: 'load',
|
||||
storeOp: 'store',
|
||||
})),
|
||||
colorAttachments: [
|
||||
{
|
||||
view: eraserMaskOut,
|
||||
clearValue: { r: 1, g: 1, b: 1, a: 1 },
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
{
|
||||
view: sourceMapOut,
|
||||
loadOp: 'load',
|
||||
storeOp: 'store',
|
||||
},
|
||||
{
|
||||
view: influenceMapOut,
|
||||
loadOp: 'load',
|
||||
storeOp: 'store',
|
||||
},
|
||||
{
|
||||
view: trailMapOut,
|
||||
loadOp: 'load',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(pipeline);
|
||||
passEncoder.setPipeline(this.combinedPipeline);
|
||||
this.commonState.execute(passEncoder);
|
||||
passEncoder.setBindGroup(1, this.bindGroup);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.draw(EraserTexturePipeline.VERTICES_PER_LINE_SEGMENT * this.lineCount, 1);
|
||||
passEncoder.draw(EraserTexturePipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +236,10 @@ export class EraserTexturePipeline {
|
|||
entryPoint: 'vertex',
|
||||
buffers: [
|
||||
{
|
||||
arrayStride: Float32Array.BYTES_PER_ELEMENT * 6,
|
||||
arrayStride:
|
||||
Float32Array.BYTES_PER_ELEMENT *
|
||||
EraserTexturePipeline.ATTRIBUTES_PER_LINE_SEGMENT,
|
||||
stepMode: 'instance',
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 0,
|
||||
|
|
@ -219,11 +251,6 @@ export class EraserTexturePipeline {
|
|||
format: 'float32x2',
|
||||
offset: Float32Array.BYTES_PER_ELEMENT * 2,
|
||||
},
|
||||
{
|
||||
shaderLocation: 2,
|
||||
format: 'float32x2',
|
||||
offset: Float32Array.BYTES_PER_ELEMENT * 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -261,84 +288,8 @@ export class EraserTexturePipeline {
|
|||
target: Float32Array,
|
||||
offset: number,
|
||||
from: vec2,
|
||||
to: vec2,
|
||||
width: number
|
||||
): number {
|
||||
const dx = to[0] - from[0];
|
||||
const dy = to[1] - from[1];
|
||||
const length = Math.hypot(dx, dy);
|
||||
const directionX = length > 0 ? dx / length : 1;
|
||||
const directionY = length > 0 ? dy / length : 0;
|
||||
const scaledDirectionX = directionX * width;
|
||||
const scaledDirectionY = directionY * width;
|
||||
const perpendicularX = directionY * width;
|
||||
const perpendicularY = -directionX * width;
|
||||
|
||||
const startX = from[0] - scaledDirectionX;
|
||||
const startY = from[1] - scaledDirectionY;
|
||||
const endX = to[0] + scaledDirectionX;
|
||||
const endY = to[1] + scaledDirectionY;
|
||||
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX + perpendicularX,
|
||||
startY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
return this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX - perpendicularX,
|
||||
endY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
}
|
||||
|
||||
private writeVertex(
|
||||
target: Float32Array,
|
||||
offset: number,
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
from: vec2,
|
||||
to: vec2
|
||||
): number {
|
||||
target[offset++] = screenX;
|
||||
target[offset++] = screenY;
|
||||
target[offset++] = from[0];
|
||||
target[offset++] = from[1];
|
||||
target[offset++] = to[0];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
struct Settings {
|
||||
eraserRadiusSquared: f32,
|
||||
lineDistanceEpsilon: f32,
|
||||
clearRed: f32,
|
||||
clearGreen: f32,
|
||||
clearBlue: f32,
|
||||
clearAlpha: f32,
|
||||
padding0: f32,
|
||||
padding1: f32,
|
||||
padding2: f32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
|
|
@ -10,8 +14,8 @@ struct Settings {
|
|||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) start: vec2<f32>,
|
||||
@location(2) end: vec2<f32>
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
}
|
||||
|
||||
struct EraserTextureTargets {
|
||||
|
|
@ -20,12 +24,20 @@ struct EraserTextureTargets {
|
|||
@location(2) trail: vec4<f32>,
|
||||
}
|
||||
|
||||
struct EraserCombinedTargets {
|
||||
@location(0) mask: vec4<f32>,
|
||||
@location(1) source: vec4<f32>,
|
||||
@location(2) influence: vec4<f32>,
|
||||
@location(3) trail: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@builtin(vertex_index) vertexIndex: u32,
|
||||
@location(0) start: vec2<f32>,
|
||||
@location(1) end: vec2<f32>
|
||||
) -> VertexOutput {
|
||||
let screenPosition = segment_vertex_position(vertexIndex, start, end, sqrt(settings.eraserRadiusSquared));
|
||||
let uv = screenPosition / state.size;
|
||||
let position = vec2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
||||
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, end);
|
||||
|
|
@ -34,30 +46,53 @@ fn vertex(
|
|||
@fragment
|
||||
fn fragment(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) start: vec2<f32>,
|
||||
@location(2) end: vec2<f32>
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
) -> @location(0) vec4<f32> {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
discard;
|
||||
}
|
||||
|
||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
return getEraserClearValue();
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMrt(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) start: vec2<f32>,
|
||||
@location(2) end: vec2<f32>
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
) -> EraserTextureTargets {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
discard;
|
||||
}
|
||||
|
||||
let cleared = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
let cleared = getEraserClearValue();
|
||||
return EraserTextureTargets(cleared, cleared, cleared);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentCombined(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
) -> EraserCombinedTargets {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
discard;
|
||||
}
|
||||
|
||||
let cleared = getEraserClearValue();
|
||||
return EraserCombinedTargets(cleared, cleared, cleared, cleared);
|
||||
}
|
||||
|
||||
fn getEraserClearValue() -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
settings.clearRed,
|
||||
settings.clearGreen,
|
||||
settings.clearBlue,
|
||||
settings.clearAlpha
|
||||
);
|
||||
}
|
||||
|
||||
fn shouldDiscardEraserFragment(
|
||||
screenPosition: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
|
|
@ -71,7 +106,7 @@ fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>
|
|||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
|
||||
if denominator <= 0.0001 {
|
||||
if denominator <= settings.lineDistanceEpsilon {
|
||||
return dot(pa, pa);
|
||||
}
|
||||
|
||||
|
|
@ -79,3 +114,34 @@ fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>
|
|||
let nearestOffset = pa - direction * q;
|
||||
return dot(nearestOffset, nearestOffset);
|
||||
}
|
||||
|
||||
fn segment_vertex_position(
|
||||
vertexIndex: u32,
|
||||
start: vec2<f32>,
|
||||
end: vec2<f32>,
|
||||
radius: f32
|
||||
) -> vec2<f32> {
|
||||
let directionVector = end - start;
|
||||
let segmentLength = length(directionVector);
|
||||
var direction = vec2<f32>(1.0, 0.0);
|
||||
if segmentLength > 0.0 {
|
||||
direction = directionVector / segmentLength;
|
||||
}
|
||||
let perpendicular = vec2<f32>(direction.y, -direction.x);
|
||||
let corner = segment_vertex_corner(vertexIndex % 6u);
|
||||
let center = mix(start, end, (corner.x + 1.0) * 0.5);
|
||||
return center + direction * corner.x * radius + perpendicular * corner.y * radius;
|
||||
}
|
||||
|
||||
fn segment_vertex_corner(index: u32) -> vec2<f32> {
|
||||
if index == 0u {
|
||||
return vec2<f32>(-1.0, 1.0);
|
||||
}
|
||||
if index == 1u || index == 3u {
|
||||
return vec2<f32>(-1.0, -1.0);
|
||||
}
|
||||
if index == 2u || index == 4u {
|
||||
return vec2<f32>(1.0, 1.0);
|
||||
}
|
||||
return vec2<f32>(1.0, -1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import { RenderSettings } from './render-settings';
|
|||
import shader from './render.wgsl?raw';
|
||||
|
||||
export class RenderPipeline {
|
||||
private static readonly UNIFORM_COUNT = 16;
|
||||
private static readonly UNIFORM_COUNT = 20;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly sampledPipeline: GPURenderPipeline;
|
||||
private readonly sampler: GPUSampler;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
RenderPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly vertexBuffer: GPUBuffer;
|
||||
|
||||
private readonly bindGroupsByTexture = new WeakMap<
|
||||
GPUTextureView,
|
||||
|
|
@ -33,8 +33,7 @@ export class RenderPipeline {
|
|||
) {
|
||||
this.bindGroupLayout = device.createBindGroupLayout(RenderPipeline.bindGroupLayout);
|
||||
|
||||
const { buffer, vertex } = setUpFullScreenQuad(device);
|
||||
this.vertexBuffer = buffer;
|
||||
const vertex = setUpFullScreenQuad(device);
|
||||
|
||||
this.sampler = device.createSampler({
|
||||
magFilter: 'linear',
|
||||
|
|
@ -42,7 +41,8 @@ export class RenderPipeline {
|
|||
});
|
||||
|
||||
const format = navigator.gpu.getPreferredCanvasFormat();
|
||||
this.pipeline = this.createPipeline(format, vertex);
|
||||
this.pipeline = this.createPipeline(format, vertex, 'fragment');
|
||||
this.sampledPipeline = this.createPipeline(format, vertex, 'fragmentSampled');
|
||||
|
||||
this.uniforms = this.device.createBuffer({
|
||||
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||
|
|
@ -52,7 +52,8 @@ export class RenderPipeline {
|
|||
|
||||
private createPipeline(
|
||||
format: GPUTextureFormat,
|
||||
vertex: GPUVertexState
|
||||
vertex: GPUVertexState,
|
||||
fragmentEntryPoint: string
|
||||
): GPURenderPipeline {
|
||||
return this.device.createRenderPipeline({
|
||||
layout: this.device.createPipelineLayout({
|
||||
|
|
@ -61,7 +62,7 @@ export class RenderPipeline {
|
|||
vertex,
|
||||
fragment: {
|
||||
module: smartCompile(this.device, CommonState.shaderCode, shader),
|
||||
entryPoint: 'fragment',
|
||||
entryPoint: fragmentEntryPoint,
|
||||
targets: [
|
||||
{
|
||||
format,
|
||||
|
|
@ -69,7 +70,7 @@ export class RenderPipeline {
|
|||
],
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-strip',
|
||||
topology: 'triangle-list',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -78,6 +79,10 @@ export class RenderPipeline {
|
|||
channelColors,
|
||||
backgroundColor,
|
||||
clarity,
|
||||
renderTraceNormalizationFloor,
|
||||
renderBrushColorBase,
|
||||
renderBrushColorStrengthMultiplier,
|
||||
backgroundGrainStrength,
|
||||
}: RenderSettings & {
|
||||
channelColors: Array<[number, number, number]>;
|
||||
backgroundColor: [number, number, number];
|
||||
|
|
@ -99,6 +104,10 @@ export class RenderPipeline {
|
|||
this.uniformValues[13] = backgroundColor[1];
|
||||
this.uniformValues[14] = backgroundColor[2];
|
||||
this.uniformValues[15] = clarity;
|
||||
this.uniformValues[16] = renderTraceNormalizationFloor;
|
||||
this.uniformValues[17] = renderBrushColorBase;
|
||||
this.uniformValues[18] = renderBrushColorStrengthMultiplier;
|
||||
this.uniformValues[19] = backgroundGrainStrength;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
|
|
@ -128,9 +137,8 @@ export class RenderPipeline {
|
|||
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
this.commonState.execute(passEncoder);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.setBindGroup(1, bindGroup);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.draw(3, 1);
|
||||
passEncoder.end();
|
||||
|
||||
return canvasTexture;
|
||||
|
|
@ -154,11 +162,10 @@ export class RenderPipeline {
|
|||
},
|
||||
],
|
||||
});
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
passEncoder.setPipeline(this.sampledPipeline);
|
||||
this.commonState.execute(passEncoder);
|
||||
passEncoder.setVertexBuffer(0, this.vertexBuffer);
|
||||
passEncoder.setBindGroup(1, bindGroup);
|
||||
passEncoder.draw(4, 1);
|
||||
passEncoder.draw(3, 1);
|
||||
passEncoder.end();
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +213,6 @@ export class RenderPipeline {
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
this.vertexBuffer.destroy();
|
||||
this.uniforms.destroy();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
export interface RenderSettings {
|
||||
clarity: number;
|
||||
renderTraceNormalizationFloor: number;
|
||||
renderBrushColorBase: number;
|
||||
renderBrushColorStrengthMultiplier: number;
|
||||
backgroundGrainStrength: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ struct Settings {
|
|||
backgroundColorPadding2: f32,
|
||||
backgroundColor: vec3<f32>,
|
||||
clarity: f32,
|
||||
traceNormalizationFloor: f32,
|
||||
brushColorBase: f32,
|
||||
brushColorStrengthMultiplier: f32,
|
||||
backgroundGrainStrength: f32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
|
|
@ -15,9 +19,29 @@ struct Settings {
|
|||
@group(1) @binding(3) var sourceMap: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||
fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let textureSize = vec2<i32>(textureDimensions(trailMap, 0));
|
||||
let pixel = clamp(vec2<i32>(position.xy), vec2<i32>(0, 0), textureSize - vec2<i32>(1, 1));
|
||||
let traces = textureLoad(trailMap, pixel, 0);
|
||||
let sources = textureLoad(sourceMap, pixel, 0);
|
||||
return renderColor(traces, sources, vec2<i32>(position.xy));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentSampled(
|
||||
@location(0) uv: vec2<f32>,
|
||||
@builtin(position) position: vec4<f32>
|
||||
) -> @location(0) vec4<f32> {
|
||||
let traces = textureSample(trailMap, Sampler, uv);
|
||||
let sources = textureSample(sourceMap, Sampler, uv);
|
||||
return renderColor(traces, sources, vec2<i32>(position.xy));
|
||||
}
|
||||
|
||||
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
|
||||
let background = getTexturedBackground(pixel);
|
||||
if max(max(max(traces.r, traces.g), traces.b), max(max(sources.r, sources.g), sources.b)) <= 0.0 {
|
||||
return vec4(background, 1);
|
||||
}
|
||||
|
||||
let traceStrengths = vec3(
|
||||
clarity(traces.r),
|
||||
|
|
@ -34,17 +58,19 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
|||
strengths.r * settings.colorA
|
||||
+ strengths.g * settings.colorB
|
||||
+ strengths.b * settings.colorC;
|
||||
let normalizedTraceColor = traceColor / max(1.0, strengths.r + strengths.g + strengths.b);
|
||||
let normalizedTraceColor =
|
||||
traceColor / max(settings.traceNormalizationFloor, strengths.r + strengths.g + strengths.b);
|
||||
let brushColor =
|
||||
sourceStrengths.r * settings.colorA
|
||||
+ sourceStrengths.g * settings.colorB
|
||||
+ sourceStrengths.b * settings.colorC;
|
||||
let brushStrength = clamp(max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b), 0, 1);
|
||||
let color = max(normalizedTraceColor, brushColor * (1.2 + brushStrength * 1.6));
|
||||
|
||||
let strength = clamp(max(max(max(strengths.r, strengths.g), strengths.b), brushStrength), 0, 1);
|
||||
let background = getTexturedBackground(uv);
|
||||
let brushStrength = max(max(sourceStrengths.r, sourceStrengths.g), sourceStrengths.b);
|
||||
let color = max(
|
||||
normalizedTraceColor,
|
||||
brushColor * (settings.brushColorBase + brushStrength * settings.brushColorStrengthMultiplier)
|
||||
);
|
||||
|
||||
let strength = max(max(strengths.r, strengths.g), strengths.b);
|
||||
return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1);
|
||||
}
|
||||
|
||||
|
|
@ -52,11 +78,14 @@ fn clarity(strength: f32) -> f32 {
|
|||
return pow(clamp(strength, 0, 1), settings.clarity);
|
||||
}
|
||||
|
||||
fn getTexturedBackground(uv: vec2<f32>) -> vec3<f32> {
|
||||
let noiseSize = vec2<f32>(textureDimensions(noise, 0));
|
||||
let pixel = floor(uv * state.size);
|
||||
let noiseCoord = vec2<i32>(fract(pixel / noiseSize) * noiseSize);
|
||||
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
|
||||
let noiseSize = vec2<i32>(textureDimensions(noise, 0));
|
||||
let noiseCoord = pixel % noiseSize;
|
||||
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;
|
||||
|
||||
return clamp(settings.backgroundColor + vec3(grain * 0.018), vec3(0), vec3(1));
|
||||
return clamp(
|
||||
settings.backgroundColor + vec3(grain * settings.backgroundGrainStrength),
|
||||
vec3(0),
|
||||
vec3(1)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import agentShader from './agents/agent.wgsl?raw';
|
|||
import { BrushPipeline } from './brush/brush-pipeline';
|
||||
import brushShader from './brush/brush.wgsl?raw';
|
||||
import { CommonState } from './common-state/common-state';
|
||||
import { CopyPipeline } from './copy/copy-pipeline';
|
||||
import copyShader from './copy/copy.wgsl?raw';
|
||||
import diffusionShader from './diffusion/diffuse.wgsl?raw';
|
||||
import { DiffusionPipeline } from './diffusion/diffusion-pipeline';
|
||||
import { EraserAgentPipeline } from './eraser/eraser-agent-pipeline';
|
||||
|
|
@ -103,7 +101,8 @@ describe('WGSL uniform layout contracts', () => {
|
|||
fieldNames: [
|
||||
'moveRate',
|
||||
'turnRate',
|
||||
'sensorAngle',
|
||||
'sensorAngleSin',
|
||||
'sensorAngleCos',
|
||||
'sensorOffset',
|
||||
'turnWhenLost',
|
||||
'individualTrailWeight',
|
||||
|
|
@ -118,6 +117,21 @@ describe('WGSL uniform layout contracts', () => {
|
|||
'color3ToColor1',
|
||||
'color3ToColor2',
|
||||
'color3ToColor3',
|
||||
'sourceAttractionWeight',
|
||||
'sourceSlowMoveRate',
|
||||
'sourceTrailWeightMultiplier',
|
||||
'forwardRotationScale',
|
||||
'introNearDistanceInner',
|
||||
'introNearDistanceMin',
|
||||
'introNearSensorOffsetMultiplier',
|
||||
'introTargetAngleBlend',
|
||||
'introProgressCutoff',
|
||||
'introTurnRateMultiplier',
|
||||
'introRandomTurnMultiplier',
|
||||
'introFarMoveMultiplier',
|
||||
'introNearMoveMultiplier',
|
||||
'introStepStopDistance',
|
||||
'randomTimeScale',
|
||||
],
|
||||
});
|
||||
expectStructUniformLayout({
|
||||
|
|
@ -127,9 +141,17 @@ describe('WGSL uniform layout contracts', () => {
|
|||
fieldNames: [
|
||||
'brushSize',
|
||||
'brushSizeVariation',
|
||||
'padding0',
|
||||
'padding1',
|
||||
'brushFeatherRatio',
|
||||
'brushMinimumFeather',
|
||||
'brushValue',
|
||||
'brushCoarseNoiseScale',
|
||||
'brushGrainNoiseScale',
|
||||
'brushGrainNoiseOffsetX',
|
||||
'brushGrainNoiseOffsetY',
|
||||
'brushDiscardThreshold',
|
||||
'brushGrainMinStrength',
|
||||
'brushGrainMaxStrength',
|
||||
'brushGeometryRadius',
|
||||
],
|
||||
});
|
||||
expectStructUniformLayout({
|
||||
|
|
@ -141,6 +163,10 @@ describe('WGSL uniform layout contracts', () => {
|
|||
'decayRateTrails',
|
||||
'inverseDiffusionRateBrush',
|
||||
'decayRateBrush',
|
||||
'diffusionNeighborDivisor',
|
||||
'brushDecayAlphaOffset',
|
||||
'padding0',
|
||||
'padding1',
|
||||
],
|
||||
});
|
||||
expectStructUniformLayout({
|
||||
|
|
@ -156,6 +182,10 @@ describe('WGSL uniform layout contracts', () => {
|
|||
'backgroundColorPadding2',
|
||||
'backgroundColor',
|
||||
'clarity',
|
||||
'traceNormalizationFloor',
|
||||
'brushColorBase',
|
||||
'brushColorStrengthMultiplier',
|
||||
'backgroundGrainStrength',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -165,25 +195,25 @@ describe('WGSL uniform layout contracts', () => {
|
|||
pipeline: EraserAgentPipeline,
|
||||
source: eraserAgentShader,
|
||||
structName: 'Settings',
|
||||
fieldNames: ['agentCount', 'padding0', 'padding1', 'padding2'],
|
||||
fieldNames: ['agentCount', 'eraserMaskAlphaThreshold', 'padding1', 'padding2'],
|
||||
});
|
||||
expectStructUniformLayout({
|
||||
pipeline: EraserTexturePipeline,
|
||||
source: eraserTextureShader,
|
||||
structName: 'Settings',
|
||||
fieldNames: ['eraserRadiusSquared', 'padding0', 'padding1', 'padding2'],
|
||||
fieldNames: [
|
||||
'eraserRadiusSquared',
|
||||
'lineDistanceEpsilon',
|
||||
'clearRed',
|
||||
'clearGreen',
|
||||
'clearBlue',
|
||||
'clearAlpha',
|
||||
'padding0',
|
||||
'padding1',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps copy uniforms aligned with WGSL', () => {
|
||||
const match = /var<uniform>\s+sourceScaler:\s*(?<type>[^;]+);/.exec(copyShader);
|
||||
|
||||
expect(match?.groups?.type).toBe('vec2<f32>');
|
||||
expect(wgslFloatCountsByType[match?.groups?.type ?? '']).toBe(
|
||||
getUniformCount(CopyPipeline)
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps agent-generation uniforms large enough for every generation shader', () => {
|
||||
const generationUniformCounts = [
|
||||
countUniformScalars(resizeShader, 'ResizeSettings'),
|
||||
|
|
@ -196,7 +226,7 @@ describe('WGSL uniform layout contracts', () => {
|
|||
});
|
||||
|
||||
it('guards invalid high agent color indexes instead of treating them as color 3', () => {
|
||||
expect(agentShader).toContain('agent.colorIndex < 0.0 || agent.colorIndex >= 2.5');
|
||||
expect(agentShader).toContain('colorIndex < 0.0 || colorIndex >= 2.5');
|
||||
expect(agentShader).toContain('if colorIndex < 2.5');
|
||||
expect(agentShader).toContain('return vec3<f32>(0.0, 0.0, 0.0);');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue