From e7a7f4330413c799157c7a0a57f7d318a9ed02b6 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Fri, 5 Dec 2025 23:50:05 -0500 Subject: [PATCH 01/15] water node basis --- src/components/common/webgpu-canvas.tsx | 3 +- .../editor/node-graph/context-menu.tsx | 3 + src/components/nodes/index.ts | 2 + src/components/nodes/water-node.tsx | 16 ++ src/hooks/use-pipelines.ts | 22 +- src/lib/graph/index.ts | 32 ++- src/lib/graph/node-mapping.ts | 2 + src/lib/graph/node-types.ts | 9 +- src/lib/renderers/terrain-renderer.ts | 256 +++++++++++++++++- src/lib/renderers/water-pipeline.ts | 93 +++++++ src/lib/scene.ts | 2 + src/lib/scene/stage.ts | 4 +- src/lib/shaders/shaders.ts | 2 + src/lib/shaders/water.cs.wgsl | 63 +++++ src/lib/shaders/water.fs.wgsl | 86 ++++++ 15 files changed, 584 insertions(+), 11 deletions(-) create mode 100644 src/components/nodes/water-node.tsx create mode 100644 src/lib/renderers/water-pipeline.ts create mode 100644 src/lib/shaders/water.cs.wgsl create mode 100644 src/lib/shaders/water.fs.wgsl diff --git a/src/components/common/webgpu-canvas.tsx b/src/components/common/webgpu-canvas.tsx index 5654539..ddeda24 100644 --- a/src/components/common/webgpu-canvas.tsx +++ b/src/components/common/webgpu-canvas.tsx @@ -105,7 +105,8 @@ export default function WebGPUCanvas({ // setup scene const camera = new Camera(webGPUContext); const mesh = new Plane(20, 100); - const stage = new Stage(camera, mesh); + const waterMesh = new Plane(20, 100); + const stage = new Stage(camera, mesh, waterMesh); const controller = new AbortController(); const init = async () => { diff --git a/src/components/editor/node-graph/context-menu.tsx b/src/components/editor/node-graph/context-menu.tsx index 2d8d062..ce28ed9 100644 --- a/src/components/editor/node-graph/context-menu.tsx +++ b/src/components/editor/node-graph/context-menu.tsx @@ -177,6 +177,9 @@ export default function ContextMenu({ state, closeMenu, reactFlowWrapper }: Cont createNode('terrain')}> Terrain (Output) + createNode('water')}> + Water (Output) + diff --git a/src/components/nodes/index.ts b/src/components/nodes/index.ts index c773bfc..fd3a940 100644 --- a/src/components/nodes/index.ts +++ b/src/components/nodes/index.ts @@ -21,6 +21,7 @@ import TrigMathNodeFloat from './trig-math-node'; import UnsignedIntNode from './unsigned-int-node'; import VectorNode from './vector-node'; import VertexDataNode from './vertex-data-node'; +import WaterNode from './water-node'; export const nodeTypes: NodeTypes = { transform: TransformNode, @@ -44,4 +45,5 @@ export const nodeTypes: NodeTypes = { unsignedInt: UnsignedIntNode, loadGeo: LoadGeoNode, builtinGeo: BuiltInGeoNode, + water: WaterNode, }; diff --git a/src/components/nodes/water-node.tsx b/src/components/nodes/water-node.tsx new file mode 100644 index 0000000..79d2f53 --- /dev/null +++ b/src/components/nodes/water-node.tsx @@ -0,0 +1,16 @@ +import type { NodeProps } from 'reactflow'; + +import * as TerrainGenNode from '@/components/common/terraingen-node'; +import * as nodeTypes from '@/lib/graph/node-types'; + +const HANDLES = nodeTypes.HANDLES.water; + +function WaterNode({ ...props }: NodeProps) { + return ( + + + + ); +} + +export default WaterNode; diff --git a/src/hooks/use-pipelines.ts b/src/hooks/use-pipelines.ts index 2807808..e80b176 100644 --- a/src/hooks/use-pipelines.ts +++ b/src/hooks/use-pipelines.ts @@ -19,6 +19,13 @@ export function usePipelines({ terrainRendererRef }: UsePipelinesOptions) { [terrainRendererRef], ); + const setWaterPipeline = useCallback( + (pipeline: scene.WaterPipeline) => { + terrainRendererRef.current?.configureWaterPipeline(pipeline); + }, + [terrainRendererRef], + ); + const setInstancingPipeline = useCallback( (pipeline: scene.InstancingPipeline) => { void terrainRendererRef.current?.configureInstancingPipeline(pipeline); @@ -29,36 +36,41 @@ export function usePipelines({ terrainRendererRef }: UsePipelinesOptions) { const setUniform = useCallback( (key: string, value: number | [number, number, number]) => { console.log('setting uniform', key, 'to', value); + // Try terrain pipeline first terrainRendererRef.current?.setDisplacePipelineUniform(key, value); + // Also try water pipeline (it will log a warning if not found, which is fine) + terrainRendererRef.current?.setWaterPipelineUniform(key, value); }, [terrainRendererRef], ); const rebuildPipelinesFromNode = useCallback( (nodeId: string, options: { nodes: graph.PipelineNode[]; edges: Edge[] }) => { - const { displacePipeline, instancingPipeline } = graph.generatePipelinesFromNode( + const { displacePipeline, waterPipeline, instancingPipeline } = graph.generatePipelinesFromNode( nodeId, options.nodes, options.edges, ); if (displacePipeline) setDisplacePipeline(displacePipeline); + if (waterPipeline) setWaterPipeline(waterPipeline); if (instancingPipeline) void setInstancingPipeline(instancingPipeline); }, - [setDisplacePipeline, setInstancingPipeline], + [setDisplacePipeline, setWaterPipeline, setInstancingPipeline], ); const rebuildAllPipelines = useCallback( (options: { nodes: graph.PipelineNode[]; edges: Edge[] }) => { - const { displacePipeline, instancingPipeline } = graph.generateAllPipelines( + const { displacePipeline, waterPipeline, instancingPipeline } = graph.generateAllPipelines( options.nodes, options.edges, ); if (displacePipeline) setDisplacePipeline(displacePipeline); - if (instancingPipeline) setInstancingPipeline(instancingPipeline); + if (waterPipeline) setWaterPipeline(waterPipeline); + if (instancingPipeline) void setInstancingPipeline(instancingPipeline); }, - [setDisplacePipeline, setInstancingPipeline], + [setDisplacePipeline, setWaterPipeline, setInstancingPipeline], ); return { diff --git a/src/lib/graph/index.ts b/src/lib/graph/index.ts index 63a41b3..e4a88e4 100644 --- a/src/lib/graph/index.ts +++ b/src/lib/graph/index.ts @@ -10,6 +10,7 @@ import type * as util from '@/lib/shaders/jit/types/util'; export type OutputNodeUpdates = { displacePipeline?: scene.DisplacePipeline; instancingPipeline?: scene.InstancingPipeline; + waterPipeline?: scene.WaterPipeline; }; export type PipelineNode = types.Node & nodeTypes.All; @@ -86,6 +87,35 @@ function generatePipelines( displacePipeline = { instructionSet, uniforms, outputs }; } + // find water pipeline + const waterNode = activeNodes.find((node) => node.type === 'water'); + let waterPipeline: scene.WaterPipeline | undefined = undefined; + if (waterNode) { + const orderedDependencyNodes = traversal.getOrderedNodes(waterNode.id, nodes, edges); + + // generate uniforms + const uniforms = orderedDependencyNodes.flatMap(getUniforms); + + // generate instruction set + const instructionSet = orderedDependencyNodes + .map((node) => getInstruction(node, orderedDependencyNodes, edges)) + .filter((instruction) => instruction !== null); + + // Get height key from the incoming edge of the water node + const heightEdge = edges.find((edge) => edge.target === waterNode.id); + const heightEdgeSourceNode = orderedDependencyNodes.find( + (node) => node.id === heightEdge?.source, + ); + const outputs: scene.WaterPipeline['outputs'] = { + height: nodeMapping.getHandleKey({ + sourceNode: heightEdgeSourceNode!, + outgoingHandleId: heightEdge!.sourceHandle!, + }), + }; + + waterPipeline = { instructionSet, uniforms, outputs }; + } + const instancingNode = activeNodes.find((node) => node.type === 'instancing'); let instancingPipeline: scene.InstancingPipeline | undefined = undefined; @@ -151,7 +181,7 @@ function generatePipelines( }; } - return { displacePipeline, instancingPipeline }; + return { displacePipeline, waterPipeline, instancingPipeline }; } function getInstruction( diff --git a/src/lib/graph/node-mapping.ts b/src/lib/graph/node-mapping.ts index 36de301..b26f517 100644 --- a/src/lib/graph/node-mapping.ts +++ b/src/lib/graph/node-mapping.ts @@ -114,6 +114,7 @@ export const INSTRUCTION_MAPPING: InstructionMapping null, terrain: () => null, + water: () => null, // TODO: this transform functionality is not real until we have geometry transform: dummyHandler, @@ -299,4 +300,5 @@ export const UNIFORM_MAPPING: UniformMapping builtinGeo: () => [], scatter: () => [], instancing: () => [], + water: () => [], }; diff --git a/src/lib/graph/node-types.ts b/src/lib/graph/node-types.ts index 8ef0fd1..3e03eba 100644 --- a/src/lib/graph/node-types.ts +++ b/src/lib/graph/node-types.ts @@ -31,6 +31,7 @@ export type BuiltinGeometry = Node<'builtinGeo', { meshPath: string }>; export type Scatter = Node<'scatter', { instances: number; threshold: number }>; export type Instancing = Node<'instancing'>; export type UnsignedInt = Node<'unsignedInt', { value: number }>; +export type Water = Node<'water'>; export type All = | Vector @@ -53,7 +54,8 @@ export type All = | BuiltinGeometry | Scatter | Instancing - | UnsignedInt; + | UnsignedInt + | Water; /** * Handle IDs for each node type @@ -149,6 +151,10 @@ export const HANDLES = { in: {}, out: { result: 'uint-out' }, }, + water: { + in: { height: 'float-height-in' }, + out: {}, + }, } as const satisfies { [nodeType in All['type']]: { in: Record; out: Record }; }; @@ -188,4 +194,5 @@ export const NODE_PREFABS: { [nodeType in All['type']]: All & { type: nodeType } loadGeo: { type: 'loadGeo', data: { meshPath: '', fileContent: '', fileType: '' } }, builtinGeo: { type: 'builtinGeo', data: { meshPath: '/models/tree.obj' } }, unsignedInt: { type: 'unsignedInt', data: { value: 0 } }, + water: { type: 'water', data: {} }, }; diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 985c00e..3bbf318 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -1,5 +1,7 @@ import path from 'path-browserify'; +import { WaterPipeline } from './water-pipeline'; + import type { IRenderer } from '@/components/common/webgpu-canvas'; import { InstancePointsPipeline } from '@/lib/renderers/pipelines/instance-points-pipeline'; import { IndirectInstancer } from '@/lib/renderers/pipelines/instancer'; @@ -14,6 +16,7 @@ import { instanceComputeShaderTemplate } from '@/lib/shaders/jit/templates/insta import * as shaders from '@/lib/shaders/shaders'; import type { WebGPUContext } from '@/lib/webgpu-context'; + export type TerrainRendererGlobalParameters = { size: number; resolution: number; @@ -36,6 +39,9 @@ export class TerrainRenderer implements IRenderer { // terrain compute pipeline private readonly terrainComputePipeline: TerrainPipeline; + // water compute pipeline + private readonly waterComputePipeline: WaterPipeline; + // custom compute pipeline private readonly customBindGroupLayout: GPUBindGroupLayout; private readonly customBindGroup: GPUBindGroup; @@ -79,6 +85,25 @@ export class TerrainRenderer implements IRenderer { /** Custom displace pipeline, gets reconfigured whenever node structure is changed */ private customDisplacePipeline: GPUComputePipeline; + // Water pipeline state + private waterPipelineConfigured: boolean = false; + + // Water custom compute pipeline + private readonly customWaterBindGroupLayout: GPUBindGroupLayout; + private readonly customWaterBindGroup: GPUBindGroup; + + private readonly customWaterUniformBindGroupLayout: GPUBindGroupLayout; + private readonly customWaterUniformBindGroup: GPUBindGroup; + + private customWaterNodeGraphUniformsBindGroupLayout: GPUBindGroupLayout; + private customWaterNodeGraphUniformsBindGroup: GPUBindGroup; + + private waterNodeGraphUniformBuffer!: GPUBuffer; + private waterNodeGraphUniformLayout!: Map | undefined; + private waterNodeGraphUniformConfig!: scene.WaterPipeline['uniforms'] | undefined; + + private customWaterDisplacePipeline: GPUComputePipeline; + /** overall render pipeline, must get recreated upon canvas resize */ private pipeline: GPURenderPipeline; @@ -116,7 +141,8 @@ export class TerrainRenderer implements IRenderer { // create vertex data this.stage.groundPlane.createBuffers(this.device); - + // create vertex data for water + this.stage.waterPlane.createBuffers(this.device); // set up bind groups, layouts, pipelines etc // scene uniform layouts and groups @@ -155,6 +181,8 @@ export class TerrainRenderer implements IRenderer { // compute pipeline that creates the terrain this.terrainComputePipeline = new TerrainPipeline(this.device, this.stage.groundPlane); + // compute pipeline that creates the water plane + this.waterComputePipeline = new WaterPipeline(this.device, this.stage.waterPlane); // ---------------------------------------------------------------------------------------- // -------------------- CUSTOM COMPUTE PIPELINE @@ -237,6 +265,78 @@ export class TerrainRenderer implements IRenderer { 2, ); + // ---------------------------------------------------------------------------------------- + // -------------------- CUSTOM WATER COMPUTE PIPELINE + // ---------------------------------------------------------------------------------------- + + this.customWaterBindGroupLayout = this.device.createBindGroupLayout({ + label: 'custom water compute bind group layout', + entries: [ + { + // vertices + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + }); + + this.customWaterBindGroup = this.device.createBindGroup({ + label: 'custom water compute bind group', + layout: this.customWaterBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this.stage.waterPlane.vertexBuffer! } }, + ], + }); + + this.customWaterUniformBindGroupLayout = this.device.createBindGroupLayout({ + label: 'custom water compute uniform bind group layout', + entries: [ + { + // uniform containing mesh size and resolution + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + ], + }); + + this.customWaterUniformBindGroup = this.device.createBindGroup({ + label: 'custom water compute uniform bind group', + layout: this.customWaterUniformBindGroupLayout, + entries: [{ binding: 0, resource: { buffer: this.stage.waterPlane.uniformsBuffer! } }], + }); + + this.customWaterNodeGraphUniformsBindGroupLayout = this.device.createBindGroupLayout({ + label: 'custom water node graph uniform bind group layout', + entries: [], + }); + + this.customWaterNodeGraphUniformsBindGroup = this.device.createBindGroup({ + label: 'custom water node graph uniforms bind group', + layout: this.customWaterNodeGraphUniformsBindGroupLayout, + entries: [], + }); + + this.customWaterDisplacePipeline = this.device.createComputePipeline({ + label: 'custom water compute pipeline', + layout: this.device.createPipelineLayout({ + label: 'custom water compute pipeline layout', + bindGroupLayouts: [this.customWaterBindGroupLayout, this.customWaterUniformBindGroupLayout], + }), + compute: { + module: this.device.createShaderModule({ + label: 'custom water compute shader', + code: shaders.waterComputeSrc, + }), + entryPoint: 'main', + }, + }); + // ---------------------------------------------------------------------------------------- // -------------------- RUNNING COMPUTES // ---------------------------------------------------------------------------------------- @@ -264,7 +364,19 @@ export class TerrainRenderer implements IRenderer { // pass 3: calculate terrain normals this.normalsComputePipeline.runComputePass(computePass); - // pass 4: create points on terrain to instance on + // pass 4: create water plane + this.waterComputePipeline.runComputePass(computePass); + + // pass 5: run custom water compute pipeline from node graph + if (this.waterPipelineConfigured) { + computePass.setPipeline(this.customWaterDisplacePipeline); + computePass.setBindGroup(0, this.customWaterBindGroup); + computePass.setBindGroup(1, this.customWaterUniformBindGroup); + computePass.setBindGroup(2, this.customWaterNodeGraphUniformsBindGroup); + computePass.dispatchWorkgroups(Math.ceil(this.stage.waterPlane.numVertices / 64)); + } + + // pass 6: create points on terrain to instance on this.instancePointsComputePipeline.runComputePass(computePass); computePass.end(); @@ -330,6 +442,141 @@ export class TerrainRenderer implements IRenderer { }); } + configureWaterPipeline(config: scene.WaterPipeline) { + this.waterPipelineConfigured = true; + + const customWaterComputeShader = jit.generateDisplaceShaderCode( + config, + displaceComputeShaderTemplate, + ); + + console.log('custom water compute shader:', customWaterComputeShader); + + this.waterNodeGraphUniformConfig = config.uniforms; + + const { totalSize, offsets } = jit.calculateUniformLayout(config.uniforms); + this.waterNodeGraphUniformLayout = offsets; + + if (this.waterNodeGraphUniformBuffer) { + this.waterNodeGraphUniformBuffer.destroy(); + } + + this.waterNodeGraphUniformBuffer = this.device.createBuffer({ + label: 'water uniform buffer', + size: Math.max(totalSize, 16), + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.initializeWaterNodeGraphUniforms(config.uniforms); + + this.customWaterNodeGraphUniformsBindGroupLayout = this.device.createBindGroupLayout({ + label: 'custom water nodegraph bind group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'uniform' }, + }, + ], + }); + + this.customWaterNodeGraphUniformsBindGroup = this.device.createBindGroup({ + label: 'custom water nodegraph bind group', + layout: this.customWaterNodeGraphUniformsBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.waterNodeGraphUniformBuffer }, + }, + ], + }); + + this.customWaterDisplacePipeline = this.device.createComputePipeline({ + label: 'custom water compute pipeline', + layout: this.device.createPipelineLayout({ + label: 'custom water compute pipeline layout', + bindGroupLayouts: [ + this.customWaterBindGroupLayout, + this.customWaterUniformBindGroupLayout, + this.customWaterNodeGraphUniformsBindGroupLayout, + ], + }), + compute: { + module: this.device.createShaderModule({ + label: 'custom water compute shader', + code: customWaterComputeShader, + }), + entryPoint: 'main', + }, + }); + + // RUN COMPUTE PIPELINE + const encoder = this.device.createCommandEncoder(); + this.runComputes(encoder); + this.device.queue.submit([encoder.finish()]); + } + + private initializeWaterNodeGraphUniforms(uniforms: scene.WaterPipeline['uniforms']) { + if (!this.waterNodeGraphUniformBuffer || !this.waterNodeGraphUniformLayout) return; + + for (const uniform of uniforms) { + if (uniform.initialValue !== null) { + const offset = this.waterNodeGraphUniformLayout.get(uniform.key); + if (offset === undefined) continue; + + if (uniform.type === 'f32') { + const data = new Float32Array([uniform.initialValue]); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } else if (uniform.type === 'u32') { + const data = new Uint32Array([uniform.initialValue]); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } else if (uniform.type === 'vec3f') { + const data = new Float32Array(uniform.initialValue); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } + } + } + } + + disableWaterPipeline() { + this.waterPipelineConfigured = false; + } + + setWaterPipelineUniform(key: string, value: number | [number, number, number]) { + if (!this.waterPipelineConfigured) { + console.log('Cannot set water uniform'); + return; + } + + const offset = this.waterNodeGraphUniformLayout?.get(key); + if (offset === undefined) { + console.warn(`Water uniform key "${key}" not found`); + return; + } + + const uniformConfig = this.waterNodeGraphUniformConfig?.find((u) => u.key === key); + if (!uniformConfig) { + console.warn(`Water uniform config for key "${key}" not found`); + return; + } + + if (uniformConfig.type === 'f32') { + const data = new Float32Array([value as number]); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } else if (uniformConfig.type === 'u32') { + const data = new Uint32Array([value as number]); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } else if (uniformConfig.type === 'vec3f') { + const data = new Float32Array(value as [number, number, number]); + this.device.queue.writeBuffer(this.waterNodeGraphUniformBuffer, offset, data); + } + + const encoder = this.device.createCommandEncoder(); + this.runComputes(encoder); + this.device.queue.submit([encoder.finish()]); + console.log('rerunning compute after water uniform update'); + } + // ------------------------------------------------------------------------------------------ // ------ Required methods for IRenderer interface // ------------------------------------------------------------------------------------------ @@ -375,6 +622,11 @@ export class TerrainRenderer implements IRenderer { renderPass.setIndexBuffer(this.stage.groundPlane.indexBuffer!, 'uint32'); renderPass.drawIndexedIndirect(this.stage.groundPlane.indirectBuffer!, 0); + // Render water plane + renderPass.setVertexBuffer(0, this.stage.waterPlane.vertexBuffer); + renderPass.setIndexBuffer(this.stage.waterPlane.indexBuffer!, 'uint32'); + renderPass.drawIndexedIndirect(this.stage.waterPlane.indirectBuffer!, 0); + if (this.indirectInstancer) { this.indirectInstancer.runRenderPass(renderPass, this.sceneUniformsBindGroup); } diff --git a/src/lib/renderers/water-pipeline.ts b/src/lib/renderers/water-pipeline.ts new file mode 100644 index 0000000..099300c --- /dev/null +++ b/src/lib/renderers/water-pipeline.ts @@ -0,0 +1,93 @@ +import { Mesh } from '@/lib/scene/mesh'; +import * as shaders from '@/lib/shaders/shaders'; + +export class WaterPipeline { + device: GPUDevice; + mesh: Mesh; + + waterDataBindGroupLayout: GPUBindGroupLayout; + waterDataBindGroup: GPUBindGroup; + + waterUniformBindGroupLayout: GPUBindGroupLayout; + waterUniformBindGroup: GPUBindGroup; + + waterPipeline: GPUComputePipeline; + + constructor(device: GPUDevice, mesh: Mesh) { + this.device = device; + this.mesh = mesh; + + this.waterDataBindGroupLayout = this.device.createBindGroupLayout({ + label: 'water compute bind group layout', + entries: [ + { + // vertices + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + { + // indices + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + }); + + this.waterDataBindGroup = this.device.createBindGroup({ + label: 'water compute bind group', + layout: this.waterDataBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this.mesh.vertexBuffer! } }, + { binding: 1, resource: { buffer: this.mesh.indexBuffer! } }, + ], + }); + + this.waterUniformBindGroupLayout = this.device.createBindGroupLayout({ + label: 'water compute uniform bind group layout', + entries: [ + { + // uniform containing mesh size and resolution + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + ], + }); + + this.waterUniformBindGroup = this.device.createBindGroup({ + label: 'water compute uniform bind group', + layout: this.waterUniformBindGroupLayout, + entries: [{ binding: 0, resource: { buffer: this.mesh.uniformsBuffer! } }], + }); + + this.waterPipeline = this.device.createComputePipeline({ + label: 'water compute pipeline', + layout: this.device.createPipelineLayout({ + label: 'water compute pipeline layout', + bindGroupLayouts: [this.waterDataBindGroupLayout, this.waterUniformBindGroupLayout], + }), + compute: { + module: this.device.createShaderModule({ + label: 'water compute shader', + code: shaders.waterComputeSrc, + }), + entryPoint: 'main', + }, + }); + } + + runComputePass(computePass: GPUComputePassEncoder) { + computePass.setPipeline(this.waterPipeline); + computePass.setBindGroup(0, this.waterDataBindGroup); + computePass.setBindGroup(1, this.waterUniformBindGroup); + computePass.dispatchWorkgroups(Math.ceil(this.mesh.numVertices / 64)); + } +} \ No newline at end of file diff --git a/src/lib/scene.ts b/src/lib/scene.ts index 90e40c8..df61a3c 100644 --- a/src/lib/scene.ts +++ b/src/lib/scene.ts @@ -7,6 +7,8 @@ export type DisplacePipeline = jitShaders.DisplaceShaderConfig; export type InstancingPipeline = jitShaders.InstancingShaderConfig; +export type WaterPipeline = jitShaders.DisplaceShaderConfig; + export type PreviewNode = { bar: string; }; diff --git a/src/lib/scene/stage.ts b/src/lib/scene/stage.ts index 2c85ec9..638003f 100644 --- a/src/lib/scene/stage.ts +++ b/src/lib/scene/stage.ts @@ -4,9 +4,11 @@ import { Plane } from './mesh'; export class Stage { readonly camera: Camera; readonly groundPlane: Plane; + readonly waterPlane: Plane; - constructor(camera: Camera, mesh: Plane) { + constructor(camera: Camera, mesh: Plane, water: Plane) { this.camera = camera; this.groundPlane = mesh; + this.waterPlane = water; } } diff --git a/src/lib/shaders/shaders.ts b/src/lib/shaders/shaders.ts index 45797d9..7a1c657 100644 --- a/src/lib/shaders/shaders.ts +++ b/src/lib/shaders/shaders.ts @@ -8,6 +8,7 @@ import naiveFragRaw from './naive.fs.wgsl?raw'; import naiveVertRaw from './naive.vs.wgsl?raw'; import normalsComputeRaw from './normals.cs.wgsl?raw'; import terrainComputeRaw from './terrain.cs.wgsl?raw'; +import waterComputeRaw from './water.cs.wgsl?raw'; // CONSTANTS (for use in shaders) (need to be hardcoded in deployed environment) @@ -34,3 +35,4 @@ export const terrainComputeSrc: string = processShaderRaw(terrainComputeRaw); export const normalsComputeSrc: string = processShaderRaw(normalsComputeRaw); export const terrainPointsComputeSrc: string = processShaderRaw(instancePointsComputeRaw); export const instanceSrc: string = processShaderRaw(instancingRaw); +export const waterComputeSrc: string = processShaderRaw(waterComputeRaw); \ No newline at end of file diff --git a/src/lib/shaders/water.cs.wgsl b/src/lib/shaders/water.cs.wgsl new file mode 100644 index 0000000..e830dc9 --- /dev/null +++ b/src/lib/shaders/water.cs.wgsl @@ -0,0 +1,63 @@ +// compute shader for generating water plane geometry + +@group(0) @binding(0) +var vertices: array; + +@group(0) @binding(1) +var indices: array; + +@group(1) @binding(0) +var meshUniforms : MeshUniforms; + +fn vertexOffset(i: u32) -> u32 { + return i * 8u; +} + +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) id: vec3) { + let subdivisions = u32(meshUniforms.resolution); + let size = meshUniforms.size; + let step = size / f32(subdivisions); + + let vertexCount = (subdivisions + 1u) * (subdivisions + 1u); + let indexCount = subdivisions * subdivisions * 6u; + + // generate vertices for water plane + if (id.x < vertexCount) { + let row = id.x / (subdivisions + 1u); + let col = id.x % (subdivisions + 1u); + + let x = -size / 2.0 + f32(col) * step; + let z = -size / 2.0 + f32(row) * step; + + let vOffset = vertexOffset(id.x); + vertices[vOffset + 0] = x; // pos.x + vertices[vOffset + 1] = 0.0; // pos.y (height - will be set by custom pipeline) + vertices[vOffset + 2] = z; // pos.z + vertices[vOffset + 3] = 0.0; // nor.x + vertices[vOffset + 4] = 1.0; // nor.y + vertices[vOffset + 5] = 0.0; // nor.z + vertices[vOffset + 6] = 0.0; // uv.x + vertices[vOffset + 7] = 0.0; // uv.y + } + + // generate indices for water plane + if (id.x < subdivisions * subdivisions) { + let i: u32 = id.x / subdivisions; + let j: u32 = id.x % subdivisions; + + let topLeft: u32 = i * (subdivisions + 1u) + j; + let topRight: u32 = topLeft + 1u; + let bottomLeft: u32 = (i + 1u) * (subdivisions + 1u) + j; + let bottomRight: u32 = bottomLeft + 1u; + + let baseIndex: u32 = id.x * 6u; + indices[baseIndex + 0u] = topLeft; + indices[baseIndex + 1u] = bottomLeft; + indices[baseIndex + 2u] = topRight; + + indices[baseIndex + 3u] = bottomLeft; + indices[baseIndex + 4u] = bottomRight; + indices[baseIndex + 5u] = topRight; + } +} \ No newline at end of file diff --git a/src/lib/shaders/water.fs.wgsl b/src/lib/shaders/water.fs.wgsl new file mode 100644 index 0000000..3092294 --- /dev/null +++ b/src/lib/shaders/water.fs.wgsl @@ -0,0 +1,86 @@ +struct FragmentInput +{ + @location(0) pos: vec3f, + @location(1) nor: vec3f, + @location(2) uv: vec2f +} + +fn random3D(seed: vec3f) -> vec3f { + let dot_product = dot(seed, vec3f(12.9898, 78.233, 45.164)); + let sin_value = sin(dot_product) * 43758.5453; + let fract_value = fract(sin_value); + + return vec3f(fract_value, fract_value, fract_value); +} + +fn worley_noise(pos: vec3f) -> f32 { + var posInt = floor(pos); + var posFract = fract(pos); + var minDist = 1.0; + + for (var z: i32 = -1; z <= 1; z = z + 1) { + for (var y: i32 = -1; y <= 1; y = y + 1) { + for (var x: i32 = -1; x <= 1; x = x + 1) { + var neighbor = vec3f(f32(x), f32(y), f32(z)); + var point = random3D(posInt + neighbor); + var diff = neighbor + point - posFract; + var dist = length(diff); + minDist = min(minDist, dist); + } + } + } + + return minDist; +} + +@group(0) @binding(0) var camera : CameraUniforms; + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4f +{ + // Use Y (height) as time component for animation + let scale = 3.0; + let animatedPos = vec3f( + in.pos.x * scale, + in.pos.z * scale, + in.pos.y * 2.0 // Use height as pseudo-time + ); + + // Sample Worley noise at multiple scales for detail + let noise1 = worley_noise(animatedPos); + let noise2 = worley_noise(animatedPos * 2.0 + vec3f(100.0, 100.0, 100.0)); + let noise3 = worley_noise(animatedPos * 4.0 + vec3f(200.0, 200.0, 200.0)); + + // Combine noises for layered caustic effect + let combinedNoise = noise1 * 0.5 + noise2 * 0.3 + noise3 * 0.2; + + // Create caustics by inverting and applying power + let caustic = pow(1.0 - combinedNoise, 2.5); + + // Define water colors + let deepWaterColor = vec3f(0.0, 0.15, 0.3); // Deep blue + let shallowWaterColor = vec3f(0.2, 0.5, 0.7); // Lighter cyan-blue + let causticColor = vec3f(0.6, 0.8, 1.0); // Bright highlights + + // Mix based on caustic pattern + var waterColor = mix(deepWaterColor, shallowWaterColor, caustic); + waterColor = mix(waterColor, causticColor, caustic * caustic * 0.5); + + // Add lighting + let lightDir = normalize(vec3f(-1.0, 1.0, -1.0)); + let diffuse = max(dot(in.nor, lightDir), 0.2); + var finalColor = waterColor * diffuse; + + // Add specular highlights for water sparkle + let viewDir = normalize(camera.viewDir.xyz); + let halfDir = normalize(lightDir + viewDir); + let specular = pow(max(dot(in.nor, halfDir), 0.0), 64.0); + finalColor = finalColor + vec3f(specular * 0.8); + + // Add edge foam/fresnel effect + let viewDotNormal = abs(dot(viewDir, in.nor)); + let fresnel = pow(1.0 - viewDotNormal, 3.0); + finalColor = mix(finalColor, vec3f(0.9, 0.95, 1.0), fresnel * 0.3); + + return vec4f(finalColor, 0.85); // Semi-transparent for depth feel +} \ No newline at end of file From 6e96a9fa6ff1aa9609b91f7be5861da677d1de60 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 00:06:17 -0500 Subject: [PATCH 02/15] temp fragment shader --- src/lib/renderers/terrain-renderer.ts | 49 +++++++++++++++++++++++++++ src/lib/shaders/shaders.ts | 4 ++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 3bbf318..54df4de 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -103,6 +103,7 @@ export class TerrainRenderer implements IRenderer { private waterNodeGraphUniformConfig!: scene.WaterPipeline['uniforms'] | undefined; private customWaterDisplacePipeline: GPUComputePipeline; + private waterRenderPipeline: GPURenderPipeline; /** overall render pipeline, must get recreated upon canvas resize */ private pipeline: GPURenderPipeline; @@ -178,6 +179,7 @@ export class TerrainRenderer implements IRenderer { this.depthTextureView = this.depthTexture.createView(); this.pipeline = this.createRenderPipeline(); + this.waterRenderPipeline = this.createWaterRenderPipeline(); // compute pipeline that creates the terrain this.terrainComputePipeline = new TerrainPipeline(this.device, this.stage.groundPlane); @@ -442,6 +444,50 @@ export class TerrainRenderer implements IRenderer { }); } + private createWaterRenderPipeline() { + return this.device.createRenderPipeline({ + layout: this.device.createPipelineLayout({ + label: 'water pipeline layout', + bindGroupLayouts: [this.sceneUniformsBindGroupLayout], + }), + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, + vertex: { + module: this.device.createShaderModule({ + label: 'water vert shader', + code: shaders.naiveVertSrc, + }), + buffers: [TerrainRenderer.VertexBufferLayout], + }, + fragment: { + module: this.device.createShaderModule({ + label: 'water frag shader', + code: shaders.waterFragSrc, + }), + targets: [ + { + format: this.webGPU.canvasFormat, + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + operation: 'add', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha', + operation: 'add', + }, + }, + }, + ], + }, + }); + } + configureWaterPipeline(config: scene.WaterPipeline) { this.waterPipelineConfigured = true; @@ -587,6 +633,7 @@ export class TerrainRenderer implements IRenderer { this.depthTextureView = this.depthTexture.createView(); this.pipeline = this.createRenderPipeline(); + this.waterRenderPipeline = this.createWaterRenderPipeline(); } onFrame(frameInfo: { time: number; deltaTime: number }) { @@ -623,6 +670,8 @@ export class TerrainRenderer implements IRenderer { renderPass.drawIndexedIndirect(this.stage.groundPlane.indirectBuffer!, 0); // Render water plane + renderPass.setPipeline(this.waterRenderPipeline); + renderPass.setBindGroup(0, this.sceneUniformsBindGroup); renderPass.setVertexBuffer(0, this.stage.waterPlane.vertexBuffer); renderPass.setIndexBuffer(this.stage.waterPlane.indexBuffer!, 'uint32'); renderPass.drawIndexedIndirect(this.stage.waterPlane.indirectBuffer!, 0); diff --git a/src/lib/shaders/shaders.ts b/src/lib/shaders/shaders.ts index 7a1c657..d4affd8 100644 --- a/src/lib/shaders/shaders.ts +++ b/src/lib/shaders/shaders.ts @@ -9,6 +9,7 @@ import naiveVertRaw from './naive.vs.wgsl?raw'; import normalsComputeRaw from './normals.cs.wgsl?raw'; import terrainComputeRaw from './terrain.cs.wgsl?raw'; import waterComputeRaw from './water.cs.wgsl?raw'; +import waterFragRaw from './water.fs.wgsl?raw'; // CONSTANTS (for use in shaders) (need to be hardcoded in deployed environment) @@ -35,4 +36,5 @@ export const terrainComputeSrc: string = processShaderRaw(terrainComputeRaw); export const normalsComputeSrc: string = processShaderRaw(normalsComputeRaw); export const terrainPointsComputeSrc: string = processShaderRaw(instancePointsComputeRaw); export const instanceSrc: string = processShaderRaw(instancingRaw); -export const waterComputeSrc: string = processShaderRaw(waterComputeRaw); \ No newline at end of file +export const waterComputeSrc: string = processShaderRaw(waterComputeRaw); +export const waterFragSrc: string = processShaderRaw(waterFragRaw); \ No newline at end of file From 9ce70d394c0c1a932d2ea6674dfa1ba1f038bc72 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 00:52:13 -0500 Subject: [PATCH 03/15] adding time to cam --- src/components/common/webgpu-canvas.tsx | 2 +- src/lib/scene/camera.ts | 12 +++++++++++- src/lib/shaders/common.wgsl | 4 ++++ src/lib/shaders/water.cs.wgsl | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/common/webgpu-canvas.tsx b/src/components/common/webgpu-canvas.tsx index ddeda24..3001567 100644 --- a/src/components/common/webgpu-canvas.tsx +++ b/src/components/common/webgpu-canvas.tsx @@ -122,7 +122,7 @@ export default function WebGPUCanvas({ height: divRef.current.clientHeight, }); - let lastTime = Date.now(); + let lastTime = 0; const doFrame = (time: number) => { if (!rendererRef.current) return; // TODO: probably add some kind of stats profiling stuff diff --git a/src/lib/scene/camera.ts b/src/lib/scene/camera.ts index dac5fe2..1ffeda0 100644 --- a/src/lib/scene/camera.ts +++ b/src/lib/scene/camera.ts @@ -8,7 +8,7 @@ function toRadians(degrees: number) { } class CameraUniforms { - readonly buffer = new ArrayBuffer(224); + readonly buffer = new ArrayBuffer(240); private readonly floatView = new Float32Array(this.buffer, 0, 16); private readonly invProjMatView = new Float32Array(this.buffer, 64, 16); private readonly viewMatView = new Float32Array(this.buffer, 128, 16); @@ -17,6 +17,7 @@ class CameraUniforms { private readonly cameraHeightView = new Float32Array(this.buffer, 212, 1); private readonly nearPlaneView = new Float32Array(this.buffer, 216, 1); private readonly farPlaneView = new Float32Array(this.buffer, 220, 1); + private readonly timeView = new Float32Array(this.buffer, 224, 1); set viewProjMat(mat: Float32Array) { this.floatView.set(mat.subarray(0, 16), 0); @@ -49,6 +50,10 @@ class CameraUniforms { set farPlane(far: number) { this.farPlaneView[0] = far; } + + set time(time: number) { + this.timeView[0] = time; + } } export class Camera { @@ -64,6 +69,7 @@ export class Camera { pitch: number = 0; moveSpeed: number = 0.004; sensitivity: number = 0.15; + time: number = 0; static readonly nearPlane = 0.1; static readonly farPlane = 1000; @@ -192,6 +198,8 @@ export class Camera { onFrame(deltaTime: number) { this.processInput(deltaTime); + this.time += deltaTime / 1000.0; + const lookPos = vec3.add(this.cameraPos, vec3.scale(this.cameraFront, 1)); const viewMat = mat4.lookAt(this.cameraPos, lookPos, [0, 1, 0]); const viewProjMat = mat4.mul(this.projMat, viewMat); @@ -210,6 +218,8 @@ export class Camera { // write to extra buffers needed for light clustering here this.uniforms.viewMat = viewMat; + this.uniforms.time = this.time; + // upload `this.uniforms.buffer` (host side) to `this.uniformsBuffer` (device side) this.device.queue.writeBuffer(this.uniformsBuffer, 0, this.uniforms.buffer); } diff --git a/src/lib/shaders/common.wgsl b/src/lib/shaders/common.wgsl index 4497398..2ed4a45 100644 --- a/src/lib/shaders/common.wgsl +++ b/src/lib/shaders/common.wgsl @@ -9,6 +9,10 @@ struct CameraUniforms { cameraHeight: f32, nearPlane: f32, farPlane: f32, + time: f32, + _padding1: f32, + _padding2: f32, + _padding3: f32, } struct MeshUniforms { diff --git a/src/lib/shaders/water.cs.wgsl b/src/lib/shaders/water.cs.wgsl index e830dc9..d445cab 100644 --- a/src/lib/shaders/water.cs.wgsl +++ b/src/lib/shaders/water.cs.wgsl @@ -32,7 +32,7 @@ fn main(@builtin(global_invocation_id) id: vec3) { let vOffset = vertexOffset(id.x); vertices[vOffset + 0] = x; // pos.x - vertices[vOffset + 1] = 0.0; // pos.y (height - will be set by custom pipeline) + vertices[vOffset + 1] = 0.0; // pos.y vertices[vOffset + 2] = z; // pos.z vertices[vOffset + 3] = 0.0; // nor.x vertices[vOffset + 4] = 1.0; // nor.y From 07285ca924481b684b05eded66c1977c2a3debe9 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 11:57:53 -0500 Subject: [PATCH 04/15] temp water shader --- src/lib/shaders/water.fs.wgsl | 100 +++++++++++++++++----------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/lib/shaders/water.fs.wgsl b/src/lib/shaders/water.fs.wgsl index 3092294..7404d54 100644 --- a/src/lib/shaders/water.fs.wgsl +++ b/src/lib/shaders/water.fs.wgsl @@ -13,24 +13,36 @@ fn random3D(seed: vec3f) -> vec3f { return vec3f(fract_value, fract_value, fract_value); } -fn worley_noise(pos: vec3f) -> f32 { - var posInt = floor(pos); - var posFract = fract(pos); - var minDist = 1.0; +fn worley_noise(pos: vec3f, time: f32) -> f32 { + var posInt = floor(pos); + var posFract = fract(pos); + var minDist = 1.0; - for (var z: i32 = -1; z <= 1; z = z + 1) { - for (var y: i32 = -1; y <= 1; y = y + 1) { - for (var x: i32 = -1; x <= 1; x = x + 1) { - var neighbor = vec3f(f32(x), f32(y), f32(z)); - var point = random3D(posInt + neighbor); - var diff = neighbor + point - posFract; - var dist = length(diff); - minDist = min(minDist, dist); - } - } - } + for (var z: i32 = -1; z <= 1; z = z + 1) { + for (var y: i32 = -1; y <= 1; y = y + 1) { + for (var x: i32 = -1; x <= 1; x = x + 1) { + var neighbor = vec3f(f32(x), f32(y), f32(z)); + var point = random3D(posInt + neighbor); - return minDist; + let timeSpeed = 0.8; + let offsetX = sin(time * timeSpeed + point.x * 6.28) * 0.5; + let offsetY = cos(time * timeSpeed * 0.7 + point.y * 6.28) * 0.5; + let offsetZ = sin(time * timeSpeed * 0.5 + point.z * 6.28) * 0.5; + + point = point + vec3f(offsetX, offsetY, offsetZ) * 0.3; + + var diff = neighbor + point - posFract; + var dist = length(diff); + minDist = min(minDist, dist); + } + } + } + + return minDist; +} + +fn celShade(value: f32, bands: f32) -> f32 { + return max(floor(value * bands) / bands, 0.0); } @group(0) @binding(0) var camera : CameraUniforms; @@ -38,49 +50,37 @@ fn worley_noise(pos: vec3f) -> f32 { @fragment fn main(in: FragmentInput) -> @location(0) vec4f { - // Use Y (height) as time component for animation let scale = 3.0; - let animatedPos = vec3f( + let scaledPos = vec3f( in.pos.x * scale, in.pos.z * scale, - in.pos.y * 2.0 // Use height as pseudo-time + in.pos.y * 2.0 ); - // Sample Worley noise at multiple scales for detail - let noise1 = worley_noise(animatedPos); - let noise2 = worley_noise(animatedPos * 2.0 + vec3f(100.0, 100.0, 100.0)); - let noise3 = worley_noise(animatedPos * 4.0 + vec3f(200.0, 200.0, 200.0)); - - // Combine noises for layered caustic effect - let combinedNoise = noise1 * 0.5 + noise2 * 0.3 + noise3 * 0.2; - - // Create caustics by inverting and applying power - let caustic = pow(1.0 - combinedNoise, 2.5); + let noise1 = worley_noise(scaledPos, camera.time); + let noise2 = worley_noise(scaledPos * 2.0, camera.time * 1.5); - // Define water colors - let deepWaterColor = vec3f(0.0, 0.15, 0.3); // Deep blue - let shallowWaterColor = vec3f(0.2, 0.5, 0.7); // Lighter cyan-blue - let causticColor = vec3f(0.6, 0.8, 1.0); // Bright highlights + let combinedNoise = noise1 * 0.7 + noise2 * 0.3; + + let deepOceanColor = vec3f(0.0, 0.3, 0.6); + let midOceanColor = vec3f(0.1, 0.5, 0.8); + let shallowColor = vec3f(0.3, 0.7, 0.9); + let foamColor = vec3f(0.9, 0.95, 1.0); + + let t1 = smoothstep(0.15, 0.3, combinedNoise); + let t2 = smoothstep(0.4, 0.55, combinedNoise); + let t3 = smoothstep(0.65, 0.8, combinedNoise); - // Mix based on caustic pattern - var waterColor = mix(deepWaterColor, shallowWaterColor, caustic); - waterColor = mix(waterColor, causticColor, caustic * caustic * 0.5); + var waterC = deepOceanColor; + waterC = mix(waterC, midOceanColor, t1); + waterC = mix(waterC, shallowColor, t2); + waterC = mix(waterC, foamColor, t3); - // Add lighting let lightDir = normalize(vec3f(-1.0, 1.0, -1.0)); let diffuse = max(dot(in.nor, lightDir), 0.2); - var finalColor = waterColor * diffuse; - - // Add specular highlights for water sparkle - let viewDir = normalize(camera.viewDir.xyz); - let halfDir = normalize(lightDir + viewDir); - let specular = pow(max(dot(in.nor, halfDir), 0.0), 64.0); - finalColor = finalColor + vec3f(specular * 0.8); - - // Add edge foam/fresnel effect - let viewDotNormal = abs(dot(viewDir, in.nor)); - let fresnel = pow(1.0 - viewDotNormal, 3.0); - finalColor = mix(finalColor, vec3f(0.9, 0.95, 1.0), fresnel * 0.3); + var finalColor = waterC * diffuse; + + finalColor = mix(finalColor, waterC, combinedNoise * combinedNoise * 0.5); - return vec4f(finalColor, 0.85); // Semi-transparent for depth feel + return vec4f(finalColor, 0.85); } \ No newline at end of file From 13836c465083ba0ed70242d9f2954947ea719591 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 14:44:51 -0500 Subject: [PATCH 05/15] adding note --- src/hooks/use-pipelines.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/use-pipelines.ts b/src/hooks/use-pipelines.ts index e80b176..7133d80 100644 --- a/src/hooks/use-pipelines.ts +++ b/src/hooks/use-pipelines.ts @@ -36,9 +36,8 @@ export function usePipelines({ terrainRendererRef }: UsePipelinesOptions) { const setUniform = useCallback( (key: string, value: number | [number, number, number]) => { console.log('setting uniform', key, 'to', value); - // Try terrain pipeline first + // double check if ok to set both terrainRendererRef.current?.setDisplacePipelineUniform(key, value); - // Also try water pipeline (it will log a warning if not found, which is fine) terrainRendererRef.current?.setWaterPipelineUniform(key, value); }, [terrainRendererRef], From 8f25968cb7cc288024d926cb995a333e287421c3 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 15:20:02 -0500 Subject: [PATCH 06/15] format --- src/hooks/use-pipelines.ts | 13 +-- src/lib/renderers/terrain-renderer.ts | 12 +- src/lib/renderers/water-pipeline.ts | 158 +++++++++++++------------- src/lib/shaders/shaders.ts | 2 +- 4 files changed, 90 insertions(+), 95 deletions(-) diff --git a/src/hooks/use-pipelines.ts b/src/hooks/use-pipelines.ts index 7133d80..97475e5 100644 --- a/src/hooks/use-pipelines.ts +++ b/src/hooks/use-pipelines.ts @@ -45,11 +45,8 @@ export function usePipelines({ terrainRendererRef }: UsePipelinesOptions) { const rebuildPipelinesFromNode = useCallback( (nodeId: string, options: { nodes: graph.PipelineNode[]; edges: Edge[] }) => { - const { displacePipeline, waterPipeline, instancingPipeline } = graph.generatePipelinesFromNode( - nodeId, - options.nodes, - options.edges, - ); + const { displacePipeline, waterPipeline, instancingPipeline } = + graph.generatePipelinesFromNode(nodeId, options.nodes, options.edges); if (displacePipeline) setDisplacePipeline(displacePipeline); if (waterPipeline) setWaterPipeline(waterPipeline); @@ -60,10 +57,8 @@ export function usePipelines({ terrainRendererRef }: UsePipelinesOptions) { const rebuildAllPipelines = useCallback( (options: { nodes: graph.PipelineNode[]; edges: Edge[] }) => { - const { displacePipeline, waterPipeline, instancingPipeline } = graph.generateAllPipelines( - options.nodes, - options.edges, - ); + const { displacePipeline, waterPipeline, instancingPipeline } = + graph.generateAllPipelines(options.nodes, options.edges); if (displacePipeline) setDisplacePipeline(displacePipeline); if (waterPipeline) setWaterPipeline(waterPipeline); diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 54df4de..0c5bdf5 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -16,7 +16,6 @@ import { instanceComputeShaderTemplate } from '@/lib/shaders/jit/templates/insta import * as shaders from '@/lib/shaders/shaders'; import type { WebGPUContext } from '@/lib/webgpu-context'; - export type TerrainRendererGlobalParameters = { size: number; resolution: number; @@ -39,7 +38,7 @@ export class TerrainRenderer implements IRenderer { // terrain compute pipeline private readonly terrainComputePipeline: TerrainPipeline; - // water compute pipeline + // water compute pipeline private readonly waterComputePipeline: WaterPipeline; // custom compute pipeline @@ -288,9 +287,7 @@ export class TerrainRenderer implements IRenderer { this.customWaterBindGroup = this.device.createBindGroup({ label: 'custom water compute bind group', layout: this.customWaterBindGroupLayout, - entries: [ - { binding: 0, resource: { buffer: this.stage.waterPlane.vertexBuffer! } }, - ], + entries: [{ binding: 0, resource: { buffer: this.stage.waterPlane.vertexBuffer! } }], }); this.customWaterUniformBindGroupLayout = this.device.createBindGroupLayout({ @@ -328,7 +325,10 @@ export class TerrainRenderer implements IRenderer { label: 'custom water compute pipeline', layout: this.device.createPipelineLayout({ label: 'custom water compute pipeline layout', - bindGroupLayouts: [this.customWaterBindGroupLayout, this.customWaterUniformBindGroupLayout], + bindGroupLayouts: [ + this.customWaterBindGroupLayout, + this.customWaterUniformBindGroupLayout, + ], }), compute: { module: this.device.createShaderModule({ diff --git a/src/lib/renderers/water-pipeline.ts b/src/lib/renderers/water-pipeline.ts index 099300c..6651618 100644 --- a/src/lib/renderers/water-pipeline.ts +++ b/src/lib/renderers/water-pipeline.ts @@ -2,92 +2,92 @@ import { Mesh } from '@/lib/scene/mesh'; import * as shaders from '@/lib/shaders/shaders'; export class WaterPipeline { - device: GPUDevice; - mesh: Mesh; + device: GPUDevice; + mesh: Mesh; - waterDataBindGroupLayout: GPUBindGroupLayout; - waterDataBindGroup: GPUBindGroup; + waterDataBindGroupLayout: GPUBindGroupLayout; + waterDataBindGroup: GPUBindGroup; - waterUniformBindGroupLayout: GPUBindGroupLayout; - waterUniformBindGroup: GPUBindGroup; + waterUniformBindGroupLayout: GPUBindGroupLayout; + waterUniformBindGroup: GPUBindGroup; - waterPipeline: GPUComputePipeline; + waterPipeline: GPUComputePipeline; - constructor(device: GPUDevice, mesh: Mesh) { - this.device = device; - this.mesh = mesh; + constructor(device: GPUDevice, mesh: Mesh) { + this.device = device; + this.mesh = mesh; - this.waterDataBindGroupLayout = this.device.createBindGroupLayout({ - label: 'water compute bind group layout', - entries: [ - { - // vertices - binding: 0, - visibility: GPUShaderStage.COMPUTE, - buffer: { - type: 'storage', - }, - }, - { - // indices - binding: 1, - visibility: GPUShaderStage.COMPUTE, - buffer: { - type: 'storage', - }, - }, - ], - }); + this.waterDataBindGroupLayout = this.device.createBindGroupLayout({ + label: 'water compute bind group layout', + entries: [ + { + // vertices + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + { + // indices + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + }); - this.waterDataBindGroup = this.device.createBindGroup({ - label: 'water compute bind group', - layout: this.waterDataBindGroupLayout, - entries: [ - { binding: 0, resource: { buffer: this.mesh.vertexBuffer! } }, - { binding: 1, resource: { buffer: this.mesh.indexBuffer! } }, - ], - }); + this.waterDataBindGroup = this.device.createBindGroup({ + label: 'water compute bind group', + layout: this.waterDataBindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this.mesh.vertexBuffer! } }, + { binding: 1, resource: { buffer: this.mesh.indexBuffer! } }, + ], + }); - this.waterUniformBindGroupLayout = this.device.createBindGroupLayout({ - label: 'water compute uniform bind group layout', - entries: [ - { - // uniform containing mesh size and resolution - binding: 0, - visibility: GPUShaderStage.COMPUTE, - buffer: { - type: 'uniform', - }, - }, - ], - }); + this.waterUniformBindGroupLayout = this.device.createBindGroupLayout({ + label: 'water compute uniform bind group layout', + entries: [ + { + // uniform containing mesh size and resolution + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + ], + }); - this.waterUniformBindGroup = this.device.createBindGroup({ - label: 'water compute uniform bind group', - layout: this.waterUniformBindGroupLayout, - entries: [{ binding: 0, resource: { buffer: this.mesh.uniformsBuffer! } }], - }); + this.waterUniformBindGroup = this.device.createBindGroup({ + label: 'water compute uniform bind group', + layout: this.waterUniformBindGroupLayout, + entries: [{ binding: 0, resource: { buffer: this.mesh.uniformsBuffer! } }], + }); - this.waterPipeline = this.device.createComputePipeline({ - label: 'water compute pipeline', - layout: this.device.createPipelineLayout({ - label: 'water compute pipeline layout', - bindGroupLayouts: [this.waterDataBindGroupLayout, this.waterUniformBindGroupLayout], - }), - compute: { - module: this.device.createShaderModule({ - label: 'water compute shader', - code: shaders.waterComputeSrc, - }), - entryPoint: 'main', - }, - }); - } + this.waterPipeline = this.device.createComputePipeline({ + label: 'water compute pipeline', + layout: this.device.createPipelineLayout({ + label: 'water compute pipeline layout', + bindGroupLayouts: [this.waterDataBindGroupLayout, this.waterUniformBindGroupLayout], + }), + compute: { + module: this.device.createShaderModule({ + label: 'water compute shader', + code: shaders.waterComputeSrc, + }), + entryPoint: 'main', + }, + }); + } - runComputePass(computePass: GPUComputePassEncoder) { - computePass.setPipeline(this.waterPipeline); - computePass.setBindGroup(0, this.waterDataBindGroup); - computePass.setBindGroup(1, this.waterUniformBindGroup); - computePass.dispatchWorkgroups(Math.ceil(this.mesh.numVertices / 64)); - } -} \ No newline at end of file + runComputePass(computePass: GPUComputePassEncoder) { + computePass.setPipeline(this.waterPipeline); + computePass.setBindGroup(0, this.waterDataBindGroup); + computePass.setBindGroup(1, this.waterUniformBindGroup); + computePass.dispatchWorkgroups(Math.ceil(this.mesh.numVertices / 64)); + } +} diff --git a/src/lib/shaders/shaders.ts b/src/lib/shaders/shaders.ts index d4affd8..9468774 100644 --- a/src/lib/shaders/shaders.ts +++ b/src/lib/shaders/shaders.ts @@ -37,4 +37,4 @@ export const normalsComputeSrc: string = processShaderRaw(normalsComputeRaw); export const terrainPointsComputeSrc: string = processShaderRaw(instancePointsComputeRaw); export const instanceSrc: string = processShaderRaw(instancingRaw); export const waterComputeSrc: string = processShaderRaw(waterComputeRaw); -export const waterFragSrc: string = processShaderRaw(waterFragRaw); \ No newline at end of file +export const waterFragSrc: string = processShaderRaw(waterFragRaw); From a8e329d737862ba6caf405e9c0b0165cc9694bfe Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 21:49:08 -0500 Subject: [PATCH 07/15] simple colors working for terrain --- src/components/nodes/terrain-node.tsx | 5 +++ src/lib/graph/index.ts | 15 +++++++ src/lib/graph/node-types.ts | 2 +- src/lib/renderers/terrain-renderer.ts | 64 ++++++++++++++++++++++++++- src/lib/shaders/jit/types/shaders.ts | 1 + src/lib/shaders/naive.fs.wgsl | 29 +++++++++--- 6 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/components/nodes/terrain-node.tsx b/src/components/nodes/terrain-node.tsx index 4703291..1477a43 100644 --- a/src/components/nodes/terrain-node.tsx +++ b/src/components/nodes/terrain-node.tsx @@ -9,6 +9,11 @@ function TerrainNode({ ...props }: NodeProps) { return ( + ); } diff --git a/src/lib/graph/index.ts b/src/lib/graph/index.ts index e4a88e4..596c106 100644 --- a/src/lib/graph/index.ts +++ b/src/lib/graph/index.ts @@ -76,12 +76,26 @@ function generatePipelines( const heightEdgeSourceNode = orderedDependencyNodes.find( (node) => node.id === heightEdge?.source, ); + + const waterHeightEdge = edges.find( + (edge) => edge.target === terrainNode.id && edge.targetHandle === nodeTypes.HANDLES.terrain.in.waterHeight + ); + const waterHeightSourceNode = waterHeightEdge + ? orderedDependencyNodes.find((node) => node.id === waterHeightEdge.source) + : undefined; + const outputs: scene.DisplacePipeline['outputs'] = { height: nodeMapping.getHandleKey({ // TODO: wow these type assertions are awesome (evil as fuck) sourceNode: heightEdgeSourceNode!, outgoingHandleId: heightEdge!.sourceHandle!, }), + waterHeight: waterHeightSourceNode && waterHeightEdge + ? nodeMapping.getHandleKey({ + sourceNode: waterHeightSourceNode, + outgoingHandleId: waterHeightEdge.sourceHandle!, + }) + : undefined, }; displacePipeline = { instructionSet, uniforms, outputs }; @@ -111,6 +125,7 @@ function generatePipelines( sourceNode: heightEdgeSourceNode!, outgoingHandleId: heightEdge!.sourceHandle!, }), + waterHeight: undefined, }; waterPipeline = { instructionSet, uniforms, outputs }; diff --git a/src/lib/graph/node-types.ts b/src/lib/graph/node-types.ts index 3e03eba..d080aa0 100644 --- a/src/lib/graph/node-types.ts +++ b/src/lib/graph/node-types.ts @@ -112,7 +112,7 @@ export const HANDLES = { out: { position: 'vec3-pos-out' }, }, terrain: { - in: { height: 'float-trans-in' }, + in: { height: 'float-trans-in', waterHeight: 'float-waterHeight-in' }, out: {}, }, separate: { diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 0c5bdf5..af08b08 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -81,6 +81,11 @@ export class TerrainRenderer implements IRenderer { private nodeGraphUniformLayout!: Map | undefined; private nodeGraphUniformConfig!: scene.DisplacePipeline['uniforms'] | undefined; + private waterHeightUniformBuffer: GPUBuffer; + private waterHeightBindGroupLayout: GPUBindGroupLayout; + private waterHeightBindGroup: GPUBindGroup; + private currentWaterHeight: number = 0.0; + /** Custom displace pipeline, gets reconfigured whenever node structure is changed */ private customDisplacePipeline: GPUComputePipeline; @@ -170,6 +175,41 @@ export class TerrainRenderer implements IRenderer { ], }); + this.waterHeightUniformBuffer = this.device.createBuffer({ + label: 'water height uniform', + size: 16, // f32 with padding + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + // Initialize with 0 + this.device.queue.writeBuffer( + this.waterHeightUniformBuffer, + 0, + new Float32Array([0.0]) + ); + + this.waterHeightBindGroupLayout = this.device.createBindGroupLayout({ + label: 'water height bind group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' }, + }, + ], + }); + + this.waterHeightBindGroup = this.device.createBindGroup({ + label: 'water height bind group', + layout: this.waterHeightBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.waterHeightUniformBuffer }, + }, + ], + }); + // initialize depth texture and depth texture view this.depthTexture = this.createDepthTexture({ width: this.webGPU.canvas.width * window.devicePixelRatio, @@ -416,7 +456,7 @@ export class TerrainRenderer implements IRenderer { return this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ label: 'naive pipeline layout', - bindGroupLayouts: [this.sceneUniformsBindGroupLayout], + bindGroupLayouts: [this.sceneUniformsBindGroupLayout, this.waterHeightBindGroupLayout], }), depthStencil: { depthWriteEnabled: true, @@ -665,6 +705,7 @@ export class TerrainRenderer implements IRenderer { renderPass.setPipeline(this.pipeline); renderPass.setBindGroup(0, this.sceneUniformsBindGroup); + renderPass.setBindGroup(1, this.waterHeightBindGroup); renderPass.setVertexBuffer(0, this.stage.groundPlane.vertexBuffer); renderPass.setIndexBuffer(this.stage.groundPlane.indexBuffer!, 'uint32'); renderPass.drawIndexedIndirect(this.stage.groundPlane.indirectBuffer!, 0); @@ -710,6 +751,17 @@ export class TerrainRenderer implements IRenderer { // also TODO: reuse code between this and our constructor + if (config.outputs.waterHeight) { + console.log('Water height output key:', config.outputs.waterHeight); + + const waterHeightKey = config.outputs.waterHeight.replace('nodeGraphUniforms.', ''); + const waterHeightUniform = config.uniforms.find(u => u.key === waterHeightKey); + + if (waterHeightUniform && waterHeightUniform.type === 'f32' && waterHeightUniform.initialValue !== null) { + this.setWaterHeightForTerrain(waterHeightUniform.initialValue); + } + } + const customComputeShader = jit.generateDisplaceShaderCode( config, displaceComputeShaderTemplate, @@ -963,4 +1015,14 @@ export class TerrainRenderer implements IRenderer { this.device.queue.submit([encoder.finish()]); console.log('rerunning compute after uniform update'); } + + setWaterHeightForTerrain(height: number) { + this.currentWaterHeight = height; + this.device.queue.writeBuffer( + this.waterHeightUniformBuffer, + 0, + new Float32Array([height]) + ); + console.log('Updated water height for terrain fragment shader:', height); + } } diff --git a/src/lib/shaders/jit/types/shaders.ts b/src/lib/shaders/jit/types/shaders.ts index 588f68d..4b847bb 100644 --- a/src/lib/shaders/jit/types/shaders.ts +++ b/src/lib/shaders/jit/types/shaders.ts @@ -13,6 +13,7 @@ type ShaderConfigBase = { export type DisplaceShaderConfig = ShaderConfigBase & { outputs: { height: util.ReferenceKey; + waterHeight?: util.ReferenceKey; }; }; diff --git a/src/lib/shaders/naive.fs.wgsl b/src/lib/shaders/naive.fs.wgsl index 9a03686..1ef40e9 100644 --- a/src/lib/shaders/naive.fs.wgsl +++ b/src/lib/shaders/naive.fs.wgsl @@ -1,5 +1,3 @@ -// default fragment shader - struct FragmentInput { @location(0) pos: vec3f, @@ -7,13 +5,34 @@ struct FragmentInput @location(2) uv: vec2f } +struct WaterHeightUniforms { + height: f32, +} + +@group(0) @binding(0) var camera : CameraUniforms; +@group(1) @binding(0) var waterHeight : WaterHeightUniforms; + @fragment fn main(in: FragmentInput) -> @location(0) vec4f { - // do lambertian shading + let grass = vec3f(0.32, 0.41, 0.06); + let sand = vec3f(0.95, 0.88, 0.71); + let underwater = vec3f(0.2, 0.3, 0.4); + + var baseColor = grass; + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } else if (waterHeight.height <= in.pos.y && waterHeight.height > in.pos.y - 0.2f) { + let t = (in.pos.y - waterHeight.height) / 0.2; + baseColor = mix(sand, grass, t); + } + else { + baseColor = grass; + } + let lightDir = normalize(vec3f(-1.0, 1.0, -1.0)); let diffuse = max(dot(in.nor, lightDir), 0.0); - let color = vec3f(0.5, 0.5, 0.5) * diffuse; + let color = baseColor * diffuse; return vec4f(color, 1.0); -} +} \ No newline at end of file From 501a8118f09b19647975446f9ee9ed710cb35bf1 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sat, 6 Dec 2025 23:24:50 -0500 Subject: [PATCH 08/15] sand and blend for grass --- src/lib/shaders/naive.fs.wgsl | 88 +++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/src/lib/shaders/naive.fs.wgsl b/src/lib/shaders/naive.fs.wgsl index 1ef40e9..1310f77 100644 --- a/src/lib/shaders/naive.fs.wgsl +++ b/src/lib/shaders/naive.fs.wgsl @@ -12,27 +12,75 @@ struct WaterHeightUniforms { @group(0) @binding(0) var camera : CameraUniforms; @group(1) @binding(0) var waterHeight : WaterHeightUniforms; +// Simple hash function for noise +fn hash(p: vec2f) -> f32 { + let p3 = fract(vec3f(p.x, p.y, p.x) * 0.13); + let dot_product = dot(p3, vec3f(p3.y, p3.z, p3.x) + 3.333); + return fract((p3.x + p3.y) * dot_product); +} + +// thanks copilot +fn noise(p: vec2f) -> f32 { + let i = floor(p); + let f = fract(p); + + // Cubic interpolation + let u = f * f * (3.0 - 2.0 * f); + + // Sample corners + let a = hash(i); + let b = hash(i + vec2f(1.0, 0.0)); + let c = hash(i + vec2f(0.0, 1.0)); + let d = hash(i + vec2f(1.0, 1.0)); + + // Interpolate + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +fn fbm(p: vec2f) -> f32 { + var value = 0.0; + var amplitude = 0.5; + var frequency = 1.0; + var pos = p; + + for (var i = 0; i < 4; i++) { + value += amplitude * noise(pos * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return value; +} + @fragment fn main(in: FragmentInput) -> @location(0) vec4f { - let grass = vec3f(0.32, 0.41, 0.06); - let sand = vec3f(0.95, 0.88, 0.71); - let underwater = vec3f(0.2, 0.3, 0.4); - - var baseColor = grass; - if (waterHeight.height > in.pos.y) { - baseColor = underwater; - } else if (waterHeight.height <= in.pos.y && waterHeight.height > in.pos.y - 0.2f) { - let t = (in.pos.y - waterHeight.height) / 0.2; - baseColor = mix(sand, grass, t); - } - else { - baseColor = grass; - } - - let lightDir = normalize(vec3f(-1.0, 1.0, -1.0)); - let diffuse = max(dot(in.nor, lightDir), 0.0); - let color = baseColor * diffuse; - - return vec4f(color, 1.0); + let sandScale = 20.0; // Controls grain size + let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); + + // Base colors with slight variation + let grass = vec3f(0.32, 0.41, 0.06); + let sandBase = vec3f(0.95, 0.88, 0.71); + let sandDark = vec3f(0.85, 0.78, 0.61); + let underwater = vec3f(0.2, 0.3, 0.4); + + let sand = mix(sandDark, sandBase, sandNoise); + + var baseColor = grass; + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } else if (in.pos.y <= waterHeight.height + 0.2) { + baseColor = sand; + } else if (in.pos.y <= waterHeight.height + 0.5) { + let t = (in.pos.y - (waterHeight.height + 0.2)) / ((waterHeight.height + 0.5) - (waterHeight.height + 0.2)); + baseColor = mix(sand, grass, t); + } else { + baseColor = grass; + } + + let lightDir = normalize(vec3f(-1.0, 1.0, -1.0)); + let diffuse = max(dot(in.nor, lightDir), 0.0); + let color = baseColor * diffuse; + + return vec4f(color, 1.0); } \ No newline at end of file From 03eea67df78a091d8d2d3d44252348777a4461a5 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 00:04:44 -0500 Subject: [PATCH 09/15] sand & grass shaders --- src/lib/shaders/naive.fs.wgsl | 55 ++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/lib/shaders/naive.fs.wgsl b/src/lib/shaders/naive.fs.wgsl index 1310f77..04ea007 100644 --- a/src/lib/shaders/naive.fs.wgsl +++ b/src/lib/shaders/naive.fs.wgsl @@ -52,27 +52,60 @@ fn fbm(p: vec2f) -> f32 { return value; } +fn celShade(value: f32, bands: f32) -> f32 { + return floor(value * bands) / bands; +} + +fn grassTexture(pos: vec3f) -> vec3f { + let noise1 = fbm(pos.xz * 8.0); + let noise2 = noise(pos.xz * 2.0); + + let combinedNoise = noise1 * 0.6 + noise2 * 0.4; + + let grassBright = vec3f(0.45, 0.85, 0.15); + let grassMid = vec3f(0.25, 0.65, 0.10); + let grassDark = vec3f(0.15, 0.45, 0.05); + + let t1 = smoothstep(0.3, 0.5, combinedNoise); + let t2 = smoothstep(0.4, 0.8, combinedNoise); + + var grassColor = grassDark; + grassColor = mix(grassColor, grassMid, t1); + grassColor = mix(grassColor, grassBright, t2); + + return grassColor; +} + +fn sandTexture(pos: vec3f) -> vec3f { + let sandScale = 20.0; + let sandNoise = fbm(pos.xz * sandScale); + + let sandBase = vec3f(0.95, 0.88, 0.71); + let sandDark = vec3f(0.85, 0.78, 0.61); + + return mix(sandDark, sandBase, sandNoise); +} + @fragment fn main(in: FragmentInput) -> @location(0) vec4f { - let sandScale = 20.0; // Controls grain size + let sandScale = 20.0; let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); - - // Base colors with slight variation - let grass = vec3f(0.32, 0.41, 0.06); - let sandBase = vec3f(0.95, 0.88, 0.71); - let sandDark = vec3f(0.85, 0.78, 0.61); + + let grass = grassTexture(in.pos); + let sand = sandTexture(in.pos); let underwater = vec3f(0.2, 0.3, 0.4); - - let sand = mix(sandDark, sandBase, sandNoise); + let grassStart = waterHeight.height + 0.8; + let sandEnd = waterHeight.height + 0.4; + var baseColor = grass; if (waterHeight.height > in.pos.y) { baseColor = underwater; - } else if (in.pos.y <= waterHeight.height + 0.2) { + } else if (in.pos.y <= sandEnd) { baseColor = sand; - } else if (in.pos.y <= waterHeight.height + 0.5) { - let t = (in.pos.y - (waterHeight.height + 0.2)) / ((waterHeight.height + 0.5) - (waterHeight.height + 0.2)); + } else if (in.pos.y <= grassStart) { + let t = (in.pos.y - sandEnd) / (grassStart - sandEnd); baseColor = mix(sand, grass, t); } else { baseColor = grass; From c3856930a2a069905ced2e063852089e8c967e30 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 20:10:49 -0500 Subject: [PATCH 10/15] format --- src/lib/graph/index.ts | 23 +++++++++++++---------- src/lib/renderers/terrain-renderer.ts | 20 ++++++++------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/lib/graph/index.ts b/src/lib/graph/index.ts index e880214..175b353 100644 --- a/src/lib/graph/index.ts +++ b/src/lib/graph/index.ts @@ -78,7 +78,9 @@ function generatePipelines( ); const waterHeightEdge = edges.find( - (edge) => edge.target === terrainNode.id && edge.targetHandle === nodeTypes.HANDLES.terrain.in.waterHeight + (edge) => + edge.target === terrainNode.id && + edge.targetHandle === nodeTypes.HANDLES.terrain.in.waterHeight, ); const waterHeightSourceNode = waterHeightEdge ? orderedDependencyNodes.find((node) => node.id === waterHeightEdge.source) @@ -90,12 +92,13 @@ function generatePipelines( sourceNode: heightEdgeSourceNode!, outgoingHandleId: heightEdge!.sourceHandle!, }), - waterHeight: waterHeightSourceNode && waterHeightEdge - ? nodeMapping.getHandleKey({ - sourceNode: waterHeightSourceNode, - outgoingHandleId: waterHeightEdge.sourceHandle!, - }) - : undefined, + waterHeight: + waterHeightSourceNode && waterHeightEdge + ? nodeMapping.getHandleKey({ + sourceNode: waterHeightSourceNode, + outgoingHandleId: waterHeightEdge.sourceHandle!, + }) + : undefined, }; displacePipeline = { instructionSet, uniforms, outputs }; @@ -257,9 +260,9 @@ function generatePipelines( maskKey: maskSourceNode && maskEdge ? nodeMapping.getHandleKey({ - sourceNode: maskSourceNode, - outgoingHandleId: maskEdge.sourceHandle!, - }) + sourceNode: maskSourceNode, + outgoingHandleId: maskEdge.sourceHandle!, + }) : undefined, threshold: scatterNode.data.threshold, transform: transformConfig, diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 8677b44..d6d1871 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -197,11 +197,7 @@ export class TerrainRenderer implements IRenderer { }); // Initialize with 0 - this.device.queue.writeBuffer( - this.waterHeightUniformBuffer, - 0, - new Float32Array([0.0]) - ); + this.device.queue.writeBuffer(this.waterHeightUniformBuffer, 0, new Float32Array([0.0])); this.waterHeightBindGroupLayout = this.device.createBindGroupLayout({ label: 'water height bind group layout', @@ -854,9 +850,13 @@ export class TerrainRenderer implements IRenderer { console.log('Water height output key:', config.outputs.waterHeight); const waterHeightKey = config.outputs.waterHeight.replace('nodeGraphUniforms.', ''); - const waterHeightUniform = config.uniforms.find(u => u.key === waterHeightKey); + const waterHeightUniform = config.uniforms.find((u) => u.key === waterHeightKey); - if (waterHeightUniform && waterHeightUniform.type === 'f32' && waterHeightUniform.initialValue !== null) { + if ( + waterHeightUniform && + waterHeightUniform.type === 'f32' && + waterHeightUniform.initialValue !== null + ) { this.setWaterHeightForTerrain(waterHeightUniform.initialValue); } } @@ -1152,11 +1152,7 @@ export class TerrainRenderer implements IRenderer { setWaterHeightForTerrain(height: number) { this.currentWaterHeight = height; - this.device.queue.writeBuffer( - this.waterHeightUniformBuffer, - 0, - new Float32Array([height]) - ); + this.device.queue.writeBuffer(this.waterHeightUniformBuffer, 0, new Float32Array([height])); console.log('Updated water height for terrain fragment shader:', height); } } From 112a1cc12edafc53ca95dcab896adcf37b5d4aa3 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 21:05:53 -0500 Subject: [PATCH 11/15] fix to increase water --- src/lib/renderers/terrain-renderer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index d6d1871..7f68d7b 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -1109,6 +1109,7 @@ export class TerrainRenderer implements IRenderer { setMeshUniforms(size: number, resolution: number) { this.stage.groundPlane.updateUniforms(this.device, size, resolution); + this.stage.waterPlane.updateUniforms(this.device, size, resolution); const encoder = this.device.createCommandEncoder(); this.runComputes(encoder); From 415349037f90d79d15e31221ef61aa82dc3d7c5e Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 22:11:15 -0500 Subject: [PATCH 12/15] base color set based on biometype --- src/components/editor/index.tsx | 2 +- src/components/nodes/terrain-node.tsx | 31 +++++++++++- src/lib/graph/index.ts | 67 +++++++++++++++---------- src/lib/graph/node-types.ts | 15 ++++-- src/lib/renderers/terrain-renderer.ts | 47 +++++++++++++++++- src/lib/shaders/jit/types/shaders.ts | 1 + src/lib/shaders/naive.fs.wgsl | 71 ++++++++++++++++++++------- 7 files changed, 184 insertions(+), 50 deletions(-) diff --git a/src/components/editor/index.tsx b/src/components/editor/index.tsx index ad57e57..30ef6ac 100644 --- a/src/components/editor/index.tsx +++ b/src/components/editor/index.tsx @@ -25,7 +25,7 @@ const initialNodes: (Node & nodeTypes.All)[] = [ id: 'terrain-out', position: { x: 250, y: 0 }, type: 'terrain', - data: {}, + data: { biome: 'Grassland' }, }, ]; diff --git a/src/components/nodes/terrain-node.tsx b/src/components/nodes/terrain-node.tsx index 1477a43..37de0b6 100644 --- a/src/components/nodes/terrain-node.tsx +++ b/src/components/nodes/terrain-node.tsx @@ -1,11 +1,27 @@ -import type { NodeProps } from 'reactflow'; +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'; +type TerrainNodeData = nodeTypes.Terrain['data']; const HANDLES = nodeTypes.HANDLES.terrain; -function TerrainNode({ ...props }: NodeProps) { +function TerrainNode({ id, data, ...props }: NodeProps) { + const { setNodes } = useReactFlow(); + const { triggerNodePipelineUpdate } = useGraphGlobals(); + + const onBiomeChange = (biome: TerrainNodeData['biome']) => { + helpers.updateNodeData({ id, setNodes, newData: { biome } }); + }; + + useEffect(() => { + triggerNodePipelineUpdate(id); + }, [data.biome, id, triggerNodePipelineUpdate]); + return ( @@ -14,6 +30,17 @@ function TerrainNode({ ...props }: NodeProps) { handleId={HANDLES.in.waterHeight} valueType="f32" /> + ); } diff --git a/src/lib/graph/index.ts b/src/lib/graph/index.ts index 175b353..aaf0abf 100644 --- a/src/lib/graph/index.ts +++ b/src/lib/graph/index.ts @@ -77,31 +77,48 @@ function generatePipelines( (node) => node.id === heightEdge?.source, ); - const waterHeightEdge = edges.find( - (edge) => - edge.target === terrainNode.id && - edge.targetHandle === nodeTypes.HANDLES.terrain.in.waterHeight, - ); - const waterHeightSourceNode = waterHeightEdge - ? orderedDependencyNodes.find((node) => node.id === waterHeightEdge.source) - : undefined; - - const outputs: scene.DisplacePipeline['outputs'] = { - height: nodeMapping.getHandleKey({ - // TODO: wow these type assertions are awesome (evil as fuck) - sourceNode: heightEdgeSourceNode!, - outgoingHandleId: heightEdge!.sourceHandle!, - }), - waterHeight: - waterHeightSourceNode && waterHeightEdge - ? nodeMapping.getHandleKey({ + // Add this check - if no height edge, skip building the pipeline + if (!heightEdge || !heightEdgeSourceNode) { + console.warn('Terrain node requires a height input connection'); + displacePipeline = undefined; + } else { + const waterHeightEdge = edges.find( + (edge) => + edge.target === terrainNode.id && + edge.targetHandle === nodeTypes.HANDLES.terrain.in.waterHeight, + ); + const waterHeightSourceNode = waterHeightEdge + ? orderedDependencyNodes.find((node) => node.id === waterHeightEdge.source) + : undefined; + + const outputs: scene.DisplacePipeline['outputs'] = { + height: nodeMapping.getHandleKey({ + sourceNode: heightEdgeSourceNode, + outgoingHandleId: heightEdge.sourceHandle!, + }), + + waterHeight: + waterHeightSourceNode && waterHeightEdge + ? nodeMapping.getHandleKey({ sourceNode: waterHeightSourceNode, outgoingHandleId: waterHeightEdge.sourceHandle!, }) - : undefined, - }; - - displacePipeline = { instructionSet, uniforms, outputs }; + : undefined, + + biome: (() => { + const biome = (terrainNode as nodeTypes.Terrain & { id: string }).data.biome; + const biomeMap: Record = { + Grassland: 0, + Desert: 1, + Mountain: 2, + Tundra: 3, + }; + return biomeMap[biome] ?? 0; + })(), + }; + + displacePipeline = { instructionSet, uniforms, outputs }; + } } // find water pipeline @@ -260,9 +277,9 @@ function generatePipelines( maskKey: maskSourceNode && maskEdge ? nodeMapping.getHandleKey({ - sourceNode: maskSourceNode, - outgoingHandleId: maskEdge.sourceHandle!, - }) + sourceNode: maskSourceNode, + outgoingHandleId: maskEdge.sourceHandle!, + }) : undefined, threshold: scatterNode.data.threshold, transform: transformConfig, diff --git a/src/lib/graph/node-types.ts b/src/lib/graph/node-types.ts index 08327b5..f9b6713 100644 --- a/src/lib/graph/node-types.ts +++ b/src/lib/graph/node-types.ts @@ -21,7 +21,10 @@ export type Mix = Node<'mix', { nodeType: 'Vec3' | 'Float' }>; export type SmoothstepFloat = Node<'smoothstepFloat'>; export type SmoothstepVec3 = Node<'smoothstepVec3'>; export type VertexData = Node<'vertexData'>; -export type Terrain = Node<'terrain'>; +export type Terrain = Node< + 'terrain', + { biome: 'Grassland' | 'Desert' | 'Mountain' | 'Tundra' } +>; export type Separate = Node<'separate'>; export type Combine = Node<'combine'>; export type Float = Node<'float', { value: number }>; @@ -114,7 +117,10 @@ export const HANDLES = { out: { position: 'vec3-pos-out' }, }, terrain: { - in: { height: 'float-trans-in', waterHeight: 'float-waterHeight-in' }, + in: { + height: 'float-trans-in', + waterHeight: 'float-waterHeight-in', + }, out: {}, }, separate: { @@ -174,7 +180,10 @@ export const NODE_PREFABS: { [nodeType in All['type']]: All & { type: nodeType } }, mix: { type: 'mix', data: { nodeType: 'Float' } }, vertexData: { type: 'vertexData', data: {} }, - terrain: { type: 'terrain', data: {} }, + terrain: { + type: 'terrain', + data: { biome: 'Grassland' }, + }, vector: { type: 'vector', data: { x: 0, y: 0, z: 0 } }, mathFloat: { type: 'mathFloat', diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 7f68d7b..e55bf1c 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -77,6 +77,10 @@ export class TerrainRenderer implements IRenderer { private customNodeGraphUniformsBindGroupLayout: GPUBindGroupLayout; private customNodeGraphUniformsBindGroup: GPUBindGroup; + private biomeUniformBuffer: GPUBuffer; + private biomeBindGroupLayout: GPUBindGroupLayout; + private biomeBindGroup: GPUBindGroup; + // custom uniform buffers private nodeGraphUniformBuffer!: GPUBuffer; private nodeGraphUniformLayout!: Map | undefined; @@ -190,6 +194,37 @@ export class TerrainRenderer implements IRenderer { ], }); + this.biomeUniformBuffer = this.device.createBuffer({ + label: 'biome uniform', + size: 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const defaultBiome = new Uint32Array([0]); + this.device.queue.writeBuffer(this.biomeUniformBuffer, 0, defaultBiome); + + this.biomeBindGroupLayout = this.device.createBindGroupLayout({ + label: 'biome bind group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' }, + }, + ], + }); + + this.biomeBindGroup = this.device.createBindGroup({ + label: 'biome bind group', + layout: this.biomeBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.biomeUniformBuffer }, + }, + ], + }); + this.waterHeightUniformBuffer = this.device.createBuffer({ label: 'water height uniform', size: 16, // f32 with padding @@ -546,7 +581,11 @@ export class TerrainRenderer implements IRenderer { return this.device.createRenderPipeline({ layout: this.device.createPipelineLayout({ label: 'naive pipeline layout', - bindGroupLayouts: [this.sceneUniformsBindGroupLayout, this.waterHeightBindGroupLayout], + bindGroupLayouts: [ + this.sceneUniformsBindGroupLayout, + this.waterHeightBindGroupLayout, + this.biomeBindGroupLayout, + ], }), depthStencil: { depthWriteEnabled: true, @@ -801,6 +840,7 @@ export class TerrainRenderer implements IRenderer { renderPass.setPipeline(this.pipeline); renderPass.setBindGroup(0, this.sceneUniformsBindGroup); renderPass.setBindGroup(1, this.waterHeightBindGroup); + renderPass.setBindGroup(2, this.biomeBindGroup); renderPass.setVertexBuffer(0, this.stage.groundPlane.vertexBuffer); renderPass.setIndexBuffer(this.stage.groundPlane.indexBuffer!, 'uint32'); renderPass.drawIndexedIndirect(this.stage.groundPlane.indirectBuffer!, 0); @@ -861,6 +901,11 @@ export class TerrainRenderer implements IRenderer { } } + if (config.outputs.biome !== undefined) { + const biomeData = new Uint32Array([config.outputs.biome]); + this.device.queue.writeBuffer(this.biomeUniformBuffer, 0, biomeData); + } + const customComputeShader = jit.generateDisplaceShaderCode( config, displaceComputeShaderTemplate, diff --git a/src/lib/shaders/jit/types/shaders.ts b/src/lib/shaders/jit/types/shaders.ts index c400f3d..1cbccbd 100644 --- a/src/lib/shaders/jit/types/shaders.ts +++ b/src/lib/shaders/jit/types/shaders.ts @@ -14,6 +14,7 @@ export type DisplaceShaderConfig = ShaderConfigBase & { outputs: { height: util.ReferenceKey; waterHeight?: util.ReferenceKey; + biome?: number; }; }; diff --git a/src/lib/shaders/naive.fs.wgsl b/src/lib/shaders/naive.fs.wgsl index d8dc8b7..b2d7b9b 100644 --- a/src/lib/shaders/naive.fs.wgsl +++ b/src/lib/shaders/naive.fs.wgsl @@ -10,12 +10,17 @@ struct FragmentInput @location(3) shadow_pos: vec3f, } +struct BiomeUniforms { + biomeType: u32, +} + struct WaterHeightUniforms { height: f32, } @group(0) @binding(0) var camera : CameraUniforms; @group(1) @binding(0) var waterHeight : WaterHeightUniforms; +@group(2) @binding(0) var biome : BiomeUniforms; // Simple hash function for noise fn hash(p: vec2f) -> f32 { @@ -97,28 +102,58 @@ fn sandTexture(pos: vec3f) -> vec3f { @fragment fn main(in: FragmentInput) -> @location(0) vec4f { - let sandScale = 20.0; - let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); + let biomeType = biome.biomeType; + var baseColor: vec3f; - let grass = grassTexture(in.pos); - let sand = sandTexture(in.pos); - let underwater = vec3f(0.2, 0.3, 0.4); + if (biomeType == 0) { + // grassland + let sandScale = 20.0; + let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); - let grassStart = waterHeight.height + 0.8; - let sandEnd = waterHeight.height + 0.4; - - var baseColor = grass; - if (waterHeight.height > in.pos.y) { - baseColor = underwater; - } else if (in.pos.y <= sandEnd) { - baseColor = sand; - } else if (in.pos.y <= grassStart) { - let t = (in.pos.y - sandEnd) / (grassStart - sandEnd); - baseColor = mix(sand, grass, t); - } else { + let grass = grassTexture(in.pos); + let sand = sandTexture(in.pos); + let underwater = vec3f(0.2, 0.3, 0.4); + + let grassStart = waterHeight.height + 0.8; + let sandEnd = waterHeight.height + 0.4; + baseColor = grass; + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } else if (in.pos.y <= sandEnd) { + baseColor = sand; + } else if (in.pos.y <= grassStart) { + let t = (in.pos.y - sandEnd) / (grassStart - sandEnd); + baseColor = mix(sand, grass, t); + } else { + baseColor = grass; + } + } else if (biomeType == 1) { + // desert + let sandScale = 20.0; + let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); + + let sand = sandTexture(in.pos); + let underwater = vec3f(0.2, 0.3, 0.4); + + baseColor = sand; + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } else { + baseColor = sand; + } + } else if (biomeType == 2) { + // mountain + baseColor = vec3f(0.2, 0.3, 0.4); + } else if (biomeType == 3) { + // tundra + baseColor = vec3f(1.0, 1.0, 1.0); } - + else { + // Default to grass texture + baseColor = grassTexture(in.pos); + } + let shadowSample = textureSample(shadow_map, shadow_sampler, in.shadow_pos.xy); let isShadowed = shadowSample < in.shadow_pos.z - shadowBias; From e9aecd5f7117fdfe9cb05b4dd740e6a3ad0aca32 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 22:28:02 -0500 Subject: [PATCH 13/15] texturesss --- src/lib/shaders/naive.fs.wgsl | 59 +++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/lib/shaders/naive.fs.wgsl b/src/lib/shaders/naive.fs.wgsl index b2d7b9b..eb1cd04 100644 --- a/src/lib/shaders/naive.fs.wgsl +++ b/src/lib/shaders/naive.fs.wgsl @@ -95,6 +95,46 @@ fn sandTexture(pos: vec3f) -> vec3f { return mix(sandDark, sandBase, sandNoise); } + +fn snowTexture(pos: vec3f) -> vec3f { + let sandScale = 20.0; + let sandNoise = fbm(pos.xz * sandScale); + + let sandBase = vec3f(0.94, 0.99, 1.0); + let sandDark = vec3f(0.812, 0.953, 0.969); + + return mix(sandDark, sandBase, sandNoise); +} + +fn mountainTexture(pos: vec3f) -> vec3f { + let noise1 = fbm(pos.xz * 3.0); + let noise2 = noise(pos.xz * 10.0); + let noise3 = noise(pos.xz * 30.0); + + let combinedNoise = noise1 * 0.5 + noise2 * 0.3 + noise3 * 0.2; + + let rockDark = vec3f(0.35, 0.35, 0.38); + let rockMid = vec3f(0.50, 0.48, 0.45); + let rockLight = vec3f(0.65, 0.62, 0.58); + + let t1 = smoothstep(0.25, 0.45, combinedNoise); + let t2 = smoothstep(0.55, 0.75, combinedNoise); + + var rockColor = rockDark; + rockColor = mix(rockColor, rockMid, t1); + rockColor = mix(rockColor, rockLight, t2); + + let snowStart = 1.0; + let snowFull = 1.5; + let snowTint = vec3f(0.95, 0.96, 0.98); + + let snowAmount = smoothstep(snowStart, snowFull, pos.y); + + rockColor = mix(rockColor, snowTint, snowAmount); + + return rockColor; +} + @group(0) @binding(1) var directionalLightUniforms: DirectionalLightUniforms; @group(0) @binding(2) var shadow_map: texture_depth_2d; @group(0) @binding(3) var shadow_sampler: sampler; @@ -104,6 +144,7 @@ fn main(in: FragmentInput) -> @location(0) vec4f { let biomeType = biome.biomeType; var baseColor: vec3f; + let underwater = vec3f(0.2, 0.3, 0.4); if (biomeType == 0) { // grassland @@ -112,7 +153,6 @@ fn main(in: FragmentInput) -> @location(0) vec4f let grass = grassTexture(in.pos); let sand = sandTexture(in.pos); - let underwater = vec3f(0.2, 0.3, 0.4); let grassStart = waterHeight.height + 0.8; let sandEnd = waterHeight.height + 0.4; @@ -134,7 +174,6 @@ fn main(in: FragmentInput) -> @location(0) vec4f let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); let sand = sandTexture(in.pos); - let underwater = vec3f(0.2, 0.3, 0.4); baseColor = sand; if (waterHeight.height > in.pos.y) { @@ -144,10 +183,24 @@ fn main(in: FragmentInput) -> @location(0) vec4f } } else if (biomeType == 2) { // mountain - baseColor = vec3f(0.2, 0.3, 0.4); + baseColor = mountainTexture(in.pos); + + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } } else if (biomeType == 3) { // tundra + let sandScale = 20.0; + let sandNoise = fbm(vec2f(in.pos.x, in.pos.z) * sandScale); + + let snow = snowTexture(in.pos); + baseColor = vec3f(1.0, 1.0, 1.0); + if (waterHeight.height > in.pos.y) { + baseColor = underwater; + } else { + baseColor = snow; + } } else { // Default to grass texture From 6a3268fc60b54bada7fd8b18be58956e81d00867 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 22:34:07 -0500 Subject: [PATCH 14/15] format --- src/lib/graph/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/graph/index.ts b/src/lib/graph/index.ts index cbf40ac..f675278 100644 --- a/src/lib/graph/index.ts +++ b/src/lib/graph/index.ts @@ -100,9 +100,9 @@ function generatePipelines( waterHeight: waterHeightSourceNode && waterHeightEdge ? nodeMapping.getHandleKey({ - sourceNode: waterHeightSourceNode, - outgoingHandleId: waterHeightEdge.sourceHandle!, - }) + sourceNode: waterHeightSourceNode, + outgoingHandleId: waterHeightEdge.sourceHandle!, + }) : undefined, biome: (() => { @@ -281,9 +281,9 @@ function generatePipelines( maskKey: maskSourceNode && maskEdge ? nodeMapping.getHandleKey({ - sourceNode: maskSourceNode, - outgoingHandleId: maskEdge.sourceHandle!, - }) + sourceNode: maskSourceNode, + outgoingHandleId: maskEdge.sourceHandle!, + }) : undefined, threshold: scatterNode.data.threshold, transform: transformConfig, From 7713bac0db13bc2a34f5d9385a89f494d44e8676 Mon Sep 17 00:00:00 2001 From: Neha Thumu Date: Sun, 7 Dec 2025 22:42:24 -0500 Subject: [PATCH 15/15] fix --- src/lib/renderers/terrain-renderer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/renderers/terrain-renderer.ts b/src/lib/renderers/terrain-renderer.ts index 885bfd8..c1868dc 100644 --- a/src/lib/renderers/terrain-renderer.ts +++ b/src/lib/renderers/terrain-renderer.ts @@ -90,7 +90,6 @@ export class TerrainRenderer implements IRenderer { private waterHeightUniformBuffer: GPUBuffer; private waterHeightBindGroupLayout: GPUBindGroupLayout; private waterHeightBindGroup: GPUBindGroup; - private currentWaterHeight: number = 0.0; /** Custom displace pipeline, gets reconfigured whenever node structure is changed */ private customDisplacePipeline: GPUComputePipeline; @@ -1316,7 +1315,6 @@ export class TerrainRenderer implements IRenderer { } setWaterHeightForTerrain(height: number) { - this.currentWaterHeight = height; this.device.queue.writeBuffer(this.waterHeightUniformBuffer, 0, new Float32Array([height])); console.log('Updated water height for terrain fragment shader:', height); }