Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0e01d2d
mix and math merging
Dec 2, 2025
825f443
math fix, removing unnecessary files for math & mix
Dec 2, 2025
952ae74
fixes for mix
Dec 2, 2025
ce3b9b2
organization
Dec 2, 2025
dc849e2
more cleanup
Dec 2, 2025
f880063
format
Dec 2, 2025
3736de2
transform node generates stuff in the wrong pipeline atm
Dec 4, 2025
1164142
need to fix terrain renderer
Dec 4, 2025
767ecd2
I think it works??
Dec 4, 2025
9e5d28e
format fixes
Dec 4, 2025
4378c75
mask float ported over to code
Dec 5, 2025
45bd83d
code gen
Dec 5, 2025
07a18ea
format
Dec 5, 2025
00b4291
added identity if transform is undefined
Dec 5, 2025
a65ec48
create bitmap from image texture
sgmq0 Dec 5, 2025
48b0678
adding useEffect to scatter
Dec 5, 2025
e79ed20
adding useEffect to scatter
Dec 5, 2025
18a024b
undoing changes in this branch
Dec 5, 2025
859ccaa
load a single texture
sgmq0 Dec 5, 2025
bff9f19
Merge branch 'main' into neha/merge-nodes
Dec 5, 2025
059b4f0
format
Dec 5, 2025
e150a62
Merge branch 'main' into ray/texture-loading
sgmq0 Dec 5, 2025
1e1e664
gltf models that have material color and no texture
sgmq0 Dec 5, 2025
2fcff3d
Merge branch 'main' into ray/texture-loading
sgmq0 Dec 6, 2025
405802f
default texture color that works with objs
sgmq0 Dec 6, 2025
97cc9e3
avocado :)
sgmq0 Dec 6, 2025
78596ce
fixing to be y up
Dec 6, 2025
432e227
format
Dec 6, 2025
ce4b370
fix gltf loading
sgmq0 Dec 7, 2025
56b5719
format issues
sgmq0 Dec 7, 2025
19b0db9
Merge branch 'neha/merge-nodes' into ray/texture-loading
sgmq0 Dec 7, 2025
6d7184d
make textures mandatory
sgmq0 Dec 7, 2025
7eadef0
support for multiple textures
sgmq0 Dec 7, 2025
7b62801
Merge branch 'ray/texture-loading' into neha/scatter-mask-fix
sgmq0 Dec 7, 2025
190c543
instancing with generated shader
sgmq0 Dec 7, 2025
a0a795a
apply transforms in the right order (yay)
sgmq0 Dec 7, 2025
a8ee1c3
...?
sgmq0 Dec 7, 2025
87925ef
format
sgmq0 Dec 7, 2025
8355f7a
extremely sus threshold
sgmq0 Dec 7, 2025
6d80dd7
masking yay...
sgmq0 Dec 7, 2025
ad10143
Merge branch 'main' into neha/scatter-mask-fix
sgmq0 Dec 7, 2025
bff4258
format
sgmq0 Dec 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added models/BoxTextured.glb
Binary file not shown.
Binary file added models/PineTree.glb
Binary file not shown.
133 changes: 87 additions & 46 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/components/nodes/scatter-node.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { useEffect } from 'react';
import { useReactFlow, type NodeProps } from 'reactflow';

import * as helpers from './helpers';

import * as TerrainGenNode from '@/components/common/terraingen-node';
import { useGraphGlobals } from '@/hooks/use-graph-globals';
import * as nodeTypes from '@/lib/graph/node-types';

const HANDLES = nodeTypes.HANDLES.scatter;
type ScatterNodeData = nodeTypes.Scatter['data'];

function ScatterNode({ id, data, ...props }: NodeProps<ScatterNodeData>) {
const { setNodes } = useReactFlow();
const { triggerNodePipelineUpdate } = useGraphGlobals();

const onChange = (key: 'instances' | 'threshold', value: number) => {
helpers.updateNodeData<ScatterNodeData>({ id, setNodes, newData: { [key]: value } });
};

useEffect(() => {
triggerNodePipelineUpdate(id);
}, [data.instances, data.threshold, id, triggerNodePipelineUpdate]);

return (
<TerrainGenNode.Root title="Scatter" {...props}>
<TerrainGenNode.HandleOutput handleId={HANDLES.out.result} valueType="vec3f[]" />
Expand Down
14 changes: 14 additions & 0 deletions src/lib/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ function generatePipelines(
return { displacePipeline };
}

const maskEdge = edges.find(
(edge) =>
edge.target === scatterNode.id && edge.targetHandle === nodeTypes.HANDLES.scatter.in.a,
);

const maskSourceNode = orderedDependencyNodes.find((node) => node.id === maskEdge?.source);
let transformConfig: { translate: string; rotate: string; scale: string } | undefined;
if (transformNode) {
const translateEdge = edges.find(
Expand Down Expand Up @@ -203,6 +209,14 @@ function generatePipelines(
outgoingHandleId: scatterEdge.sourceHandle!,
}),
meshPath: geometryNode.data.meshPath,
maskKey:
maskSourceNode && maskEdge
? nodeMapping.getHandleKey({
sourceNode: maskSourceNode,
outgoingHandleId: maskEdge.sourceHandle!,
})
: undefined,
threshold: scatterNode.data.threshold,
transform: transformConfig,
fileContent:
geometryNode.type === 'loadGeo' ? (geometryNode.data.fileContent as string) : undefined,
Expand Down
18 changes: 17 additions & 1 deletion src/lib/renderers/pipelines/instance-points-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class InstancePointsPipeline {

instanceCount = 1;
instanceCountBuffer: GPUBuffer;
//numCreatedInstancesBuffer: GPUBuffer;

instancingBindGroupLayout: GPUBindGroupLayout;
instancingBindGroup: GPUBindGroup;
Expand All @@ -34,7 +35,7 @@ export class InstancePointsPipeline {
this.customInstanceCode = customInstanceCode;
this.instancePoints = this.device.createBuffer({
label: 'instancing points vertex buffer',
size: Math.max(this.instanceCount * 96, 96),
size: Math.max(this.instanceCount * 112, 112),
usage:
GPUBufferUsage.VERTEX |
GPUBufferUsage.COPY_DST |
Expand All @@ -52,6 +53,12 @@ export class InstancePointsPipeline {
});
this.device.queue.writeBuffer(this.instanceCountBuffer, 0, numInstances);

// this.numCreatedInstancesBuffer = device.createBuffer({
// label: "buffer for how many valid points have been created",
// size: 4,
// usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
// });

this.instancingBindGroupLayout = this.device.createBindGroupLayout({
label: 'instancing bind group layout',
entries: [
Expand All @@ -64,12 +71,21 @@ export class InstancePointsPipeline {
},
},
{
// number of instances
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'uniform',
},
},
// {
// // count how many instances we made
// binding: 2,
// visibility: GPUShaderStage.COMPUTE,
// buffer: {
// type: 'storage',
// },
// },
],
});

Expand Down
96 changes: 95 additions & 1 deletion src/lib/renderers/pipelines/instancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class IndirectInstancer {

instancePointsComputePipeline: InstancePointsPipeline;

textureBindGroup: GPUBindGroup;
textureArray: GPUTexture;

transformBuffer: GPUBuffer | undefined;
transformBindGroupLayout: GPUBindGroupLayout | undefined;
transformBindGroup: GPUBindGroup | undefined;
Expand All @@ -24,6 +27,7 @@ export class IndirectInstancer {
instanceIndexBuffer: GPUBuffer,
sceneUniformsBindGroupLayout: GPUBindGroupLayout,
webGPU: WebGPUContext,
imageBitmaps?: ImageBitmap[],
transformMatrix?: Float32Array,
) {
this.device = device;
Expand Down Expand Up @@ -135,10 +139,33 @@ export class IndirectInstancer {
],
});

const textureBindGroupLayout = this.device.createBindGroupLayout({
label: 'texture bind group layout',
entries: [
{
// sampler type...
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
sampler: { type: 'filtering' },
},
{
// texture view
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: 'float',
viewDimension: '2d-array',
},
},
],
});

const bindGroupLayouts = [
sceneUniformsBindGroupLayout,
this.instancingPointsBindGroupLayout,
textureBindGroupLayout,
];

if (this.transformBindGroupLayout) {
bindGroupLayouts.push(this.transformBindGroupLayout);
}
Expand Down Expand Up @@ -171,18 +198,85 @@ export class IndirectInstancer {
targets: [
{
format: webGPU.canvasFormat,
blend: {
color: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
alpha: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
},
},
},
],
},
});

// create buffers for the image bitmaps
const firstSource = imageBitmaps![0];
this.textureArray = this.device.createTexture({
label: 'FAT FUCKING TEXTURE!!!!',
format: 'rgba8unorm',
size: {
width: firstSource.width,
height: firstSource.height,
depthOrArrayLayers: 20,
},
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});

for (let i = 0; i < imageBitmaps!.length; i++) {
const source = imageBitmaps![i];

this.device.queue.copyExternalImageToTexture(
{ source: source, flipY: true },
{
texture: this.textureArray,
premultipliedAlpha: true,
origin: { x: 0, y: 0, z: i + 1 },
},
{ width: source.width, height: source.height },
);
}

const sampler = this.device.createSampler({
addressModeU: 'repeat',
addressModeV: 'repeat',
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
});

this.textureBindGroup = this.device.createBindGroup({
label: 'texture bind group',
layout: textureBindGroupLayout,
entries: [
{
binding: 0,
resource: sampler,
},
{
binding: 1,
resource: this.textureArray.createView({
dimension: '2d-array',
}),
},
],
});
}

runRenderPass(renderPass: GPURenderPassEncoder, sceneUniforms: GPUBindGroup) {
renderPass.setPipeline(this.instancingRenderPipeline);
renderPass.setBindGroup(0, sceneUniforms);
renderPass.setBindGroup(1, this.instancingPointsBindGroup);
renderPass.setBindGroup(2, this.textureBindGroup);

if (this.transformBindGroup) {
renderPass.setBindGroup(2, this.transformBindGroup);
renderPass.setBindGroup(3, this.transformBindGroup);
}
renderPass.drawIndirect(this.indirectInstanceBuffer, 0);
}
Expand Down
27 changes: 16 additions & 11 deletions src/lib/renderers/terrain-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ export class TerrainRenderer implements IRenderer {
colorAttachments: [
{
view: canvasTextureView,
clearValue: [0.3, 0, 0, 1],
clearValue: [0.0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
},
Expand Down Expand Up @@ -615,10 +615,10 @@ export class TerrainRenderer implements IRenderer {

if (config.outputs.fileContent) {
if (config.outputs.fileType === 'obj') {
mesh.parseObjContent(config.outputs.fileContent);
await mesh.parseObjContent(config.outputs.fileContent);
} else if (config.outputs.fileType === 'gltf' || config.outputs.fileType === 'glb') {
const { gltfWithBuffers, gltf } = await mesh.loadGltf(config.outputs.fileContent);
mesh.parseGLTFContent(gltfWithBuffers, gltf);
await mesh.parseGLTFContent(gltfWithBuffers, gltf);
}
} else {
await mesh.loadObj(path.join(import.meta.env.BASE_URL, config.outputs.meshPath));
Expand All @@ -641,13 +641,7 @@ export class TerrainRenderer implements IRenderer {
});
this.device.queue.writeBuffer(instanceIndexBuffer, 0, mesh.indices);

const customInstanceShader = jit.generateInstanceShaderCode(
config,
instanceComputeShaderTemplate,
);

console.log('custom instance shader:', customInstanceShader);

// set uniforms
this.nodeGraphUniformConfig = config.uniforms;
const { totalSize, offsets } = jit.calculateUniformLayout(config.uniforms);
this.nodeGraphUniformLayout = offsets;
Expand Down Expand Up @@ -686,14 +680,24 @@ export class TerrainRenderer implements IRenderer {
],
});

console.log('num instances:', config.outputs.instanceCount);
// create custom instancing shader
if (!config.outputs.maskKey) {
config.outputs.maskKey = 'terrainPos.y';
}
const customInstanceShader = jit.generateInstanceShaderCode(
config,
instanceComputeShaderTemplate,
);

console.log('custom instance shader:', customInstanceShader);

// Run compute to create a buffer of points
this.instancePointsComputePipeline = new InstancePointsPipeline(
this.device,
this.stage.groundPlane,
this.normalsComputePipeline,
config.outputs.instanceCount,
customInstanceShader,
);

const encoder = this.device.createCommandEncoder();
Expand Down Expand Up @@ -729,6 +733,7 @@ export class TerrainRenderer implements IRenderer {
instanceIndexBuffer,
this.sceneUniformsBindGroupLayout,
this.webGPU,
mesh.textures,
transformMatrix,
);
}
Expand Down
Loading