diff --git a/packages/core/examples/volume_rendering/main.ts b/packages/core/examples/volume_rendering/main.ts index a5cd50ad1..b540b1964 100644 --- a/packages/core/examples/volume_rendering/main.ts +++ b/packages/core/examples/volume_rendering/main.ts @@ -30,17 +30,16 @@ const volumeLayer = new VolumeLayer({ }, { visible: true, - color: [1, 1, 1], + color: [0, 0, 1], contrastLimits: [108, 353], }, { visible: true, - color: [1, 1, 1], + color: [0, 1, 0], contrastLimits: [144, 3825], }, ], }); -volumeLayer.opacityMultiplier = 0.001; const idetik = new Idetik({ canvas: document.querySelector("#canvas")!, viewports: [ @@ -81,7 +80,7 @@ const opacityControls = { return (Math.log10(volumeLayer.opacityMultiplier) + 3) / 4; }, set opacity(sliderValue: number) { - volumeLayer.opacityMultiplier = Math.pow(10, sliderValue * 4 - 3); + volumeLayer.opacityMultiplier = 10 ** (sliderValue * 4 - 3); }, }; volumeFolder @@ -89,7 +88,7 @@ volumeFolder .name("Opacity") .decimals(2); volumeFolder - .add(volumeLayer, "earlyTerminationAlpha", 0.8, 1.0, 0.01) + .add(volumeLayer, "earlyTerminationAlpha", 0.01, 1.0, 0.01) .name("Early termination threshold"); const overlaysFolder = gui.addFolder("Debug"); @@ -97,5 +96,5 @@ overlaysFolder .add(volumeLayer, "debugShowWireframes") .name("Show tile wireframes"); overlaysFolder - .add(volumeLayer, "debugShowDegenerateRays") - .name("Show degenerate rays (length 0)"); + .add(volumeLayer, "debugSetOITWeightOne") + .name("Set OIT weight to 1 (no depth)"); diff --git a/packages/core/src/core/renderable_object.ts b/packages/core/src/core/renderable_object.ts index 264dc178d..44a134d07 100644 --- a/packages/core/src/core/renderable_object.ts +++ b/packages/core/src/core/renderable_object.ts @@ -33,6 +33,10 @@ export abstract class RenderableObject extends Node { return stale; } + public get visible() { + return true; + } + public get geometry() { return this.geometry_; } diff --git a/packages/core/src/layers/volume_layer.ts b/packages/core/src/layers/volume_layer.ts index 1a966a038..0bf075fbf 100644 --- a/packages/core/src/layers/volume_layer.ts +++ b/packages/core/src/layers/volume_layer.ts @@ -1,14 +1,20 @@ -import { Chunk, ChunkSource, SliceCoordinates } from "../data/chunk"; -import { Layer, RenderContext } from "../core/layer"; +import type { Chunk, ChunkSource, SliceCoordinates } from "../data/chunk"; +import { Layer, type RenderContext } from "../core/layer"; import { VolumeRenderable } from "../objects/renderable/volume_renderable"; -import { IdetikContext } from "../idetik"; -import { ChunkStoreView, INTERNAL_POLICY_KEY } from "../core/chunk_store_view"; -import { ImageSourcePolicy } from "../core/image_source_policy"; +import type { IdetikContext } from "../idetik"; +import { + type ChunkStoreView, + INTERNAL_POLICY_KEY, +} from "../core/chunk_store_view"; +import type { ImageSourcePolicy } from "../core/image_source_policy"; import { Texture3D } from "../objects/textures/texture_3d"; import { RenderablePool } from "../utilities/renderable_pool"; import { vec3 } from "gl-matrix"; import { sortFrontToBack } from "../math/sort_by_distance"; -import { ChannelProps, ChannelsEnabled } from "../objects/textures/channel"; +import type { + ChannelProps, + ChannelsEnabled, +} from "../objects/textures/channel"; export type VolumeLayerProps = { source: ChunkSource; @@ -38,9 +44,9 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { // TODO: Make a debug config object to manage debug options private debugShowWireframes_ = false; - public debugShowDegenerateRays = false; + public debugSetOITWeightOne = false; public relativeStepSize = 1.0; - public opacityMultiplier = 0.1; + public opacityMultiplier = 0.5; public earlyTerminationAlpha = 0.99; public get debugShowWireframes() { @@ -102,6 +108,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { private createVolume(chunk: Chunk) { const volume = new VolumeRenderable( Texture3D.createWithChunk(chunk), + chunk.chunkIndex.c, this.channelProps_ ); this.updateVolumeChunk(volume, chunk); @@ -238,7 +245,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled { public getUniforms(): Record { return { - DebugShowDegenerateRays: Number(this.debugShowDegenerateRays), + DebugSetOITWeightOne: Number(this.debugSetOITWeightOne), RelativeStepSize: this.relativeStepSize * this.interactiveStepSizeScale_, OpacityMultiplier: this.opacityMultiplier, EarlyTerminationAlpha: this.earlyTerminationAlpha, diff --git a/packages/core/src/objects/renderable/volume_renderable.ts b/packages/core/src/objects/renderable/volume_renderable.ts index 681af27f5..e7d80384b 100644 --- a/packages/core/src/objects/renderable/volume_renderable.ts +++ b/packages/core/src/objects/renderable/volume_renderable.ts @@ -5,40 +5,51 @@ import type { TextureDataType } from "../textures/texture"; import type { Texture3D } from "../textures/texture_3d"; import { vec3 } from "gl-matrix"; import { - Channel, - ChannelProps, + type Channel, + type ChannelProps, validateChannel, validateChannels, } from "../textures/channel"; export class VolumeRenderable extends RenderableObject { public voxelScale: vec3 = vec3.fromValues(1, 1, 1); + public channelIndex: number = 0; private channels_: Required[]; - constructor(texture: Texture3D, channels: ChannelProps[] = []) { + constructor( + texture: Texture3D, + channelIndex: number = 0, + channels: ChannelProps[] = [] + ) { super(); this.geometry = new BoxGeometry(1, 1, 1, 1, 1, 1); this.setTexture(0, texture); this.programName = dataTypeToVolumeShader(texture.dataType); this.cullFaceMode = "front"; this.depthTest = false; - // TODO handle visibility property of channels + this.channelIndex = channelIndex; this.channels_ = validateChannels(texture, channels); } + public get visible() { + const channel = this.getChannelOrDefault(this.channelIndex); + return channel.visible; + } + public get type() { return "VolumeRenderable"; } public override getUniforms(): Record { - const channel = this.channels_[0] ?? validateChannel(this.textures[0], {}); + const channel = this.getChannelOrDefault(this.channelIndex); const { color, contrastLimits } = channel; return { VoxelScale: this.voxelScale, Color: color.rgb, ValueOffset: -contrastLimits[0], ValueScale: 1 / (contrastLimits[1] - contrastLimits[0]), + DebugShowChunkBoundaries: Number(this.wireframeEnabled), }; } @@ -58,6 +69,12 @@ export class VolumeRenderable extends RenderableObject { this.channels_[channelIndex] = newChannel; } + + private getChannelOrDefault(channelIndex: number): Required { + return ( + this.channels_[channelIndex] ?? validateChannel(this.textures[0], {}) + ); + } } function dataTypeToVolumeShader(dataType: TextureDataType): Shader { diff --git a/packages/core/src/renderers/shaders/index.ts b/packages/core/src/renderers/shaders/index.ts index bac15d596..1dd74025a 100644 --- a/packages/core/src/renderers/shaders/index.ts +++ b/packages/core/src/renderers/shaders/index.ts @@ -10,6 +10,8 @@ import wireframeFragmentShader from "./wireframe_frag.glsl"; import volumeVertexShader from "./volume_vert.glsl"; import volumeFragmentShader from "./volume_frag.glsl"; import labelImage from "./label_image_frag.glsl"; +import transparentCompositeVertexShader from "./transparent_composite_vert.glsl"; +import transparentCompositeFragmentShader from "./transparent_composite_frag.glsl"; export type Shader = | "floatScalarImage" @@ -24,6 +26,7 @@ export type Shader = | "uintScalarImage" | "uintScalarImageArray" | "uintVolume" + | "transparentComposite" | "wireframe"; type ShaderCode = { @@ -92,4 +95,8 @@ export const shaderCode: Record = { fragment: volumeFragmentShader, fragmentDefines: new Map([["TEXTURE_DATA_TYPE_UINT", "1"]]), }, + transparentComposite: { + vertex: transparentCompositeVertexShader, + fragment: transparentCompositeFragmentShader, + }, }; diff --git a/packages/core/src/renderers/shaders/transparent_composite_frag.glsl b/packages/core/src/renderers/shaders/transparent_composite_frag.glsl new file mode 100644 index 000000000..1dc2e5924 --- /dev/null +++ b/packages/core/src/renderers/shaders/transparent_composite_frag.glsl @@ -0,0 +1,16 @@ +#version 300 es + +precision highp float; +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; + +in vec2 TexCoords; +layout (location = 0) out vec4 fragColor; + +void main() { + vec4 tex0 = texture(Sampler0, TexCoords); + vec4 tex1 = texture(Sampler1, TexCoords); + vec4 accum = vec4(tex0.rgb, tex1.r); + float revealage = tex0.a; + fragColor = vec4(accum.rgb / clamp(accum.a, 1e-4, 5e4), revealage); +} diff --git a/packages/core/src/renderers/shaders/transparent_composite_vert.glsl b/packages/core/src/renderers/shaders/transparent_composite_vert.glsl new file mode 100644 index 000000000..5f8b8956c --- /dev/null +++ b/packages/core/src/renderers/shaders/transparent_composite_vert.glsl @@ -0,0 +1,16 @@ +#version 300 es + +out vec2 TexCoords; + +void main() { + vec2 pos; + if (gl_VertexID == 0) { + pos = vec2(-1.0, -1.0); + } else if (gl_VertexID == 1) { + pos = vec2(3.0, -1.0); + } else { + pos = vec2(-1.0, 3.0); + } + TexCoords = pos * 0.5 + 0.5; + gl_Position = vec4(pos, 0.0, 1.0); +} diff --git a/packages/core/src/renderers/shaders/volume_frag.glsl b/packages/core/src/renderers/shaders/volume_frag.glsl index 5e54815b4..5006594d5 100644 --- a/packages/core/src/renderers/shaders/volume_frag.glsl +++ b/packages/core/src/renderers/shaders/volume_frag.glsl @@ -3,7 +3,8 @@ precision highp float; -layout (location = 0) out vec4 fragColor; +layout(location = 0) out vec4 FragData0; +layout(location = 1) out float FragData1; #if defined TEXTURE_DATA_TYPE_INT uniform mediump isampler3D ImageSampler; @@ -14,6 +15,9 @@ uniform mediump sampler3D ImageSampler; #endif uniform highp vec3 CameraPositionModel; +uniform mat4 Projection; +uniform mat4 ModelView; +uniform mat4 ModelViewProjection; in highp vec3 PositionModel; // The bounding box in model space is normalized to -0.5 to 0.5 @@ -21,7 +25,6 @@ vec3 boundingboxMin = vec3(-0.50); vec3 boundingboxMax = vec3(0.50); // Volume rendering parameters -uniform bool DebugShowDegenerateRays; uniform float OpacityMultiplier; uniform float EarlyTerminationAlpha; uniform float RelativeStepSize; @@ -29,6 +32,13 @@ uniform float ValueScale; uniform float ValueOffset; uniform vec3 Color; uniform vec3 VoxelScale; +uniform bool DebugShowChunkBoundaries; +uniform bool DebugSetOITWeightOne; + +float computeOITWeight(float alpha, float depth) { + float d = (1.0 - depth); + return alpha * max(1e-2, 3e3 * d * d * d); +} vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 boxMax) { vec3 reciprocalRayDir = 1.0 / rayDir; @@ -47,6 +57,25 @@ vec2 findBoxIntersectionsAlongRay(vec3 rayOrigin, vec3 rayDir, vec3 boxMin, vec3 } void main() { + // Initialize outputs to defaults for early exit cases + FragData0 = vec4(0.0, 0.0, 0.0, 0.0); + FragData1 = 0.0; + + if (DebugShowChunkBoundaries) { + vec3 distToMin = abs(PositionModel - boundingboxMin); + vec3 distToMax = abs(PositionModel - boundingboxMax); + bvec3 nearMin = lessThan(distToMin, vec3(0.01)); + bvec3 nearMax = lessThan(distToMax, vec3(0.01)); + bvec3 nearBoundary = bvec3(nearMin.x || nearMax.x, nearMin.y || nearMax.y, nearMin.z || nearMax.z); + int numDimsOnBoundary = int(nearBoundary.x) + int(nearBoundary.y) + int(nearBoundary.z); + bool onEdge = numDimsOnBoundary >= 2; + if (onEdge) { + FragData0 = vec4(1.0, 1.0, 1.0, 0.4); + FragData1 = 1.0; + } + return; + } + // Step 1 - calculate where the ray enters and exits the volume // The ray in model space goes from the camera to the point on the back face @@ -56,11 +85,6 @@ void main() { float tEnter = rayIntersections.x; float tExit = rayIntersections.y; - if (DebugShowDegenerateRays && (tExit == tEnter)) { - fragColor = vec4(1.0, 0.0, 0.0, 1.0); - return; - } - vec3 entryPoint = CameraPositionModel + RayDirModel * tEnter; entryPoint = clamp(entryPoint + 0.5, 0.0, 1.0); vec3 exitPoint = CameraPositionModel + RayDirModel * tExit; @@ -88,25 +112,38 @@ void main() { // Step 3 - perform the ray marching and compositing in front to back order vec3 position = entryPoint; + vec4 clipPosition; vec4 accumulatedColor = vec4(0.0); - float sampledData, sampleAlpha, blendedSampleAlpha; + vec3 sampleColor; + float revealage = 1.0; + float sampledData, sampleAlpha, blendedSampleAlpha, rayDepth, weight, scaledData; // Scale intensity to opacity. - // OpacityMultiplier and MaxIntensity control the transfer function. + // ValueScale is the // worldSpaceStepSize corrects for anisotropic voxels. - float intensityScale = OpacityMultiplier * worldSpaceStepSize * ValueScale; + float intensityScale = worldSpaceStepSize * ValueScale; // March until we reach the number of samples or accumulate enough opacity - for (int i = 0; i < numSamples && accumulatedColor.a < EarlyTerminationAlpha; i++) { + for (int i = 0; i < numSamples && revealage > (1.0 - EarlyTerminationAlpha); i++) { + // Sample the volume data and convert to color and opacity sampledData = vec4(texture(ImageSampler, position)).r; - sampleAlpha = (sampledData + ValueOffset) * intensityScale; - blendedSampleAlpha = (1.0 - accumulatedColor.a) * sampleAlpha; + scaledData = (sampledData + ValueOffset) * intensityScale; + sampleAlpha = clamp(scaledData * OpacityMultiplier, 0.0, 1.0); + sampleColor = Color * sampleAlpha; + + // Weighted blended OIT + clipPosition = ModelViewProjection * vec4(position, 1.0); + rayDepth = (clipPosition.z / clipPosition.w) * 0.5 + 0.5; + weight = computeOITWeight(sampleAlpha, rayDepth); + if (DebugSetOITWeightOne) { + weight = 1.0; + } + accumulatedColor += vec4(sampleColor, sampleAlpha) * weight; + revealage *= clamp(1.0 - sampleAlpha, 0.0, 1.0); - // Front-to-back compositing - accumulatedColor.a += blendedSampleAlpha; - accumulatedColor.rgb += Color * blendedSampleAlpha; position += stepIncrement; } - fragColor = accumulatedColor; + FragData0 = vec4(accumulatedColor.rgb, 1.0 - revealage); + FragData1 = accumulatedColor.a; } diff --git a/packages/core/src/renderers/webgl_framebuffers.ts b/packages/core/src/renderers/webgl_framebuffers.ts new file mode 100644 index 000000000..8ed26fca7 --- /dev/null +++ b/packages/core/src/renderers/webgl_framebuffers.ts @@ -0,0 +1,101 @@ +import { ImageTexture2D } from "./webgl_textures"; + +export class TransparencyBuffer { + private readonly gl_: WebGL2RenderingContext; + private framebuffer_: WebGLFramebuffer; + private buffers_: number[]; + public textures: ImageTexture2D[] = []; + + constructor(gl: WebGL2RenderingContext, width: number, height: number) { + this.gl_ = gl; + + this.framebuffer_ = this.gl_.createFramebuffer(); + const ext = this.gl_.getExtension("EXT_color_buffer_float"); + if (!ext) { + throw new Error( + "EXT_color_buffer_float extension is not supported by the browser and required for transparency buffer setup" + ); + } + this.buffers_ = [this.gl_.COLOR_ATTACHMENT0, this.gl_.COLOR_ATTACHMENT1]; + this.resize(width, height); + } + + bindTextures() { + this.gl_.activeTexture(this.gl_.TEXTURE0); + this.textures[0].bind(); + this.gl_.activeTexture(this.gl_.TEXTURE1); + this.textures[1].bind(); + } + + resize(width: number, height: number) { + if (width == 0 || height == 0) { + return; + } + this.gl_.bindFramebuffer(this.gl_.FRAMEBUFFER, this.framebuffer_); + + const texture0 = new ImageTexture2D(this.gl_, width, height); + this.gl_.activeTexture(this.gl_.TEXTURE0); + texture0.bind(); + this.gl_.framebufferTexture2D( + this.gl_.FRAMEBUFFER, + this.gl_.COLOR_ATTACHMENT0, + this.gl_.TEXTURE_2D, + texture0.texture, + 0 + ); + + const texture1 = new ImageTexture2D(this.gl_, width, height, "r"); + this.gl_.activeTexture(this.gl_.TEXTURE1); + texture1.bind(); + this.gl_.framebufferTexture2D( + this.gl_.FRAMEBUFFER, + this.gl_.COLOR_ATTACHMENT1, + this.gl_.TEXTURE_2D, + texture1.texture, + 0 + ); + + this.textures = [texture0, texture1]; + + const status = this.gl_.checkFramebufferStatus(this.gl_.FRAMEBUFFER); + if (status !== this.gl_.FRAMEBUFFER_COMPLETE) { + throw new Error( + `Failed to create transparency framebuffer: status ${status}` + ); + } + + this.gl_.bindFramebuffer(this.gl_.FRAMEBUFFER, null); + } + + begin() { + this.gl_.bindFramebuffer(this.gl_.FRAMEBUFFER, this.framebuffer_); + this.gl_.drawBuffers(this.buffers_); + this.gl_.blendFuncSeparate( + WebGL2RenderingContext.ONE, + WebGL2RenderingContext.ONE, + WebGL2RenderingContext.ZERO, + WebGL2RenderingContext.ONE_MINUS_SRC_ALPHA + ); + this.clear(); + this.gl_.enable(this.gl_.BLEND); + } + + end() { + // TODO: does this need to reset the state touched? + // or is it fine because on the next render loop that state will be reconfigured anyway? + this.gl_.bindFramebuffer(this.gl_.FRAMEBUFFER, null); + } + + clear() { + this.gl_.bindFramebuffer(this.gl_.FRAMEBUFFER, this.framebuffer_); + this.gl_.clearColor(0.0, 0.0, 0.0, 1.0); + this.gl_.clear(this.gl_.COLOR_BUFFER_BIT); + } + + dispose() { + this.gl_.deleteFramebuffer(this.framebuffer_); + for (const texture of this.textures) { + this.gl_.deleteTexture(texture.texture); + } + } +} diff --git a/packages/core/src/renderers/webgl_renderer.ts b/packages/core/src/renderers/webgl_renderer.ts index 85ecb13cc..71abce7bb 100644 --- a/packages/core/src/renderers/webgl_renderer.ts +++ b/packages/core/src/renderers/webgl_renderer.ts @@ -1,21 +1,22 @@ import { Renderer } from "../core/renderer"; -import { WebGLShaderProgram } from "./webgl_shader_program"; +import type { WebGLShaderProgram } from "./webgl_shader_program"; import { WebGLShaderPrograms } from "./webgl_shader_programs"; import { Logger } from "../utilities/logger"; import { WebGLBuffers } from "./webgl_buffers"; import { WebGLTextures } from "./webgl_textures"; +import { TransparencyBuffer } from "./webgl_framebuffers"; -import { Layer } from "../core/layer"; +import type { Layer } from "../core/layer"; import { WebGLState } from "./WebGLState"; -import { RenderableObject } from "../core/renderable_object"; -import { Geometry, Primitive } from "../core/geometry"; +import type { RenderableObject } from "../core/renderable_object"; +import type { Geometry, Primitive } from "../core/geometry"; import { Box2 } from "../math/box2"; -import { Viewport } from "../core/viewport"; -import { Camera } from "../objects/cameras/camera"; +import type { Viewport } from "../core/viewport"; +import type { Camera } from "../objects/cameras/camera"; import { mat4, vec2, vec3, vec4 } from "gl-matrix"; -import { Frustum } from "../math/frustum"; +import type { Frustum } from "../math/frustum"; // Idetik defines screen-space with +Y pointing downward. // With the default camera, the basis vectors are: @@ -27,12 +28,33 @@ import { Frustum } from "../math/frustum"; // This is a mirror transform, which also flips triangle winding. const axisDirection = mat4.fromScaling(mat4.create(), [1, -1, 1]); +class CompositePass { + private readonly gl_: WebGL2RenderingContext; + private readonly vao_: WebGLVertexArrayObject; + + constructor(gl: WebGL2RenderingContext) { + this.gl_ = gl; + this.vao_ = this.gl_.createVertexArray(); + this.gl_.bindVertexArray(this.vao_); + } + present(buffer: TransparencyBuffer) { + buffer.bindTextures(); + this.gl_.blendFunc(this.gl_.ONE_MINUS_SRC_ALPHA, this.gl_.SRC_ALPHA); + this.gl_.drawArrays(this.gl_.TRIANGLES, 0, 3); + } + dispose() { + this.gl_.deleteVertexArray(this.vao_); + } +} + export class WebGLRenderer extends Renderer { private readonly gl_: WebGL2RenderingContext; private readonly programs_: WebGLShaderPrograms; private readonly bindings_: WebGLBuffers; private readonly textures_: WebGLTextures; private readonly state_: WebGLState; + private readonly transparencyBuffer_: TransparencyBuffer; + private readonly compositePass_: CompositePass; private renderedObjectsPerFrame_ = 0; constructor(canvas: HTMLCanvasElement) { @@ -55,6 +77,12 @@ export class WebGLRenderer extends Renderer { this.programs_ = new WebGLShaderPrograms(gl); this.bindings_ = new WebGLBuffers(gl); this.textures_ = new WebGLTextures(gl); + this.transparencyBuffer_ = new TransparencyBuffer( + gl, + this.canvas.width, + this.canvas.height + ); + this.compositePass_ = new CompositePass(gl); this.state_ = new WebGLState(gl); this.initStencil(); this.resize(this.canvas.width, this.canvas.height); @@ -97,11 +125,18 @@ export class WebGLRenderer extends Renderer { } this.state_.setDepthMask(false); + this.transparencyBuffer_.begin(); for (const layer of transparent) { layer.update(renderContext); if (layer.state !== "ready") continue; this.renderLayer(layer, viewport.camera, frustum); } + this.transparencyBuffer_.end(); + const program = this.programs_.use("transparentComposite"); + program.setUniform("Sampler0", 0); + program.setUniform("Sampler1", 1); + this.compositePass_.present(this.transparencyBuffer_); + this.transparencyBuffer_.clear(); this.state_.setDepthMask(true); this.renderedObjects_ = this.renderedObjectsPerFrame_; @@ -141,13 +176,18 @@ export class WebGLRenderer extends Renderer { protected renderObject(layer: Layer, objectIndex: number, camera: Camera) { const object = layer.objects[objectIndex]; + // Dispose stale textures even for non visible objects + object.popStaleTextures().forEach((texture) => { + this.textures_.disposeTexture(texture); + }); + if (!object.visible) { + return; + } + this.state_.setCullFaceMode(object.cullFaceMode); this.state_.setDepthTesting(object.depthTest); this.state_.setDepthMask(object.depthTest); this.bindings_.bindGeometry(object.geometry); - object.popStaleTextures().forEach((texture) => { - this.textures_.disposeTexture(texture); - }); object.textures.forEach((texture, index) => { this.textures_.bindTexture(texture, index); }); @@ -203,6 +243,15 @@ export class WebGLRenderer extends Renderer { case "Projection": program.setUniform(uniformName, projection); break; + case "ModelViewProjection": { + const modelViewProjection = mat4.multiply( + mat4.create(), + projection, + modelView + ); + program.setUniform(uniformName, modelViewProjection); + break; + } case "Resolution": program.setUniform(uniformName, resolution); break; diff --git a/packages/core/src/renderers/webgl_textures.ts b/packages/core/src/renderers/webgl_textures.ts index 362c8f097..26301115b 100644 --- a/packages/core/src/renderers/webgl_textures.ts +++ b/packages/core/src/renderers/webgl_textures.ts @@ -7,9 +7,9 @@ import type { TextureDataFormat, } from "../objects/textures/texture"; -import { Texture2D } from "../objects/textures/texture_2d"; -import { Texture2DArray } from "../objects/textures/texture_2d_array"; -import { Texture3D } from "../objects/textures/texture_3d"; +import type { Texture2D } from "../objects/textures/texture_2d"; +import type { Texture2DArray } from "../objects/textures/texture_2d_array"; +import type { Texture3D } from "../objects/textures/texture_3d"; type TextureFormatInfo = { internalFormat: number; @@ -363,3 +363,83 @@ export class WebGLTextures { return texture.type === "Texture3D"; } } + +type SupportedImageType = "rgb" | "r"; +export class ImageTexture2D { + public texture: WebGLTexture; + + private readonly gl_: WebGL2RenderingContext; + private width_: number; + private height_: number; + private mode_: SupportedImageType = "rgb"; + + constructor( + gl: WebGL2RenderingContext, + width: number, + height: number, + mode: SupportedImageType = "rgb" + ) { + this.gl_ = gl; + this.width_ = width; + this.height_ = height; + this.mode_ = mode; + this.texture = this.gl_.createTexture(); + this.setupTexture(); + } + + private setupTexture() { + this.bind(); + this.gl_.texParameteri( + this.gl_.TEXTURE_2D, + this.gl_.TEXTURE_WRAP_S, + this.gl_.CLAMP_TO_EDGE + ); + this.gl_.texParameteri( + this.gl_.TEXTURE_2D, + this.gl_.TEXTURE_WRAP_T, + this.gl_.CLAMP_TO_EDGE + ); + this.gl_.texParameteri( + this.gl_.TEXTURE_2D, + this.gl_.TEXTURE_MIN_FILTER, + this.gl_.NEAREST + ); + this.gl_.texParameteri( + this.gl_.TEXTURE_2D, + this.gl_.TEXTURE_MAG_FILTER, + this.gl_.NEAREST + ); + switch (this.mode_) { + case "rgb": + this.gl_.texImage2D( + this.gl_.TEXTURE_2D, + 0, + this.gl_.RGBA16F, + this.width_, + this.height_, + 0, + this.gl_.RGBA, + this.gl_.HALF_FLOAT, + null + ); + break; + case "r": + this.gl_.texImage2D( + this.gl_.TEXTURE_2D, + 0, + this.gl_.R16F, + this.width_, + this.height_, + 0, + this.gl_.RED, + this.gl_.HALF_FLOAT, + null + ); + } + this.gl_.bindTexture(this.gl_.TEXTURE_2D, null); + } + + bind() { + this.gl_.bindTexture(this.gl_.TEXTURE_2D, this.texture); + } +}