Clean up
This commit is contained in:
parent
ea0304356f
commit
7c70f15e49
65 changed files with 1122 additions and 1906 deletions
|
|
@ -15,10 +15,7 @@ struct Counters {
|
|||
|
||||
var<workgroup> workgroupAliveCount: atomic<u32>;
|
||||
var<workgroup> workgroupCompactedOffset: u32;
|
||||
|
||||
fn dead_agent() -> Agent {
|
||||
return Agent(vec2<f32>(0.0, 0.0), 0.0, -1.0, vec2<f32>(-1.0, -1.0), 0.0, 0.0);
|
||||
}
|
||||
var<workgroup> clearAliveAgentCount: u32;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
|
|
@ -35,12 +32,12 @@ fn main(
|
|||
workgroupBarrier();
|
||||
|
||||
var localCompactedIndex = 0u;
|
||||
var agent = dead_agent();
|
||||
var agent: Agent;
|
||||
var isAlive = false;
|
||||
if id < settings.agentCount {
|
||||
agent = agents[id];
|
||||
isAlive = agent.colorIndex >= 0.0;
|
||||
isAlive = agents[id].colorIndex >= 0.0;
|
||||
if isAlive {
|
||||
agent = agents[id];
|
||||
localCompactedIndex = atomicAdd(&workgroupAliveCount, 1u);
|
||||
}
|
||||
}
|
||||
|
|
@ -66,16 +63,22 @@ fn main(
|
|||
@compute @workgroup_size(64)
|
||||
fn clearCompactedTail(
|
||||
@builtin(global_invocation_id) global_id: vec3<u32>,
|
||||
@builtin(local_invocation_id) local_id: vec3<u32>,
|
||||
@builtin(num_workgroups) num_workgroups: vec3<u32>
|
||||
) {
|
||||
let id = get_id(global_id, num_workgroups);
|
||||
|
||||
if local_id.x == 0u {
|
||||
clearAliveAgentCount = atomicLoad(&counters.aliveAgentCount);
|
||||
}
|
||||
|
||||
workgroupBarrier();
|
||||
|
||||
if id >= settings.agentCount {
|
||||
return;
|
||||
}
|
||||
|
||||
let aliveAgentCount = atomicLoad(&counters.aliveAgentCount);
|
||||
if id >= aliveAgentCount {
|
||||
compactedAgents[id] = dead_agent();
|
||||
if id >= clearAliveAgentCount {
|
||||
compactedAgents[id].colorIndex = -1.0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ export class AgentGenerationPipeline {
|
|||
private readonly agentCountUniformValues = new Uint32Array(
|
||||
AgentGenerationPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly resizeUniformBuffer = new ArrayBuffer(
|
||||
AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT
|
||||
);
|
||||
private readonly resizeUniformFloatValues = new Float32Array(
|
||||
this.resizeUniformBuffer
|
||||
);
|
||||
private readonly resizeUniformUintValues = new Uint32Array(
|
||||
this.resizeUniformBuffer
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
|
|
@ -233,11 +242,11 @@ export class AgentGenerationPipeline {
|
|||
return;
|
||||
}
|
||||
|
||||
this.device.queue.writeBuffer(
|
||||
this.uniforms,
|
||||
0,
|
||||
new Float32Array([scale[0], scale[1], agentCount, 0])
|
||||
);
|
||||
this.resizeUniformFloatValues[0] = scale[0];
|
||||
this.resizeUniformFloatValues[1] = scale[1];
|
||||
this.resizeUniformUintValues[2] = Math.max(0, Math.floor(agentCount));
|
||||
this.resizeUniformUintValues[3] = 0;
|
||||
this.device.queue.writeBuffer(this.uniforms, 0, this.resizeUniformBuffer);
|
||||
|
||||
const commandEncoder = this.device.createCommandEncoder();
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
struct ResizeSettings {
|
||||
scale: vec2<f32>,
|
||||
agentCount: f32,
|
||||
padding: f32,
|
||||
agentCount: u32,
|
||||
padding: u32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> resizeSettings: ResizeSettings;
|
||||
|
|
@ -13,10 +13,11 @@ fn main(
|
|||
) {
|
||||
let id = get_id(global_id, num_workgroups);
|
||||
|
||||
if id >= u32(resizeSettings.agentCount) {
|
||||
if id >= resizeSettings.agentCount {
|
||||
return;
|
||||
}
|
||||
|
||||
agents[id].position = agents[id].position * resizeSettings.scale;
|
||||
agents[id].targetPosition = agents[id].targetPosition * resizeSettings.scale;
|
||||
let scale = resizeSettings.scale;
|
||||
agents[id].position = agents[id].position * scale;
|
||||
agents[id].targetPosition = agents[id].targetPosition * scale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,21 +10,19 @@ import { AgentSettings } from './agent-settings';
|
|||
import shader from './agent.wgsl?raw';
|
||||
|
||||
export class AgentPipeline {
|
||||
private static readonly UNIFORM_COUNT = 33;
|
||||
private static readonly UNIFORM_COUNT = 30;
|
||||
|
||||
private readonly bindGroupLayout: GPUBindGroupLayout;
|
||||
private readonly pipeline: GPUComputePipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(AgentPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformUintValues = new Uint32Array(this.uniformValues.buffer);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
AgentPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly bindGroupsByAgentsBuffer = new WeakMap<
|
||||
GPUBuffer,
|
||||
WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
>
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
>();
|
||||
|
||||
private agentCount = 0;
|
||||
|
|
@ -69,9 +67,6 @@ export class AgentPipeline {
|
|||
color3ToColor1,
|
||||
color3ToColor2,
|
||||
color3ToColor3,
|
||||
sourceAttractionWeight,
|
||||
sourceSlowMoveRate,
|
||||
sourceTrailWeightMultiplier,
|
||||
forwardRotationScale,
|
||||
introNearDistanceInner,
|
||||
introNearDistanceMin,
|
||||
|
|
@ -100,7 +95,7 @@ export class AgentPipeline {
|
|||
this.uniformValues[4] = sensorOffsetDistance;
|
||||
this.uniformValues[5] = turnWhenLost;
|
||||
this.uniformValues[6] = individualTrailWeight;
|
||||
this.uniformValues[7] = agentCount;
|
||||
this.uniformUintValues[7] = Math.max(0, Math.floor(agentCount));
|
||||
this.uniformValues[8] = introProgress ?? 1;
|
||||
this.uniformValues[9] = color1ToColor1;
|
||||
this.uniformValues[10] = color1ToColor2;
|
||||
|
|
@ -111,21 +106,18 @@ export class AgentPipeline {
|
|||
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;
|
||||
this.uniformValues[18] = forwardRotationScale;
|
||||
this.uniformValues[19] = introNearDistanceInner;
|
||||
this.uniformValues[20] = introNearDistanceMin;
|
||||
this.uniformValues[21] = introNearSensorOffsetMultiplier;
|
||||
this.uniformValues[22] = introTargetAngleBlend;
|
||||
this.uniformValues[23] = introProgressCutoff;
|
||||
this.uniformValues[24] = introTurnRateMultiplier;
|
||||
this.uniformValues[25] = introRandomTurnMultiplier;
|
||||
this.uniformValues[26] = introFarMoveMultiplier;
|
||||
this.uniformValues[27] = introNearMoveMultiplier;
|
||||
this.uniformValues[28] = introStepStopDistance;
|
||||
this.uniformValues[29] = randomTimeScale;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
|
|
@ -137,14 +129,13 @@ export class AgentPipeline {
|
|||
public execute(
|
||||
commandEncoder: GPUCommandEncoder,
|
||||
trailMapIn: GPUTextureView,
|
||||
trailMapOut: GPUTextureView,
|
||||
sourceMap: GPUTextureView
|
||||
trailMapOut: GPUTextureView
|
||||
) {
|
||||
if (this.agentCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bindGroup = this.getBindGroup(trailMapIn, trailMapOut, sourceMap);
|
||||
const bindGroup = this.getBindGroup(trailMapIn, trailMapOut);
|
||||
|
||||
const passEncoder = commandEncoder.beginComputePass();
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
|
|
@ -156,32 +147,22 @@ export class AgentPipeline {
|
|||
|
||||
private getBindGroup(
|
||||
trailMapIn: GPUTextureView,
|
||||
trailMapOut: GPUTextureView,
|
||||
sourceMap: GPUTextureView
|
||||
trailMapOut: GPUTextureView
|
||||
): GPUBindGroup {
|
||||
const agentsBuffer = this.getAgentsBuffer();
|
||||
let textureCache = this.bindGroupsByAgentsBuffer.get(agentsBuffer);
|
||||
if (!textureCache) {
|
||||
textureCache = new WeakMap<
|
||||
GPUTextureView,
|
||||
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
|
||||
>();
|
||||
textureCache = new WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>();
|
||||
this.bindGroupsByAgentsBuffer.set(agentsBuffer, textureCache);
|
||||
}
|
||||
|
||||
let outputCache = textureCache.get(trailMapIn);
|
||||
if (!outputCache) {
|
||||
outputCache = new WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>();
|
||||
outputCache = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
textureCache.set(trailMapIn, outputCache);
|
||||
}
|
||||
|
||||
let sourceCache = outputCache.get(trailMapOut);
|
||||
if (!sourceCache) {
|
||||
sourceCache = new WeakMap<GPUTextureView, GPUBindGroup>();
|
||||
outputCache.set(trailMapOut, sourceCache);
|
||||
}
|
||||
|
||||
const cached = sourceCache.get(sourceMap);
|
||||
const cached = outputCache.get(trailMapOut);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
|
@ -209,14 +190,10 @@ export class AgentPipeline {
|
|||
binding: 3,
|
||||
resource: trailMapOut,
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
resource: sourceMap,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
sourceCache.set(sourceMap, bindGroup);
|
||||
outputCache.set(trailMapOut, bindGroup);
|
||||
return bindGroup;
|
||||
}
|
||||
|
||||
|
|
@ -255,13 +232,6 @@ export class AgentPipeline {
|
|||
format: 'rgba16float',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: GPUShaderStage.COMPUTE,
|
||||
texture: {
|
||||
sampleType: 'float',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ export interface AgentSettings {
|
|||
sensorOffsetDistance: number;
|
||||
turnWhenLost: number;
|
||||
individualTrailWeight: number;
|
||||
sourceAttractionWeight: number;
|
||||
sourceSlowMoveRate: number;
|
||||
sourceTrailWeightMultiplier: number;
|
||||
forwardRotationScale: number;
|
||||
introNearDistanceMin: number;
|
||||
introNearSensorOffsetMultiplier: number;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ struct Settings {
|
|||
sensorOffset: f32,
|
||||
turnWhenLost: f32,
|
||||
individualTrailWeight: f32,
|
||||
agentCount: f32,
|
||||
agentCount: u32,
|
||||
introProgress: f32,
|
||||
color1ToColor1: f32,
|
||||
color1ToColor2: f32,
|
||||
|
|
@ -17,9 +17,6 @@ struct Settings {
|
|||
color3ToColor1: f32,
|
||||
color3ToColor2: f32,
|
||||
color3ToColor3: f32,
|
||||
sourceAttractionWeight: f32,
|
||||
sourceSlowMoveRate: f32,
|
||||
sourceTrailWeightMultiplier: f32,
|
||||
forwardRotationScale: f32,
|
||||
introNearDistanceInner: f32,
|
||||
introNearDistanceMin: f32,
|
||||
|
|
@ -37,7 +34,6 @@ struct Settings {
|
|||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
@group(1) @binding(2) var trailMapIn: texture_2d<f32>;
|
||||
@group(1) @binding(3) var trailMapOut: texture_storage_2d<rgba16float, write>;
|
||||
@group(1) @binding(4) var sourceMap: texture_2d<f32>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
|
|
@ -46,7 +42,7 @@ fn main(
|
|||
) {
|
||||
let id = get_id(global_id, num_workgroups);
|
||||
|
||||
if id >= u32(settings.agentCount) {
|
||||
if id >= settings.agentCount {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -67,63 +63,15 @@ fn main(
|
|||
}
|
||||
}
|
||||
|
||||
let randomSeed = random_seed(id, state.time);
|
||||
let randomTurn = random_float(randomSeed);
|
||||
let direction = vec2(cos(angle), sin(angle));
|
||||
|
||||
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);
|
||||
let trailRight = textureLoad(trailMapIn, rightSensor, 0);
|
||||
let sourceForwardSample = textureLoad(sourceMap, forwardSensor, 0);
|
||||
let sourceLeftSample = textureLoad(sourceMap, leftSensor, 0);
|
||||
let sourceRightSample = textureLoad(sourceMap, rightSensor, 0);
|
||||
|
||||
let channelMask = get_channel_mask(colorIndex);
|
||||
let reactionMask = get_reaction_mask(colorIndex);
|
||||
|
||||
let trailForwardWeight = dot(trailForward.rgb, reactionMask);
|
||||
let trailLeftWeight = dot(trailLeft.rgb, reactionMask);
|
||||
let trailRightWeight = dot(trailRight.rgb, reactionMask);
|
||||
|
||||
let sourceForwardWeight = dot(sourceForwardSample.rgb, reactionMask);
|
||||
let sourceLeftWeight = dot(sourceLeftSample.rgb, reactionMask);
|
||||
let sourceRightWeight = dot(sourceRightSample.rgb, reactionMask);
|
||||
|
||||
let weightForward =
|
||||
trailForwardWeight + sourceForwardWeight * settings.sourceAttractionWeight;
|
||||
let weightLeft = trailLeftWeight + sourceLeftWeight * settings.sourceAttractionWeight;
|
||||
let weightRight =
|
||||
trailRightWeight + sourceRightWeight * settings.sourceAttractionWeight;
|
||||
|
||||
var rotation = (randomTurn - 0.5) * settings.turnWhenLost;
|
||||
if weightForward >= weightLeft && weightForward >= weightRight {
|
||||
rotation = rotation * settings.forwardRotationScale;
|
||||
} else {
|
||||
rotation += sign(weightLeft - weightRight) * settings.turnRate;
|
||||
}
|
||||
|
||||
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, settings.sourceSlowMoveRate, sourceAtAgentStrength);
|
||||
var introTargetOffset = vec2<f32>(0.0, 0.0);
|
||||
var introTargetDistance = 0.0;
|
||||
let randomSeed = random_seed(id, state.time);
|
||||
var rotation = 0.0;
|
||||
var step = vec2<f32>(0.0, 0.0);
|
||||
|
||||
if hasIntroTarget {
|
||||
introTargetOffset = targetPosition - position;
|
||||
introTargetDistance = length(introTargetOffset);
|
||||
let introTargetOffset = targetPosition - position;
|
||||
let introTargetDistance = length(introTargetOffset);
|
||||
let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x);
|
||||
let nearTitle = 1.0 - smoothstep(
|
||||
settings.introNearDistanceInner,
|
||||
|
|
@ -148,19 +96,46 @@ fn main(
|
|||
+ (random_float(randomSeed + 1013904223u) - 0.5) *
|
||||
settings.turnWhenLost *
|
||||
settings.introRandomTurnMultiplier;
|
||||
moveRate = min(
|
||||
let moveRate = min(
|
||||
settings.moveRate *
|
||||
mix(settings.introFarMoveMultiplier, settings.introNearMoveMultiplier, nearTitle),
|
||||
introTargetDistance
|
||||
);
|
||||
}
|
||||
|
||||
var step = direction * moveRate;
|
||||
if hasIntroTarget {
|
||||
step = vec2<f32>(0.0, 0.0);
|
||||
if introTargetDistance > settings.introStepStopDistance {
|
||||
step = introTargetOffset / introTargetDistance * moveRate;
|
||||
}
|
||||
} else {
|
||||
let randomTurn = random_float(randomSeed);
|
||||
let direction = vec2(cos(angle), sin(angle));
|
||||
|
||||
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);
|
||||
let trailRight = textureLoad(trailMapIn, rightSensor, 0);
|
||||
|
||||
let weightForward = dot(trailForward.rgb, reactionMask);
|
||||
let weightLeft = dot(trailLeft.rgb, reactionMask);
|
||||
let weightRight = dot(trailRight.rgb, reactionMask);
|
||||
|
||||
rotation = (randomTurn - 0.5) * settings.turnWhenLost;
|
||||
if weightForward >= weightLeft && weightForward >= weightRight {
|
||||
rotation = rotation * settings.forwardRotationScale;
|
||||
} else {
|
||||
rotation += sign(weightLeft - weightRight) * settings.turnRate;
|
||||
}
|
||||
|
||||
step = direction * settings.moveRate;
|
||||
}
|
||||
|
||||
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
|
||||
|
|
@ -169,14 +144,9 @@ fn main(
|
|||
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 * settings.sourceTrailWeightMultiplier);
|
||||
var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0);
|
||||
trailBelow = vec4<f32>(
|
||||
trailBelow.rgb + channelMask * trailWeight,
|
||||
trailBelow.rgb + channelMask * settings.individualTrailWeight,
|
||||
max(trailBelow.a, 0.0)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ export const setBrushUniformValues = (
|
|||
brushSize,
|
||||
brushSizeVariation,
|
||||
brushAlpha,
|
||||
brushFeatherRatio,
|
||||
brushMinimumFeather,
|
||||
brushDiscardThreshold,
|
||||
brushCoarseNoiseScale,
|
||||
brushGrainNoiseScale,
|
||||
|
|
@ -47,30 +45,24 @@ export const setBrushUniformValues = (
|
|||
const safePixelRatio = getSafePixelRatio(pixelRatio);
|
||||
const brushRadius = (brushSize * safePixelRatio) / 2;
|
||||
const brushRadiusVariation = Math.floor(brushRadius * brushSizeVariation);
|
||||
const brushMinimumFeatherPixels = brushMinimumFeather * safePixelRatio;
|
||||
const brushFeather = Math.max(
|
||||
brushMinimumFeatherPixels,
|
||||
brushRadius * brushFeatherRatio
|
||||
);
|
||||
const brushGeometryRadius =
|
||||
brushRadius + Math.max(0, brushRadiusVariation) + brushFeather;
|
||||
const brushGeometryRadius = brushRadius + Math.max(0, brushRadiusVariation);
|
||||
|
||||
target[0] = brushRadius;
|
||||
target[1] = brushRadiusVariation;
|
||||
target[2] = brushFeatherRatio;
|
||||
target[3] = brushMinimumFeatherPixels;
|
||||
target[1] = brushRadiusVariation * 2;
|
||||
target[2] = brushGeometryRadius * brushGeometryRadius;
|
||||
target[3] = brushGeometryRadius;
|
||||
target[4] = selectedColorIndex === 0 ? 1 : 0;
|
||||
target[5] = selectedColorIndex === 1 ? 1 : 0;
|
||||
target[6] = selectedColorIndex === 2 ? 1 : 0;
|
||||
target[7] = brushAlpha;
|
||||
target[8] = brushCoarseNoiseScale * safePixelRatio;
|
||||
target[9] = brushGrainNoiseScale * safePixelRatio;
|
||||
target[8] = 1 / Math.max(Number.EPSILON, brushCoarseNoiseScale * safePixelRatio);
|
||||
target[9] = 1 / Math.max(Number.EPSILON, brushGrainNoiseScale * safePixelRatio);
|
||||
target[10] = brushGrainNoiseOffsetX;
|
||||
target[11] = brushGrainNoiseOffsetY;
|
||||
target[12] = brushDiscardThreshold;
|
||||
target[13] = brushGrainMinStrength;
|
||||
target[14] = brushGrainMaxStrength;
|
||||
target[15] = brushGeometryRadius;
|
||||
target[15] = 0;
|
||||
};
|
||||
|
||||
export class BrushPipeline {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ export interface BrushSettings {
|
|||
brushSize: number;
|
||||
brushSizeVariation: number;
|
||||
brushAlpha: number;
|
||||
brushFeatherRatio: number;
|
||||
brushMinimumFeather: number;
|
||||
brushDiscardThreshold: number;
|
||||
brushCoarseNoiseScale: number;
|
||||
brushGrainNoiseScale: number;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
struct Settings {
|
||||
brushSize: f32,
|
||||
brushSizeVariation: f32,
|
||||
brushFeatherRatio: f32,
|
||||
brushMinimumFeather: f32,
|
||||
brushGeometryRadiusSquared: f32,
|
||||
brushGeometryRadius: f32,
|
||||
brushValue: vec4<f32>,
|
||||
brushCoarseNoiseScale: f32,
|
||||
brushGrainNoiseScale: f32,
|
||||
|
|
@ -11,7 +11,6 @@ struct Settings {
|
|||
brushDiscardThreshold: f32,
|
||||
brushGrainMinStrength: f32,
|
||||
brushGrainMaxStrength: f32,
|
||||
brushGeometryRadius: f32,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
|
|
@ -20,7 +19,8 @@ struct VertexOutput {
|
|||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32,
|
||||
}
|
||||
|
||||
struct BrushTargets {
|
||||
|
|
@ -34,24 +34,36 @@ fn vertex(
|
|||
@location(0) start: vec2<f32>,
|
||||
@location(1) end: vec2<f32>
|
||||
) -> VertexOutput {
|
||||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
var inverseLengthSquared = 0.0;
|
||||
if denominator > 0.0001 {
|
||||
inverseLengthSquared = 1.0 / denominator;
|
||||
}
|
||||
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);
|
||||
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, direction, inverseLengthSquared);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMrt(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
||||
) -> BrushTargets {
|
||||
let distanceSquared = distanceSquaredFromLine(screenPosition, start, end);
|
||||
if distanceSquared > settings.brushGeometryRadius * settings.brushGeometryRadius {
|
||||
let distanceSquared = distanceSquaredFromLine(
|
||||
screenPosition,
|
||||
start,
|
||||
direction,
|
||||
inverseLengthSquared
|
||||
);
|
||||
if distanceSquared > settings.brushGeometryRadiusSquared {
|
||||
discard;
|
||||
}
|
||||
|
||||
let strength = brushStrength(screenPosition, start, end, distanceSquared);
|
||||
let strength = brushStrength(screenPosition, distanceSquared);
|
||||
|
||||
if(strength < settings.brushDiscardThreshold) {
|
||||
discard;
|
||||
|
|
@ -63,27 +75,28 @@ fn fragmentMrt(
|
|||
|
||||
fn brushStrength(
|
||||
screenPosition: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
end: vec2<f32>,
|
||||
distanceSquared: f32
|
||||
) -> f32 {
|
||||
let distance = sqrt(distanceSquared);
|
||||
let coarseNoise = textureSample(
|
||||
let coarseNoise = textureSampleLevel(
|
||||
noise,
|
||||
noiseSampler,
|
||||
fract(screenPosition / settings.brushCoarseNoiseScale)
|
||||
screenPosition * settings.brushCoarseNoiseScale,
|
||||
0.0
|
||||
).r;
|
||||
let grainNoise = textureSample(
|
||||
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation;
|
||||
let edge = 1.0 - step(radius, distance);
|
||||
if edge * max(settings.brushGrainMinStrength, settings.brushGrainMaxStrength) < settings.brushDiscardThreshold {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let grainNoise = textureSampleLevel(
|
||||
noise,
|
||||
noiseSampler,
|
||||
fract(
|
||||
screenPosition / settings.brushGrainNoiseScale +
|
||||
vec2(settings.brushGrainNoiseOffsetX, settings.brushGrainNoiseOffsetY)
|
||||
)
|
||||
screenPosition * settings.brushGrainNoiseScale +
|
||||
vec2(settings.brushGrainNoiseOffsetX, settings.brushGrainNoiseOffsetY),
|
||||
0.0
|
||||
).r;
|
||||
let radius = settings.brushSize + (coarseNoise - 0.5) * settings.brushSizeVariation * 2.0;
|
||||
let feather = max(settings.brushMinimumFeather, settings.brushSize * settings.brushFeatherRatio);
|
||||
let edge = 1.0 - smoothstep(radius - feather, radius + feather, distance);
|
||||
return edge * mix(settings.brushGrainMinStrength, settings.brushGrainMaxStrength, grainNoise);
|
||||
}
|
||||
|
||||
|
|
@ -91,16 +104,19 @@ fn brushOutput(strength: f32) -> vec4<f32> {
|
|||
return vec4(settings.brushValue.rgb * strength, settings.brushValue.a * strength);
|
||||
}
|
||||
|
||||
fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
|
||||
fn distanceSquaredFromLine(
|
||||
position: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
direction: vec2<f32>,
|
||||
inverseLengthSquared: f32
|
||||
) -> f32 {
|
||||
let pa = position - start;
|
||||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
|
||||
if denominator <= 0.0001 {
|
||||
if inverseLengthSquared <= 0.0 {
|
||||
return dot(pa, pa);
|
||||
}
|
||||
|
||||
let q = clamp(dot(pa, direction) / denominator, 0, 1);
|
||||
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0, 1);
|
||||
let nearestOffset = pa - direction * q;
|
||||
return dot(nearestOffset, nearestOffset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ export class CommonState {
|
|||
{
|
||||
binding: 1,
|
||||
resource: this.device.createSampler({
|
||||
addressModeU: 'repeat',
|
||||
addressModeV: 'repeat',
|
||||
magFilter: 'linear',
|
||||
minFilter: 'linear',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ struct Settings {
|
|||
decayRateTrails: f32,
|
||||
inverseDiffusionRateBrush: f32,
|
||||
decayRateBrush: f32,
|
||||
diffusionNeighborDivisor: f32,
|
||||
brushDecayAlphaOffset: f32,
|
||||
padding0: f32,
|
||||
diffusionNeighborScale: f32,
|
||||
brushDecayAlphaMultiplier: f32,
|
||||
brushDecayAlphaSubtract: f32,
|
||||
padding1: f32,
|
||||
};
|
||||
|
||||
|
|
@ -60,8 +60,26 @@ fn main(
|
|||
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);
|
||||
let r2 = random * random;
|
||||
let r4 = r2 * r2;
|
||||
let r8 = r4 * r4;
|
||||
let r16 = r8 * r8;
|
||||
let trailWeight = diffusion_weight(
|
||||
random,
|
||||
r2,
|
||||
r4,
|
||||
r8,
|
||||
r16,
|
||||
settings.inverseDiffusionRateTrails
|
||||
);
|
||||
let brushWeight = diffusion_weight(
|
||||
random,
|
||||
r2,
|
||||
r4,
|
||||
r8,
|
||||
r16,
|
||||
settings.inverseDiffusionRateBrush
|
||||
);
|
||||
|
||||
current += (
|
||||
propagate(centerTileIndex, -1, -1, current, trailWeight, brushWeight)
|
||||
|
|
@ -73,11 +91,11 @@ fn main(
|
|||
+ 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);
|
||||
) * settings.diffusionNeighborScale;
|
||||
|
||||
let decayed = clamp(vec4(
|
||||
current.rgb * settings.decayRateTrails,
|
||||
max(0, current.a + (current.a - settings.brushDecayAlphaOffset) * settings.decayRateBrush)
|
||||
max(0, current.a * settings.brushDecayAlphaMultiplier - settings.brushDecayAlphaSubtract)
|
||||
), vec4(0), vec4(1));
|
||||
|
||||
textureStore(trailMapOut, pixel, decayed);
|
||||
|
|
@ -110,9 +128,14 @@ fn random_from_pixel(pixel: vec2<i32>) -> f32 {
|
|||
return f32(hash) * 2.3283064365386963e-10;
|
||||
}
|
||||
|
||||
fn diffusion_weight(random: f32, inverseRate: f32) -> f32 {
|
||||
let r = clamp(random, 0.0, 1.0);
|
||||
|
||||
fn diffusion_weight(
|
||||
r: f32,
|
||||
r2: f32,
|
||||
r4: f32,
|
||||
r8: f32,
|
||||
r16: f32,
|
||||
inverseRate: f32
|
||||
) -> f32 {
|
||||
if inverseRate < 1.0 {
|
||||
let rootApproximation = r / max(0.5 + r * 0.5, 0.0001);
|
||||
return mix(
|
||||
|
|
@ -122,22 +145,18 @@ 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);
|
||||
}
|
||||
|
||||
let r16 = r8 * r8;
|
||||
return mix(r8, r16, clamp((inverseRate - 8.0) * 0.125, 0.0, 1.0))
|
||||
* min(1.0, 16.0 / inverseRate);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,12 +41,18 @@ export const setDiffusionUniformValues = (
|
|||
}: DiffusionUniformSettings
|
||||
): void => {
|
||||
const decayDivisor = Math.max(Number.EPSILON, diffusionDecayRateDivisor);
|
||||
const brushDecayRate = decayRateBrush / decayDivisor;
|
||||
const neighborDivisor = Number.isFinite(diffusionNeighborDivisor)
|
||||
? Math.max(1, diffusionNeighborDivisor)
|
||||
: 1;
|
||||
target[0] = getSafeInverseDiffusionRate(diffusionRateTrails);
|
||||
target[1] = decayRateTrails / decayDivisor;
|
||||
target[2] = getSafeInverseDiffusionRate(diffusionRateBrush);
|
||||
target[3] = decayRateBrush / decayDivisor;
|
||||
target[4] = diffusionNeighborDivisor;
|
||||
target[5] = brushDecayAlphaOffset;
|
||||
target[3] = brushDecayRate;
|
||||
target[4] = 1 / neighborDivisor;
|
||||
target[5] = 1 + brushDecayRate;
|
||||
target[6] = brushDecayAlphaOffset * brushDecayRate;
|
||||
target[7] = 0;
|
||||
};
|
||||
|
||||
export class DiffusionPipeline {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class EraserAgentPipeline {
|
|||
private readonly pipeline: GPUComputePipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(EraserAgentPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformUintValues = new Uint32Array(this.uniformValues.buffer);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
EraserAgentPipeline.UNIFORM_COUNT
|
||||
);
|
||||
|
|
@ -93,7 +94,7 @@ export class EraserAgentPipeline {
|
|||
this.activeSegmentCount = this.pendingSegmentCount;
|
||||
this.pendingSegmentCount = 0;
|
||||
|
||||
this.uniformValues[0] = agentCount;
|
||||
this.uniformUintValues[0] = Math.max(0, Math.floor(agentCount));
|
||||
this.uniformValues[1] = eraserMaskAlphaThreshold;
|
||||
this.uniformValues[2] = 0;
|
||||
this.uniformValues[3] = 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
struct Settings {
|
||||
agentCount: f32,
|
||||
agentCount: u32,
|
||||
eraserMaskAlphaThreshold: f32,
|
||||
padding1: f32,
|
||||
padding2: f32,
|
||||
|
|
@ -15,7 +15,7 @@ fn main(
|
|||
) {
|
||||
let id = get_id(global_id, num_workgroups);
|
||||
|
||||
if id >= u32(settings.agentCount) {
|
||||
if id >= settings.agentCount {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ fn main(
|
|||
);
|
||||
let maskSample = textureLoad(eraserMask, maskPosition, 0);
|
||||
|
||||
if maskSample.a < settings.eraserMaskAlphaThreshold {
|
||||
if maskSample.r < settings.eraserMaskAlphaThreshold {
|
||||
agents[id].colorIndex = -1.0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,12 @@ export class EraserTexturePipeline {
|
|||
});
|
||||
|
||||
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
|
||||
this.combinedPipeline = this.createPipeline(shaderModule, 'fragmentCombined', 4);
|
||||
this.combinedPipeline = this.createPipeline(shaderModule, 'fragmentCombined', [
|
||||
'r8unorm',
|
||||
'rgba16float',
|
||||
'rgba16float',
|
||||
'rgba16float',
|
||||
]);
|
||||
|
||||
this.uniforms = this.device.createBuffer({
|
||||
size: EraserTexturePipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
|
||||
|
|
@ -119,7 +124,7 @@ export class EraserTexturePipeline {
|
|||
this.uniformValues[3] = eraserClearGreen;
|
||||
this.uniformValues[4] = eraserClearBlue;
|
||||
this.uniformValues[5] = eraserClearAlpha;
|
||||
this.uniformValues[6] = 0;
|
||||
this.uniformValues[6] = eraserRadius;
|
||||
this.uniformValues[7] = 0;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
|
|
@ -225,7 +230,7 @@ export class EraserTexturePipeline {
|
|||
private createPipeline(
|
||||
shaderModule: GPUShaderModule,
|
||||
fragmentEntryPoint: string,
|
||||
colorTargetCount: number
|
||||
targetFormats: Array<GPUTextureFormat>
|
||||
): GPURenderPipeline {
|
||||
return this.device.createRenderPipeline({
|
||||
layout: this.device.createPipelineLayout({
|
||||
|
|
@ -258,9 +263,7 @@ export class EraserTexturePipeline {
|
|||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: fragmentEntryPoint,
|
||||
targets: Array.from({ length: colorTargetCount }, () => ({
|
||||
format: 'rgba16float' as const,
|
||||
})),
|
||||
targets: targetFormats.map((format) => ({ format })),
|
||||
},
|
||||
primitive: {
|
||||
topology: 'triangle-list',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ struct Settings {
|
|||
clearGreen: f32,
|
||||
clearBlue: f32,
|
||||
clearAlpha: f32,
|
||||
padding0: f32,
|
||||
eraserRadius: f32,
|
||||
padding1: f32,
|
||||
};
|
||||
|
||||
|
|
@ -15,7 +15,8 @@ struct VertexOutput {
|
|||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32,
|
||||
}
|
||||
|
||||
struct EraserTextureTargets {
|
||||
|
|
@ -37,19 +38,26 @@ fn vertex(
|
|||
@location(0) start: vec2<f32>,
|
||||
@location(1) end: vec2<f32>
|
||||
) -> VertexOutput {
|
||||
let screenPosition = segment_vertex_position(vertexIndex, start, end, sqrt(settings.eraserRadiusSquared));
|
||||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
var inverseLengthSquared = 0.0;
|
||||
if denominator > settings.lineDistanceEpsilon {
|
||||
inverseLengthSquared = 1.0 / denominator;
|
||||
}
|
||||
let screenPosition = segment_vertex_position(vertexIndex, start, end, settings.eraserRadius);
|
||||
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);
|
||||
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, direction, inverseLengthSquared);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
||||
) -> @location(0) vec4<f32> {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
|
||||
discard;
|
||||
}
|
||||
|
||||
|
|
@ -60,9 +68,10 @@ fn fragment(
|
|||
fn fragmentMrt(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
||||
) -> EraserTextureTargets {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
|
||||
discard;
|
||||
}
|
||||
|
||||
|
|
@ -74,14 +83,19 @@ fn fragmentMrt(
|
|||
fn fragmentCombined(
|
||||
@location(0) screenPosition: vec2<f32>,
|
||||
@location(1) @interpolate(flat) start: vec2<f32>,
|
||||
@location(2) @interpolate(flat) end: vec2<f32>
|
||||
@location(2) @interpolate(flat) direction: vec2<f32>,
|
||||
@location(3) @interpolate(flat) inverseLengthSquared: f32
|
||||
) -> EraserCombinedTargets {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, end) {
|
||||
if shouldDiscardEraserFragment(screenPosition, start, direction, inverseLengthSquared) {
|
||||
discard;
|
||||
}
|
||||
|
||||
let cleared = getEraserClearValue();
|
||||
return EraserCombinedTargets(cleared, cleared, cleared, cleared);
|
||||
return EraserCombinedTargets(getEraserMaskValue(), cleared, cleared, cleared);
|
||||
}
|
||||
|
||||
fn getEraserMaskValue() -> vec4<f32> {
|
||||
return vec4<f32>(settings.clearAlpha, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
fn getEraserClearValue() -> vec4<f32> {
|
||||
|
|
@ -96,21 +110,25 @@ fn getEraserClearValue() -> vec4<f32> {
|
|||
fn shouldDiscardEraserFragment(
|
||||
screenPosition: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
end: vec2<f32>
|
||||
direction: vec2<f32>,
|
||||
inverseLengthSquared: f32
|
||||
) -> bool {
|
||||
return distanceSquaredFromLine(screenPosition, start, end) > settings.eraserRadiusSquared;
|
||||
return distanceSquaredFromLine(screenPosition, start, direction, inverseLengthSquared) > settings.eraserRadiusSquared;
|
||||
}
|
||||
|
||||
fn distanceSquaredFromLine(position: vec2<f32>, start: vec2<f32>, end: vec2<f32>) -> f32 {
|
||||
fn distanceSquaredFromLine(
|
||||
position: vec2<f32>,
|
||||
start: vec2<f32>,
|
||||
direction: vec2<f32>,
|
||||
inverseLengthSquared: f32
|
||||
) -> f32 {
|
||||
let pa = position - start;
|
||||
let direction = end - start;
|
||||
let denominator = dot(direction, direction);
|
||||
|
||||
if denominator <= settings.lineDistanceEpsilon {
|
||||
if inverseLengthSquared <= 0.0 {
|
||||
return dot(pa, pa);
|
||||
}
|
||||
|
||||
let q = clamp(dot(pa, direction) / denominator, 0.0, 1.0);
|
||||
let q = clamp(dot(pa, direction) * inverseLengthSquared, 0.0, 1.0);
|
||||
let nearestOffset = pa - direction * q;
|
||||
return dot(nearestOffset, nearestOffset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from '../../utils/graphics/cached-buffer-write';
|
||||
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { rgbChannelToUnit, type RgbColor } from '../../utils/rgb-color';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import { RenderSettings } from './render-settings';
|
||||
import shader from './render.wgsl?raw';
|
||||
|
|
@ -13,8 +14,6 @@ export class RenderPipeline {
|
|||
|
||||
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(
|
||||
|
|
@ -35,14 +34,8 @@ export class RenderPipeline {
|
|||
|
||||
const vertex = setUpFullScreenQuad(device);
|
||||
|
||||
this.sampler = device.createSampler({
|
||||
magFilter: 'linear',
|
||||
minFilter: 'linear',
|
||||
});
|
||||
|
||||
const format = navigator.gpu.getPreferredCanvasFormat();
|
||||
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,
|
||||
|
|
@ -84,25 +77,25 @@ export class RenderPipeline {
|
|||
renderBrushColorStrengthMultiplier,
|
||||
backgroundGrainStrength,
|
||||
}: RenderSettings & {
|
||||
channelColors: Array<[number, number, number]>;
|
||||
backgroundColor: [number, number, number];
|
||||
channelColors: [RgbColor, RgbColor, RgbColor];
|
||||
backgroundColor: RgbColor;
|
||||
}) {
|
||||
const [a, b, c] = channelColors;
|
||||
this.uniformValues[0] = a[0];
|
||||
this.uniformValues[1] = a[1];
|
||||
this.uniformValues[2] = a[2];
|
||||
this.uniformValues[0] = rgbChannelToUnit(a[0]);
|
||||
this.uniformValues[1] = rgbChannelToUnit(a[1]);
|
||||
this.uniformValues[2] = rgbChannelToUnit(a[2]);
|
||||
this.uniformValues[3] = 0;
|
||||
this.uniformValues[4] = b[0];
|
||||
this.uniformValues[5] = b[1];
|
||||
this.uniformValues[6] = b[2];
|
||||
this.uniformValues[4] = rgbChannelToUnit(b[0]);
|
||||
this.uniformValues[5] = rgbChannelToUnit(b[1]);
|
||||
this.uniformValues[6] = rgbChannelToUnit(b[2]);
|
||||
this.uniformValues[7] = 0;
|
||||
this.uniformValues[8] = c[0];
|
||||
this.uniformValues[9] = c[1];
|
||||
this.uniformValues[10] = c[2];
|
||||
this.uniformValues[8] = rgbChannelToUnit(c[0]);
|
||||
this.uniformValues[9] = rgbChannelToUnit(c[1]);
|
||||
this.uniformValues[10] = rgbChannelToUnit(c[2]);
|
||||
this.uniformValues[11] = 0;
|
||||
this.uniformValues[12] = backgroundColor[0];
|
||||
this.uniformValues[13] = backgroundColor[1];
|
||||
this.uniformValues[14] = backgroundColor[2];
|
||||
this.uniformValues[12] = rgbChannelToUnit(backgroundColor[0]);
|
||||
this.uniformValues[13] = rgbChannelToUnit(backgroundColor[1]);
|
||||
this.uniformValues[14] = rgbChannelToUnit(backgroundColor[2]);
|
||||
this.uniformValues[15] = clarity;
|
||||
this.uniformValues[16] = renderTraceNormalizationFloor;
|
||||
this.uniformValues[17] = renderBrushColorBase;
|
||||
|
|
@ -162,7 +155,7 @@ export class RenderPipeline {
|
|||
},
|
||||
],
|
||||
});
|
||||
passEncoder.setPipeline(this.sampledPipeline);
|
||||
passEncoder.setPipeline(this.pipeline);
|
||||
this.commonState.execute(passEncoder);
|
||||
passEncoder.setBindGroup(1, bindGroup);
|
||||
passEncoder.draw(3, 1);
|
||||
|
|
@ -193,10 +186,6 @@ export class RenderPipeline {
|
|||
buffer: this.uniforms,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: this.sampler,
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: colorTexture,
|
||||
|
|
@ -226,13 +215,6 @@ export class RenderPipeline {
|
|||
type: 'uniform',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
sampler: {
|
||||
type: 'filtering',
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
|
|
|
|||
|
|
@ -14,27 +14,17 @@ struct Settings {
|
|||
};
|
||||
|
||||
@group(1) @binding(0) var<uniform> settings: Settings;
|
||||
@group(1) @binding(1) var Sampler: sampler;
|
||||
@group(1) @binding(2) var trailMap: texture_2d<f32>;
|
||||
@group(1) @binding(3) var sourceMap: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
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));
|
||||
}
|
||||
const NOISE_TEXTURE_MASK = 2047u;
|
||||
|
||||
@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 fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let pixel = vec2<i32>(position.xy);
|
||||
let traces = textureLoad(trailMap, pixel, 0);
|
||||
let sources = textureLoad(sourceMap, pixel, 0);
|
||||
return renderColor(traces, sources, pixel);
|
||||
}
|
||||
|
||||
fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<f32> {
|
||||
|
|
@ -48,6 +38,16 @@ fn renderColor(traces: vec4<f32>, sources: vec4<f32>, pixel: vec2<i32>) -> vec4<
|
|||
clarity(traces.g),
|
||||
clarity(traces.b)
|
||||
);
|
||||
if max(max(sources.r, sources.g), sources.b) <= 0.0 {
|
||||
let traceColor =
|
||||
traceStrengths.r * settings.colorA
|
||||
+ traceStrengths.g * settings.colorB
|
||||
+ traceStrengths.b * settings.colorC;
|
||||
let normalizedTraceColor = normalizeColorIntensity(traceColor);
|
||||
let traceStrength = max(max(traceStrengths.r, traceStrengths.g), traceStrengths.b);
|
||||
return vec4(mix(background, clamp(normalizedTraceColor, vec3(0), vec3(1)), traceStrength), 1);
|
||||
}
|
||||
|
||||
let sourceStrengths = vec3(
|
||||
clarity(sources.r),
|
||||
clarity(sources.g),
|
||||
|
|
@ -89,8 +89,7 @@ fn normalizeColorIntensity(color: vec3<f32>) -> vec3<f32> {
|
|||
}
|
||||
|
||||
fn getTexturedBackground(pixel: vec2<i32>) -> vec3<f32> {
|
||||
let noiseSize = vec2<i32>(textureDimensions(noise, 0));
|
||||
let noiseCoord = pixel % noiseSize;
|
||||
let noiseCoord = vec2<i32>(vec2<u32>(pixel) & vec2<u32>(NOISE_TEXTURE_MASK));
|
||||
let grain = textureLoad(noise, noiseCoord, 0).r - 0.5;
|
||||
|
||||
return clamp(
|
||||
|
|
|
|||
Loading…
Reference in a new issue