From 6c71c5e5d6a8047dc8e810e712acda412eca781e Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Wed, 5 Jun 2024 23:55:22 -0300 Subject: [PATCH 1/7] Added webgl shader rect to ImageShader component --- src/components/common/ImageShader/index.tsx | 361 ++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/components/common/ImageShader/index.tsx diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx new file mode 100644 index 0000000..572d8b7 --- /dev/null +++ b/src/components/common/ImageShader/index.tsx @@ -0,0 +1,361 @@ +import { useEffect, useRef, useState } from "react" + +interface ImageSource { + media?: string, + srcSet: string, + type?: string, +} + +interface ImageProps { + src: string, + width?: number, + height?: number, + alt?: string, + srcSet?: ImageSource[], + className?: string, +} + +interface ImageShaderProps { + src: string, + fragSource: string, + width?: number, + height?: number, + alt?: string, + srcSet?: ImageSource[], + className?: string, + wrapperClassName?: string, +} + +export default function ImageShader(props: ImageShaderProps) { + const vert_source = ` + attribute vec2 a_position; + + uniform mat3 u_matrix; + + void main() { + gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); + }` + + let [gl, set_gl] = useState(null) // WebGL context + let [vert_shader, set_vert_shader] = useState(null) // Image vertex shader + let [frag_shader, set_frag_shader] = useState(null) // Image fragment shader + let [shader_prog, set_shader_prog] = useState(null) // Shader program + let [pos_location, set_pos_location] = useState(0) // Position attribute location + let [matrix_location, set_matrix_location] = useState(null) // Matrix attriubute location + let [pos_buffer, set_pos_buffer] = useState(null) // Position buffer + let [can_render, set_can_render] = useState(false) // Enables webgl rendering + let [canvas_width, set_canvas_width] = useState(0) + let [canvas_height, set_canvas_height] = useState(0) + + let canvas_ref = useRef(null) + + /** Get a webgl context for a canvas ref */ + let get_gl_context = (canvas: React.RefObject): WebGLRenderingContext | null => { + if (canvas.current === null) { + return null + } + + let ctx = canvas.current.getContext("webgl") + + if (ctx === null) { + return null + } + + return ctx + } + + /** + * Creates a webgl shader. + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html + */ + let create_shader = (gl: WebGLRenderingContext, type: number, source: string) => { + const shader = gl.createShader(type); + + if (shader === null) { + return null + } + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!success) { + console.error(gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + + return null + } + + return shader; + } + + /** + * Creates a shader program. + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html + */ + let create_program = ( + gl: WebGLRenderingContext, + vertexShader: WebGLShader, + fragmentShader: WebGLShader + ) => { + const program = gl.createProgram(); + + if (program === null) { + return null + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + const success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!success) { + console.error(gl.getProgramInfoLog(program)); + gl.deleteProgram(program); + + return null + } + + return program; + } + + let projection = (width: number, height: number) => { + let dst = new Float32Array(9); + + dst[0] = 2 / width; + dst[1] = 0; + dst[2] = 0; + dst[3] = 0; + dst[4] = -2 / height; + dst[5] = 0; + dst[6] = -1; + dst[7] = 1; + dst[8] = 1; + + return dst; + } + + /** Initializes webgl and shaders */ + let init = (canvas: React.RefObject, frag_source: string) => { + try { + // Get webgl context + const ctx = get_gl_context(canvas) + if (ctx === null) throw "can't get webgl context" + set_gl(ctx) + + // Create vertex shader + const vert = create_shader(ctx, ctx.VERTEX_SHADER, vert_source) + if (vert === null) throw "can't create vertex shader" + set_vert_shader(vert) + + // Create fragment shader + const frag = create_shader(ctx, ctx.FRAGMENT_SHADER, frag_source) + if (frag === null) throw "can't create fragment shader" + set_frag_shader(frag) + + // Create shader program + const program = create_program(ctx, frag, vert) + if (program === null) throw "can't create shader program" + set_shader_prog(program) + + // Get position attribute location + const pos_attr_location = ctx.getAttribLocation(program, "a_position"); + set_pos_location(pos_attr_location) + + // Bind position buffer + const buffer = ctx.createBuffer(); + if (buffer === null) throw "can't create position buffer" + set_pos_buffer(buffer) + ctx.bindBuffer(ctx.ARRAY_BUFFER, pos_buffer) + + // Create vertices + set_vertices(ctx) + + // Bind matrix uniform + const matrix_unif_location = ctx.getUniformLocation(program, "u_matrix") + if (matrix_unif_location === null) throw "can't get matrix uniform location" + set_matrix_location(matrix_unif_location) + + } catch (err) { + console.error(err) + set_gl(null) + } + } + + let set_vertices = (gl: WebGLRenderingContext) => { + let width = gl.canvas.width + let height = gl.canvas.height + + // Create vertices + let positions = [ + 0, 0, + width, 0, + width, height, + width, height, + 0, height, + 0, 0 + ]; + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + } + + let handle_resize = () => { + if (canvas_ref.current === null) { + return + } + + set_canvas_width(canvas_ref.current.clientWidth) + set_canvas_height(canvas_ref.current.clientHeight) + + if (gl !== null) { + set_vertices(gl) + } + } + + + /** Render webgl canvas */ + let render = () => { + if (gl === null) return + if (shader_prog === null) return + if (pos_location === null) return + if (pos_buffer === null) return + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Clear the canvas + gl.clearColor(0.1, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + gl.useProgram(shader_prog); + + gl.enableVertexAttribArray(pos_location); + + // Bind the position buffer. + gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); + + // Set geometry + gl.vertexAttribPointer( + pos_location, + 2, // 2 components per iteration + gl.FLOAT, // the data is 32bit floats + false, // don't normalize the data + 0, // 0 = move forward size * sizeof(type) each iteration to get the next position + 0, // start at the beginning of the buffer + ) + + // Compute the matrices + const projectionMatrix = projection(gl.canvas.width, gl.canvas.height); + + // Set the matrix. + gl.uniformMatrix3fv(matrix_location, false, projectionMatrix); + + // Draw + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + let Image = (props: ImageProps) => { + if (props.srcSet === undefined) { + return ( + {props.alt} + ) + } else { + return ( + + {props.srcSet.map((source, idx) => ( + + ))} + + {props.alt} + + ) + } + } + + // Updated can render state + useEffect(() => { + let initialized = () => { + if (gl === null) return false + if (shader_prog === null) return false + if (pos_location === null) return false + if (pos_buffer === null) return false + return true + } + set_can_render(initialized()) + }, [ + gl, + shader_prog, + pos_location, + pos_buffer, + ]) + + // Render when can render is true + useEffect(() => { + if (!can_render) return + render() + }, [can_render]) + + // Render when canvas dimensions change + useEffect(() => { + render() + }, [canvas_width, canvas_height]) + + // Component did mount + useEffect(() => { + if (props.width !== undefined) { + set_canvas_width(props.width) + } + if (props.height !== undefined) { + set_canvas_height(props.height) + } + + handle_resize() + init(canvas_ref, props.fragSource) + + let interval = window.setInterval(render, 100); + + window.addEventListener("resize", handle_resize) + + return () => { + window.clearInterval(interval) + window.removeEventListener("resize", handle_resize) + } + }, []) + + return ( +
+ + + + {props.alt} +
+ ) +} From 0c91d30e4ab19cae9b447b9682b09ae18d4cb6eb Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Tue, 11 Jun 2024 20:12:45 -0300 Subject: [PATCH 2/7] Added basic texture rendering to ImageShader --- src/components/common/ImageShader/index.tsx | 336 ++++++++++++++---- .../home/sections/Work/WorkCard/index.tsx | 72 ++-- 2 files changed, 311 insertions(+), 97 deletions(-) diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index 572d8b7..306909d 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -29,24 +29,55 @@ interface ImageShaderProps { export default function ImageShader(props: ImageShaderProps) { const vert_source = ` attribute vec2 a_position; + attribute vec2 a_texcoord; - uniform mat3 u_matrix; + uniform vec2 u_resolution; + + varying vec2 v_texcoord; void main() { - gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); + // convert the position from pixels to 0.0 to 1.0 + vec2 zeroToOne = a_position.xy / u_resolution; + + // convert from 0->1 to 0->2 + vec2 zeroToTwo = zeroToOne * 2.0; + + // convert from 0->2 to -1->+1 (clipspace) + vec2 clipSpace = zeroToTwo - 1.0; + + gl_Position = vec4(clipSpace, 0, 1); + v_texcoord = a_texcoord; }` let [gl, set_gl] = useState(null) // WebGL context - let [vert_shader, set_vert_shader] = useState(null) // Image vertex shader - let [frag_shader, set_frag_shader] = useState(null) // Image fragment shader + + // Shader state let [shader_prog, set_shader_prog] = useState(null) // Shader program + + // Attribute location state let [pos_location, set_pos_location] = useState(0) // Position attribute location - let [matrix_location, set_matrix_location] = useState(null) // Matrix attriubute location + let [tex_coord_location, set_tex_coord_location] = useState(0) // Texcoord attribute location + // let [matrix_location, set_matrix_location] = useState(null) // Matrix attriubute location + + // Uniform location state + let [res_location, set_res_location] = useState(null) // Resolution uniform + let [sampler_location, set_sampler_location] = useState(null) // Sampler uniform + + // Buffer state let [pos_buffer, set_pos_buffer] = useState(null) // Position buffer + let [tex_coord_buffer, set_tex_coord_buffer] = useState(null) // Texcoord buffer + + // Texture state + let [texture, set_texture] = useState(null) + + // Renderer state let [can_render, set_can_render] = useState(false) // Enables webgl rendering + + // Canvas dimensions let [canvas_width, set_canvas_width] = useState(0) let [canvas_height, set_canvas_height] = useState(0) + // Canvas element reference let canvas_ref = useRef(null) /** Get a webgl context for a canvas ref */ @@ -119,85 +150,196 @@ export default function ImageShader(props: ImageShaderProps) { return program; } - let projection = (width: number, height: number) => { - let dst = new Float32Array(9); + /** Fill a buffer with position data for a rectangle */ + let set_rect_vertices = ( + gl: WebGLRenderingContext, + x: number, + y: number, + width: number, + height: number + ) => { + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + x, y, // top left + x + width, y, // top right + x + width, y + height, // bottom right + x + width, y + height, // bottom right + x, y + height, // bottom left + x, y // top left + ]), + gl.STATIC_DRAW + ); + } + + /** Fill a buffer with texture coordinates for a rectangle */ + let set_rect_texcoords = (gl: WebGLRenderingContext) => { + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 0, 0, // top left + 1, 0, // top right + 1, 1, // bottom right + 1, 1, // bottom right + 0, 1, // bottom left + 0, 0 // top left + ]), + gl.STATIC_DRAW) + } - dst[0] = 2 / width; - dst[1] = 0; - dst[2] = 0; - dst[3] = 0; - dst[4] = -2 / height; - dst[5] = 0; - dst[6] = -1; - dst[7] = 1; - dst[8] = 1; + /** Is a number of 2 */ + let is_power_of_2 = (value: number): boolean => { + return (value & (value - 1)) === 0; + } - return dst; + /** Loads an image from a src and binds it to a webgl texture */ + let load_texture = (gl: WebGLRenderingContext, src: string): Promise => { + return new Promise((resolve, reject) => { + // Create image element + const image = document.createElement("img") + + // On image loaded + image.addEventListener("load", () => { + // Create texture. + const texture = gl.createTexture() + + if (texture === null) { + reject("Failed to create texture") + return + } + + gl.bindTexture(gl.TEXTURE_2D, texture) + + // Fill texture with a 1x1 blue pixel + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255]) + ) + + gl.bindTexture(gl.TEXTURE_2D, texture) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) + + // Handle power of 2 images and non power of 2 images + if (is_power_of_2(image.width) && is_power_of_2(image.height)) { + // Generate mip maps + gl.generateMipmap(gl.TEXTURE_2D); + } else { + // Turn off mipmaps clamp image to edge + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + + resolve(texture) + }) + + // On image load failed + image.addEventListener("error", (e) => { + reject(e.message) + }) + + // Set image source + image.src = src + }) } /** Initializes webgl and shaders */ - let init = (canvas: React.RefObject, frag_source: string) => { + let init = async (canvas: React.RefObject, vert_source: string, frag_source: string) => { try { // Get webgl context const ctx = get_gl_context(canvas) if (ctx === null) throw "can't get webgl context" set_gl(ctx) + // Flip image unpack direction + ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true); + + // + // Create Shaders + // + // Create vertex shader const vert = create_shader(ctx, ctx.VERTEX_SHADER, vert_source) if (vert === null) throw "can't create vertex shader" - set_vert_shader(vert) // Create fragment shader const frag = create_shader(ctx, ctx.FRAGMENT_SHADER, frag_source) if (frag === null) throw "can't create fragment shader" - set_frag_shader(frag) // Create shader program const program = create_program(ctx, frag, vert) if (program === null) throw "can't create shader program" set_shader_prog(program) + // + // Get attribute locations + // + // Get position attribute location const pos_attr_location = ctx.getAttribLocation(program, "a_position"); set_pos_location(pos_attr_location) + // Get texcoord attribute location + const tex_coord_attr_location = ctx.getAttribLocation(program, "a_texcoord"); + set_tex_coord_location(tex_coord_attr_location) + + // + // Get uniform locations + // + + const resolution_unif_location = ctx.getUniformLocation(program, "u_resolution") + if (resolution_unif_location === null) throw "can't get u_resolution uniform location" + set_res_location(resolution_unif_location) + + const sampler_unif_location = ctx.getUniformLocation(program, "u_texture") + if (sampler_unif_location === null) throw "can't get u_texture uniform location" + set_sampler_location(sampler_unif_location) + + // + // Bind buffers + // + // Bind position buffer - const buffer = ctx.createBuffer(); - if (buffer === null) throw "can't create position buffer" - set_pos_buffer(buffer) - ctx.bindBuffer(ctx.ARRAY_BUFFER, pos_buffer) + const position_buffer = ctx.createBuffer(); + if (position_buffer === null) throw "can't create position buffer" + set_pos_buffer(position_buffer) + ctx.bindBuffer(ctx.ARRAY_BUFFER, position_buffer) + + // Create verticeis + set_rect_vertices(ctx, 0, 0, ctx.canvas.width, ctx.canvas.height) - // Create vertices - set_vertices(ctx) + // Bind texcoord buffer + var coord_buffer = ctx.createBuffer(); + if (coord_buffer === null) throw "can't create texcoord buffer" + set_tex_coord_buffer(coord_buffer) + ctx.bindBuffer(ctx.ARRAY_BUFFER, coord_buffer); + + // Create texcoord + set_rect_texcoords(ctx) + + // Load texture + let tex = await load_texture(ctx, props.src) + set_texture(tex) // Bind matrix uniform - const matrix_unif_location = ctx.getUniformLocation(program, "u_matrix") - if (matrix_unif_location === null) throw "can't get matrix uniform location" - set_matrix_location(matrix_unif_location) + // const matrix_unif_location = ctx.getUniformLocation(program, "u_matrix") + // if (matrix_unif_location === null) throw "can't get matrix uniform location" + // set_matrix_location(matrix_unif_location) } catch (err) { console.error(err) - set_gl(null) + set_can_render(false) } } - let set_vertices = (gl: WebGLRenderingContext) => { - let width = gl.canvas.width - let height = gl.canvas.height - - // Create vertices - let positions = [ - 0, 0, - width, 0, - width, height, - width, height, - 0, height, - 0, 0 - ]; - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); - } - + /** Handles window resize event */ let handle_resize = () => { if (canvas_ref.current === null) { return @@ -205,10 +347,6 @@ export default function ImageShader(props: ImageShaderProps) { set_canvas_width(canvas_ref.current.clientWidth) set_canvas_height(canvas_ref.current.clientHeight) - - if (gl !== null) { - set_vertices(gl) - } } @@ -216,8 +354,10 @@ export default function ImageShader(props: ImageShaderProps) { let render = () => { if (gl === null) return if (shader_prog === null) return - if (pos_location === null) return + if (sampler_location === null) return + if (tex_coord_buffer === null) return if (pos_buffer === null) return + if (texture === null) return gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); @@ -225,30 +365,77 @@ export default function ImageShader(props: ImageShaderProps) { gl.clearColor(0.1, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); + // Use shader program gl.useProgram(shader_prog); + // + // Position attribute + // + + // Enable attribute gl.enableVertexAttribArray(pos_location); // Bind the position buffer. gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); - // Set geometry + // Set position data pointer gl.vertexAttribPointer( pos_location, - 2, // 2 components per iteration - gl.FLOAT, // the data is 32bit floats - false, // don't normalize the data - 0, // 0 = move forward size * sizeof(type) each iteration to get the next position - 0, // start at the beginning of the buffer + 2, // 2 components per iteration + gl.FLOAT, // 32bit float data + false, // don't normalize data + 0, // move by size * sizeof(type) each iteration + 0 // offset + ) + + // + // Texcoord attribute + // + + // Enable attribute + gl.enableVertexAttribArray(tex_coord_location); + + // Bind the texcoord buffer + gl.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); + + + // Set texcoord data pointer + gl.vertexAttribPointer( + tex_coord_location, + 2, // 2 components per iteration + gl.FLOAT, // 32bit float data + false, // don't normalize data + 0, // move by size * sizeof(type) each iteration + 0 // offset ) - // Compute the matrices - const projectionMatrix = projection(gl.canvas.width, gl.canvas.height); + // + // Texture + // + + // Use texture unit 0 + gl.activeTexture(gl.TEXTURE0) + + // Bind texture + gl.bindTexture(gl.TEXTURE_2D, texture) + + // + // Uniforms + // - // Set the matrix. - gl.uniformMatrix3fv(matrix_location, false, projectionMatrix); + // Update resolution uniform + gl.uniform2f(res_location, gl.canvas.width, gl.canvas.height) + // Update sampler uniform + gl.uniform1i(sampler_location, 0) + + // Compute the camera matrix + // const projectionMatrix = projection(gl.canvas.width, gl.canvas.height); + // gl.uniformMatrix3fv(matrix_location, false, projectionMatrix); + + // // Draw + // gl.drawArrays(gl.TRIANGLES, 0, 6); } @@ -287,32 +474,50 @@ export default function ImageShader(props: ImageShaderProps) { } } + // + // Hooks + // + // Updated can render state useEffect(() => { let initialized = () => { if (gl === null) return false if (shader_prog === null) return false + if (sampler_location === null) return false if (pos_location === null) return false if (pos_buffer === null) return false + if (texture === null) return false return true } set_can_render(initialized()) }, [ gl, shader_prog, + sampler_location, pos_location, pos_buffer, + texture ]) - // Render when can render is true + // Rerender when canvas state changes useEffect(() => { if (!can_render) return render() }, [can_render]) - // Render when canvas dimensions change + // Reset position buffer and rerender when canvas dimensions change useEffect(() => { + if (gl === null || pos_buffer === null) { + return + } + + // Rebind position buffer to canvas dimensions + gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer) + set_rect_vertices(gl, 0, 0, canvas_width, canvas_height) + + // rerender render() + }, [canvas_width, canvas_height]) // Component did mount @@ -325,14 +530,11 @@ export default function ImageShader(props: ImageShaderProps) { } handle_resize() - init(canvas_ref, props.fragSource) - - let interval = window.setInterval(render, 100); + init(canvas_ref, vert_source, props.fragSource) window.addEventListener("resize", handle_resize) return () => { - window.clearInterval(interval) window.removeEventListener("resize", handle_resize) } }, []) diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index 9786f03..4dc1129 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -5,6 +5,7 @@ import { addLeadingZeros } from "../../../../../utils/numberFormatting"; import { Work } from "../../../../../types"; import SkillIcon from "../../../../common/SkillIcon"; import { Link } from "react-router-dom"; +import ImageShader from "../../../../common/ImageShader"; interface WorkCardProps { work: Work; @@ -19,6 +20,19 @@ const WorkCard: FunctionComponent = (props) => { const work_link = (id: string) => `/work/${id.toLowerCase()}`; + const frag_source = ` + precision mediump float; + + varying highp vec2 v_texcoord; + + uniform sampler2D u_texture; + + void main() { + //vec4 tex_col = texture2D(u_texture, v_texcoord); + //gl_FragColor = vec4(tex_col.x + 1., 1., 1., 1.); + gl_FragColor = texture2D(u_texture, v_texcoord); + }` + return (
= (props) => { to={work_link(props.work.id)} className="block aspect-[16/9] xl:aspect-auto w-full h-full overflow-hidden border border-brand-gray-400 rounded" > - - - - - - - - {`${props.work.title}`} - +
From 5deec36c3a049c4b3059a2916d98efe73e9c24eb Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Wed, 12 Jun 2024 16:15:17 -0300 Subject: [PATCH 3/7] Moved webgl functions to util file --- src/components/common/ImageShader/index.tsx | 348 ++++-------------- .../home/sections/Work/WorkCard/index.tsx | 2 - src/utils/webgl/images.ts | 91 +++++ src/utils/webgl/webgl.ts | 190 ++++++++++ 4 files changed, 359 insertions(+), 272 deletions(-) create mode 100644 src/utils/webgl/images.ts create mode 100644 src/utils/webgl/webgl.ts diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index 306909d..b131754 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -1,10 +1,6 @@ -import { useEffect, useRef, useState } from "react" - -interface ImageSource { - media?: string, - srcSet: string, - type?: string, -} +import { useEffect, useLayoutEffect, useRef, useState } from "react" +import { ImageSource, get_image_src } from "../../../utils/webgl/images" +import { create_program_from_strings, get_gl_context, load_texture, set_rect_texcoords, set_rect_vertices } from "../../../utils/webgl/webgl" interface ImageProps { src: string, @@ -26,6 +22,7 @@ interface ImageShaderProps { wrapperClassName?: string, } + export default function ImageShader(props: ImageShaderProps) { const vert_source = ` attribute vec2 a_position; @@ -46,7 +43,7 @@ export default function ImageShader(props: ImageShaderProps) { vec2 clipSpace = zeroToTwo - 1.0; gl_Position = vec4(clipSpace, 0, 1); - v_texcoord = a_texcoord; + v_texcoord = vec2(a_texcoord.x, 1. - a_texcoord.y); }` let [gl, set_gl] = useState(null) // WebGL context @@ -74,208 +71,23 @@ export default function ImageShader(props: ImageShaderProps) { let [can_render, set_can_render] = useState(false) // Enables webgl rendering // Canvas dimensions - let [canvas_width, set_canvas_width] = useState(0) - let [canvas_height, set_canvas_height] = useState(0) + let [canvas_width, set_canvas_width] = useState(null) + let [canvas_height, set_canvas_height] = useState(null) // Canvas element reference let canvas_ref = useRef(null) - /** Get a webgl context for a canvas ref */ - let get_gl_context = (canvas: React.RefObject): WebGLRenderingContext | null => { - if (canvas.current === null) { - return null - } - - let ctx = canvas.current.getContext("webgl") - - if (ctx === null) { - return null - } - - return ctx - } - - /** - * Creates a webgl shader. - * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html - */ - let create_shader = (gl: WebGLRenderingContext, type: number, source: string) => { - const shader = gl.createShader(type); - - if (shader === null) { - return null - } - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (!success) { - console.error(gl.getShaderInfoLog(shader)); - gl.deleteShader(shader); - - return null - } - - return shader; - } - - /** - * Creates a shader program. - * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html - */ - let create_program = ( - gl: WebGLRenderingContext, - vertexShader: WebGLShader, - fragmentShader: WebGLShader - ) => { - const program = gl.createProgram(); - - if (program === null) { - return null - } - - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - - const success = gl.getProgramParameter(program, gl.LINK_STATUS); - if (!success) { - console.error(gl.getProgramInfoLog(program)); - gl.deleteProgram(program); - - return null - } - - return program; - } - - /** Fill a buffer with position data for a rectangle */ - let set_rect_vertices = ( - gl: WebGLRenderingContext, - x: number, - y: number, - width: number, - height: number - ) => { - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - x, y, // top left - x + width, y, // top right - x + width, y + height, // bottom right - x + width, y + height, // bottom right - x, y + height, // bottom left - x, y // top left - ]), - gl.STATIC_DRAW - ); - } - - /** Fill a buffer with texture coordinates for a rectangle */ - let set_rect_texcoords = (gl: WebGLRenderingContext) => { - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0, 0, // top left - 1, 0, // top right - 1, 1, // bottom right - 1, 1, // bottom right - 0, 1, // bottom left - 0, 0 // top left - ]), - gl.STATIC_DRAW) - } - - /** Is a number of 2 */ - let is_power_of_2 = (value: number): boolean => { - return (value & (value - 1)) === 0; - } - - /** Loads an image from a src and binds it to a webgl texture */ - let load_texture = (gl: WebGLRenderingContext, src: string): Promise => { - return new Promise((resolve, reject) => { - // Create image element - const image = document.createElement("img") - - // On image loaded - image.addEventListener("load", () => { - // Create texture. - const texture = gl.createTexture() - - if (texture === null) { - reject("Failed to create texture") - return - } - - gl.bindTexture(gl.TEXTURE_2D, texture) - - // Fill texture with a 1x1 blue pixel - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - 1, - 1, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - new Uint8Array([0, 0, 255, 255]) - ) - - gl.bindTexture(gl.TEXTURE_2D, texture) - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) - - // Handle power of 2 images and non power of 2 images - if (is_power_of_2(image.width) && is_power_of_2(image.height)) { - // Generate mip maps - gl.generateMipmap(gl.TEXTURE_2D); - } else { - // Turn off mipmaps clamp image to edge - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - } - - resolve(texture) - }) - - // On image load failed - image.addEventListener("error", (e) => { - reject(e.message) - }) - - // Set image source - image.src = src - }) - } - /** Initializes webgl and shaders */ - let init = async (canvas: React.RefObject, vert_source: string, frag_source: string) => { + let init = async (canvas_ref: React.RefObject, vert_source: string, frag_source: string) => { try { + if (canvas_ref.current === null) throw "can't get canvas element" + // Get webgl context - const ctx = get_gl_context(canvas) - if (ctx === null) throw "can't get webgl context" + const ctx = get_gl_context(canvas_ref) set_gl(ctx) - // Flip image unpack direction - ctx.pixelStorei(ctx.UNPACK_FLIP_Y_WEBGL, true); - - // - // Create Shaders - // - - // Create vertex shader - const vert = create_shader(ctx, ctx.VERTEX_SHADER, vert_source) - if (vert === null) throw "can't create vertex shader" - - // Create fragment shader - const frag = create_shader(ctx, ctx.FRAGMENT_SHADER, frag_source) - if (frag === null) throw "can't create fragment shader" - // Create shader program - const program = create_program(ctx, frag, vert) - if (program === null) throw "can't create shader program" + const program = create_program_from_strings(ctx, vert_source, frag_source) set_shader_prog(program) // @@ -283,12 +95,10 @@ export default function ImageShader(props: ImageShaderProps) { // // Get position attribute location - const pos_attr_location = ctx.getAttribLocation(program, "a_position"); - set_pos_location(pos_attr_location) + set_pos_location(ctx.getAttribLocation(program, "a_position")) // Get texcoord attribute location - const tex_coord_attr_location = ctx.getAttribLocation(program, "a_texcoord"); - set_tex_coord_location(tex_coord_attr_location) + set_tex_coord_location(ctx.getAttribLocation(program, "a_texcoord")) // // Get uniform locations @@ -310,76 +120,73 @@ export default function ImageShader(props: ImageShaderProps) { const position_buffer = ctx.createBuffer(); if (position_buffer === null) throw "can't create position buffer" set_pos_buffer(position_buffer) - ctx.bindBuffer(ctx.ARRAY_BUFFER, position_buffer) // Create verticeis - set_rect_vertices(ctx, 0, 0, ctx.canvas.width, ctx.canvas.height) + set_rect_vertices( + ctx, + position_buffer, + 0, + 0, + canvas_width || ctx.canvas.width, + canvas_height || ctx.canvas.height + ) // Bind texcoord buffer var coord_buffer = ctx.createBuffer(); if (coord_buffer === null) throw "can't create texcoord buffer" set_tex_coord_buffer(coord_buffer) - ctx.bindBuffer(ctx.ARRAY_BUFFER, coord_buffer); // Create texcoord - set_rect_texcoords(ctx) + set_rect_texcoords(ctx, coord_buffer) // Load texture - let tex = await load_texture(ctx, props.src) + let img_src = await get_image_src(props.src, props.srcSet) + console.log(img_src) + let tex = await load_texture(ctx, img_src) set_texture(tex) - // Bind matrix uniform - // const matrix_unif_location = ctx.getUniformLocation(program, "u_matrix") - // if (matrix_unif_location === null) throw "can't get matrix uniform location" - // set_matrix_location(matrix_unif_location) - } catch (err) { console.error(err) set_can_render(false) } } - /** Handles window resize event */ - let handle_resize = () => { - if (canvas_ref.current === null) { - return - } - - set_canvas_width(canvas_ref.current.clientWidth) - set_canvas_height(canvas_ref.current.clientHeight) + /** Whether webgl is initialized */ + let initialized = () => { + if (gl === null) return false + if (shader_prog === null) return false + if (sampler_location === null) return false + if (tex_coord_buffer === null) return false + if (pos_buffer === null) return false + if (texture === null) return false + return true } - /** Render webgl canvas */ let render = () => { - if (gl === null) return - if (shader_prog === null) return - if (sampler_location === null) return - if (tex_coord_buffer === null) return - if (pos_buffer === null) return - if (texture === null) return + if (!initialized()) return - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl?.viewport(0, 0, gl.canvas.width, gl.canvas.height); // Clear the canvas - gl.clearColor(0.1, 0, 0, 0); - gl.clear(gl.COLOR_BUFFER_BIT); + gl?.clearColor(0.1, 0, 0, 0); + gl?.clear(gl.COLOR_BUFFER_BIT); // Use shader program - gl.useProgram(shader_prog); + gl?.useProgram(shader_prog); // // Position attribute // // Enable attribute - gl.enableVertexAttribArray(pos_location); + gl?.enableVertexAttribArray(pos_location); // Bind the position buffer. - gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); + gl?.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); // Set position data pointer - gl.vertexAttribPointer( + gl?.vertexAttribPointer( pos_location, 2, // 2 components per iteration gl.FLOAT, // 32bit float data @@ -393,14 +200,14 @@ export default function ImageShader(props: ImageShaderProps) { // // Enable attribute - gl.enableVertexAttribArray(tex_coord_location); + gl?.enableVertexAttribArray(tex_coord_location); // Bind the texcoord buffer - gl.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); + gl?.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); // Set texcoord data pointer - gl.vertexAttribPointer( + gl?.vertexAttribPointer( tex_coord_location, 2, // 2 components per iteration gl.FLOAT, // 32bit float data @@ -414,31 +221,37 @@ export default function ImageShader(props: ImageShaderProps) { // // Use texture unit 0 - gl.activeTexture(gl.TEXTURE0) + gl?.activeTexture(gl.TEXTURE0) // Bind texture - gl.bindTexture(gl.TEXTURE_2D, texture) + gl?.bindTexture(gl.TEXTURE_2D, texture) // // Uniforms // // Update resolution uniform - gl.uniform2f(res_location, gl.canvas.width, gl.canvas.height) + gl?.uniform2f(res_location, gl.canvas.width, gl.canvas.height) // Update sampler uniform - gl.uniform1i(sampler_location, 0) - - // Compute the camera matrix - // const projectionMatrix = projection(gl.canvas.width, gl.canvas.height); - // gl.uniformMatrix3fv(matrix_location, false, projectionMatrix); + gl?.uniform1i(sampler_location, 0) // // Draw // - gl.drawArrays(gl.TRIANGLES, 0, 6); + + gl?.drawArrays(gl.TRIANGLES, 0, 6); } + /** Handles window resize event */ + let handle_resize = () => { + if (canvas_ref.current === null) return + + set_canvas_width(canvas_ref.current.clientWidth) + set_canvas_height(canvas_ref.current.clientHeight) + } + + /** Image fallback component */ let Image = (props: ImageProps) => { if (props.srcSet === undefined) { return ( @@ -480,15 +293,6 @@ export default function ImageShader(props: ImageShaderProps) { // Updated can render state useEffect(() => { - let initialized = () => { - if (gl === null) return false - if (shader_prog === null) return false - if (sampler_location === null) return false - if (pos_location === null) return false - if (pos_buffer === null) return false - if (texture === null) return false - return true - } set_can_render(initialized()) }, [ gl, @@ -507,13 +311,23 @@ export default function ImageShader(props: ImageShaderProps) { // Reset position buffer and rerender when canvas dimensions change useEffect(() => { + if (!can_render) { + init(canvas_ref, vert_source, props.fragSource) + + } if (gl === null || pos_buffer === null) { return } // Rebind position buffer to canvas dimensions - gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer) - set_rect_vertices(gl, 0, 0, canvas_width, canvas_height) + set_rect_vertices( + gl, + pos_buffer, + 0, + 0, + canvas_width || gl.canvas.width, + canvas_height || gl.canvas.height + ) // rerender render() @@ -522,17 +336,11 @@ export default function ImageShader(props: ImageShaderProps) { // Component did mount useEffect(() => { - if (props.width !== undefined) { - set_canvas_width(props.width) - } - if (props.height !== undefined) { - set_canvas_height(props.height) - } - - handle_resize() - init(canvas_ref, vert_source, props.fragSource) + if (props.width !== undefined) set_canvas_width(props.width) + if (props.height !== undefined) set_canvas_height(props.height) window.addEventListener("resize", handle_resize) + window.requestAnimationFrame(() => handle_resize()) return () => { window.removeEventListener("resize", handle_resize) @@ -543,8 +351,8 @@ export default function ImageShader(props: ImageShaderProps) {
diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index 4dc1129..3265de4 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -67,8 +67,6 @@ const WorkCard: FunctionComponent = (props) => { }, ]} fragSource={frag_source} - width={580} - height={337} alt={`${props.work.title}`} wrapperClassName="w-full h-full object-cover group-hover/card:scale-105 transition duration-200" /> diff --git a/src/utils/webgl/images.ts b/src/utils/webgl/images.ts new file mode 100644 index 0000000..ad5bd51 --- /dev/null +++ b/src/utils/webgl/images.ts @@ -0,0 +1,91 @@ +/** Image helper functions */ + +/** Image source object */ +export interface ImageSource { + media?: string, // Image media query + srcSet: string, // Image src string + type: string, // Image type +} + +/** Webp image features */ +export enum WebpFeature { + LOSSY = "lossy", + LOSSLESS = "lossless", + ALPHA = "alpha", + ANIMATION = "animation" +} + +/** + * Checks whether a webp feature is supported. + * Adapted from: https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp +*/ +export function check_webp_feature( + feature: WebpFeature, +): Promise { + return new Promise((resolve, reject) => { + var kTestImages = { + lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA", + lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==", + alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==", + animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA" + }; + + var img = document.createElement("img") + + img.onload = function() { + var result = (img.width > 0) && (img.height > 0); + resolve(result) + }; + + img.onerror = function() { + reject() + }; + + img.src = "data:image/webp;base64," + kTestImages[feature]; + }) +} + +/** Get the appropriate image source provided a source string and an optional image source array */ +export async function get_image_src(src: string, src_set?: ImageSource[]): Promise { + if (src_set === undefined || src_set.length <= 0) { + return src + } + + // Check if browser supports relevant webp features + let supports_webp = await (async (): Promise => { + try { + let supports_lossless = await check_webp_feature(WebpFeature.LOSSLESS) + let supports_lossy = await check_webp_feature(WebpFeature.LOSSY) + let supports_alpha = await check_webp_feature(WebpFeature.ALPHA) + + return (supports_lossless && supports_lossy && supports_alpha) + } catch (_) { + return false + } + })() + + // Split png and webp sources into seperate arrays + let png_set = src_set.filter(({ type }) => type === "image/png") + let webp_set = src_set.filter(({ type }) => type === "image/webp") + + // Tests if an ImageSource matches a media query + let match_src = (img_src: ImageSource): Boolean => { + if (img_src.media === undefined) return true + return window.matchMedia(img_src.media).matches + } + + + if ((!supports_webp || webp_set.length <= 0) && png_set.length > 0) { + // Use png srcSet + let result = png_set.find(match_src) + if (result !== undefined) return result.srcSet + + } else if (supports_webp && webp_set.length > 0) { + // Use webp srcSet + let result = webp_set.find(match_src) + if (result !== undefined) return result.srcSet + } + + // Use src if no image sources match + return src +} diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts new file mode 100644 index 0000000..384187b --- /dev/null +++ b/src/utils/webgl/webgl.ts @@ -0,0 +1,190 @@ +/** WebGL helper functions */ + +/** Get a webgl context for a canvas ref */ +export function get_gl_context( + canvas: React.RefObject +): WebGLRenderingContext { + if (canvas.current === null) throw "canvas element is null" + + const ctx = canvas.current.getContext("webgl") + + if (ctx === null) throw "failed to get webgl context" + + return ctx +} + +/** + * Creates a webgl shader from a source string + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html +*/ +export function create_shader( + gl: WebGLRenderingContext, + type: number, + source: string +): WebGLShader { + const shader = gl.createShader(type); + + if (shader === null) { + throw "failed to create shader" + } + + gl.shaderSource(shader, source) + gl.compileShader(shader) + + const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS) + if (!success) { + let err = gl.getShaderInfoLog(shader) + gl.deleteShader(shader) + throw err || "failed to compile shader" + } + + return shader; +} + +/** + * Creates a shader program from a vertex shader and a fragment shader + * Adapted from https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html + */ +export function create_program( + gl: WebGLRenderingContext, + vertex_shader: WebGLShader, + fragment_shader: WebGLShader +): WebGLProgram { + const program = gl.createProgram(); + + if (program === null) { + throw "failed to create shader program" + } + + gl.attachShader(program, vertex_shader) + gl.attachShader(program, fragment_shader) + gl.linkProgram(program) + + const success = gl.getProgramParameter(program, gl.LINK_STATUS) + if (!success) { + let err = gl.getProgramInfoLog(program) + gl.deleteProgram(program) + throw err || "failed to link shader program" + } + + return program; +} + +/** Creates a shader program from a vertex shader source string and a fragment shader source string */ +export function create_program_from_strings( + gl: WebGLRenderingContext, + vertex_shader_src: string, + fragment_shader_src: string +): WebGLProgram { + // Create shaders from sources + const vert_shader = create_shader(gl, gl.VERTEX_SHADER, vertex_shader_src) + const frag_shader = create_shader(gl, gl.FRAGMENT_SHADER, fragment_shader_src) + + // Create program from shaders + const program = create_program(gl, vert_shader, frag_shader) + + return program +} + +/** Bind and fill a buffer with position data for a rectangle */ +export function set_rect_vertices( + gl: WebGLRenderingContext, + buffer: WebGLBuffer, + x: number, + y: number, + width: number, + height: number +) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer) + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + x, y, // top left + x + width, y, // top right + x + width, y + height, // bottom right + x + width, y + height, // bottom right + x, y + height, // bottom left + x, y // top left + ]), + gl.STATIC_DRAW + ); +} + +/** Bind and fill a buffer with texture coordinates for a rectangle */ +export function set_rect_texcoords(gl: WebGLRenderingContext, buffer: WebGLBuffer) { + gl.bindBuffer(gl.ARRAY_BUFFER, buffer) + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 0, 0, // top left + 1, 0, // top right + 1, 1, // bottom right + 1, 1, // bottom right + 0, 1, // bottom left + 0, 0 // top left + ]), + gl.STATIC_DRAW) +} + +/** Loads an image from a src and binds it to a webgl texture */ +export function load_texture(gl: WebGLRenderingContext, src: string): Promise { + return new Promise((resolve, reject) => { + // Create image element + const image = document.createElement("img") + + // On image loaded + image.addEventListener("load", () => { + // Create texture. + const texture = gl.createTexture() + + if (texture === null) { + reject("Failed to create texture") + return + } + + gl.bindTexture(gl.TEXTURE_2D, texture) + + // Fill texture with a 1x1 blue pixel + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + 1, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + new Uint8Array([0, 0, 255, 255]) + ) + + gl.bindTexture(gl.TEXTURE_2D, texture) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) + + // Handle power of 2 images and non power of 2 images + if (is_power_of_2(image.width) && is_power_of_2(image.height)) { + // Generate mip maps + gl.generateMipmap(gl.TEXTURE_2D); + } else { + // Turn off mipmaps clamp image to edge + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + + resolve(texture) + }) + + // On image load failed + image.addEventListener("error", (e) => { + reject(e.message) + }) + + // Set image source + image.src = src + }) +} + +/** Is a number of 2 */ +function is_power_of_2(value: number): boolean { + return (value & (value - 1)) === 0; +} From 4696277941ff0fa2dc0ab32ae197ffec2841b9cd Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Fri, 21 Jun 2024 12:43:03 -0300 Subject: [PATCH 4/7] Added uniform props to image shader component --- src/components/common/ImageShader/index.tsx | 211 ++++++++++++------ .../home/sections/Work/WorkCard/index.tsx | 12 +- src/utils/webgl/webgl.ts | 140 +++++++++++- 3 files changed, 289 insertions(+), 74 deletions(-) diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index b131754..a3c40d5 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -1,6 +1,13 @@ -import { useEffect, useLayoutEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { ImageSource, get_image_src } from "../../../utils/webgl/images" -import { create_program_from_strings, get_gl_context, load_texture, set_rect_texcoords, set_rect_vertices } from "../../../utils/webgl/webgl" +import { + create_program_from_strings, + get_gl_context, + load_texture, + set_rect_texcoords, + set_rect_vertices, + UniformProp +} from "../../../utils/webgl/webgl" interface ImageProps { src: string, @@ -18,35 +25,17 @@ interface ImageShaderProps { height?: number, alt?: string, srcSet?: ImageSource[], + uniforms?: UniformProp | UniformProp[], + animate?: boolean, + frameRate?: number, className?: string, wrapperClassName?: string, } export default function ImageShader(props: ImageShaderProps) { - const vert_source = ` - attribute vec2 a_position; - attribute vec2 a_texcoord; - - uniform vec2 u_resolution; - - varying vec2 v_texcoord; - - void main() { - // convert the position from pixels to 0.0 to 1.0 - vec2 zeroToOne = a_position.xy / u_resolution; - - // convert from 0->1 to 0->2 - vec2 zeroToTwo = zeroToOne * 2.0; - - // convert from 0->2 to -1->+1 (clipspace) - vec2 clipSpace = zeroToTwo - 1.0; - - gl_Position = vec4(clipSpace, 0, 1); - v_texcoord = vec2(a_texcoord.x, 1. - a_texcoord.y); - }` - - let [gl, set_gl] = useState(null) // WebGL context + // WebGl Context + let [gl, set_gl] = useState(null) // Shader state let [shader_prog, set_shader_prog] = useState(null) // Shader program @@ -54,7 +43,6 @@ export default function ImageShader(props: ImageShaderProps) { // Attribute location state let [pos_location, set_pos_location] = useState(0) // Position attribute location let [tex_coord_location, set_tex_coord_location] = useState(0) // Texcoord attribute location - // let [matrix_location, set_matrix_location] = useState(null) // Matrix attriubute location // Uniform location state let [res_location, set_res_location] = useState(null) // Resolution uniform @@ -76,9 +64,91 @@ export default function ImageShader(props: ImageShaderProps) { // Canvas element reference let canvas_ref = useRef(null) + // Component wrapper element refrence + let wrapper_ref = useRef(null) + + const vert_source = ` + attribute vec2 a_position; + attribute vec2 a_texcoord; + + uniform vec2 u_resolution; + + varying vec2 v_texcoord; + + void main() { + // convert the position from pixels to 0.0 to 1.0 + vec2 zeroToOne = a_position.xy / u_resolution; + + // convert from 0->1 to 0->2 + vec2 zeroToTwo = zeroToOne * 2.0; + + // convert from 0->2 to -1->+1 (clipspace) + vec2 clipSpace = zeroToTwo - 1.0; + + gl_Position = vec4(clipSpace, 0, 1); + v_texcoord = vec2(a_texcoord.x, 1. - a_texcoord.y); + }` + + /** Set the locations for shader uniforms */ + let set_uniform_locations = ( + gl: WebGLRenderingContext, + program: WebGLProgram, + uniform_props?: UniformProp | UniformProp[] + ) => { + // Set resolution uniform location + const resolution_unif_location = gl.getUniformLocation(program, "u_resolution") + if (resolution_unif_location === null) throw "can't get u_resolution uniform location" + set_res_location(resolution_unif_location) + + // Set texture sampler uniform location + const sampler_unif_location = gl.getUniformLocation(program, "u_texture") + if (sampler_unif_location === null) throw "can't get u_texture uniform location" + set_sampler_location(sampler_unif_location) + + if (uniform_props === undefined) return + + // Set locations of component prop uniforms + if (uniform_props instanceof UniformProp) { + uniform_props.set_location(gl, program) + } else { + uniform_props.forEach((prop) => { + prop.set_location(gl, program) + }) + } + } + + /** Set the values for shader uniforms */ + let set_uniform_values = ( + gl: WebGLRenderingContext, + uniform_props?: UniformProp | UniformProp[] + ) => { + // Set resolution uniform value + gl.uniform2f(res_location, gl.canvas.width, gl.canvas.height) + + // Set sampler uniform value + gl.uniform1i(sampler_location, 0) + + if (uniform_props === undefined) return + + // Set uniform prop values + if (uniform_props instanceof UniformProp) { + uniform_props.set_value(gl) + } else { + uniform_props.forEach((prop) => { + prop.set_value(gl) + }) + } + } /** Initializes webgl and shaders */ - let init = async (canvas_ref: React.RefObject, vert_source: string, frag_source: string) => { + let init = async ( + canvas_ref: React.RefObject, + vert_source: string, + frag_source: string, + width?: number, + height?: number, + uniforms?: UniformProp | UniformProp[], + ) => { try { if (canvas_ref.current === null) throw "can't get canvas element" @@ -101,16 +171,10 @@ export default function ImageShader(props: ImageShaderProps) { set_tex_coord_location(ctx.getAttribLocation(program, "a_texcoord")) // - // Get uniform locations + // Set uniform locations // - const resolution_unif_location = ctx.getUniformLocation(program, "u_resolution") - if (resolution_unif_location === null) throw "can't get u_resolution uniform location" - set_res_location(resolution_unif_location) - - const sampler_unif_location = ctx.getUniformLocation(program, "u_texture") - if (sampler_unif_location === null) throw "can't get u_texture uniform location" - set_sampler_location(sampler_unif_location) + set_uniform_locations(ctx, program, uniforms) // // Bind buffers @@ -127,8 +191,8 @@ export default function ImageShader(props: ImageShaderProps) { position_buffer, 0, 0, - canvas_width || ctx.canvas.width, - canvas_height || ctx.canvas.height + width || ctx.canvas.width, + height || ctx.canvas.height ) // Bind texcoord buffer @@ -141,8 +205,7 @@ export default function ImageShader(props: ImageShaderProps) { // Load texture let img_src = await get_image_src(props.src, props.srcSet) - console.log(img_src) - let tex = await load_texture(ctx, img_src) + let { texture: tex, aspect_ratio } = await load_texture(ctx, img_src) set_texture(tex) } catch (err) { @@ -164,29 +227,30 @@ export default function ImageShader(props: ImageShaderProps) { /** Render webgl canvas */ let render = () => { + if (gl === null) return if (!initialized()) return - gl?.viewport(0, 0, gl.canvas.width, gl.canvas.height); + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // Clear the canvas - gl?.clearColor(0.1, 0, 0, 0); - gl?.clear(gl.COLOR_BUFFER_BIT); + gl.clearColor(0.1, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); // Use shader program - gl?.useProgram(shader_prog); + gl.useProgram(shader_prog); // // Position attribute // // Enable attribute - gl?.enableVertexAttribArray(pos_location); + gl.enableVertexAttribArray(pos_location); // Bind the position buffer. - gl?.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); + gl.bindBuffer(gl.ARRAY_BUFFER, pos_buffer); // Set position data pointer - gl?.vertexAttribPointer( + gl.vertexAttribPointer( pos_location, 2, // 2 components per iteration gl.FLOAT, // 32bit float data @@ -203,11 +267,11 @@ export default function ImageShader(props: ImageShaderProps) { gl?.enableVertexAttribArray(tex_coord_location); // Bind the texcoord buffer - gl?.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); + gl.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); // Set texcoord data pointer - gl?.vertexAttribPointer( + gl.vertexAttribPointer( tex_coord_location, 2, // 2 components per iteration gl.FLOAT, // 32bit float data @@ -221,20 +285,16 @@ export default function ImageShader(props: ImageShaderProps) { // // Use texture unit 0 - gl?.activeTexture(gl.TEXTURE0) + gl.activeTexture(gl.TEXTURE0) // Bind texture - gl?.bindTexture(gl.TEXTURE_2D, texture) + gl.bindTexture(gl.TEXTURE_2D, texture) // // Uniforms // - // Update resolution uniform - gl?.uniform2f(res_location, gl.canvas.width, gl.canvas.height) - - // Update sampler uniform - gl?.uniform1i(sampler_location, 0) + set_uniform_values(gl, props.uniforms) // // Draw @@ -244,11 +304,29 @@ export default function ImageShader(props: ImageShaderProps) { } /** Handles window resize event */ - let handle_resize = () => { - if (canvas_ref.current === null) return + let handle_resize = (): { width?: number, height?: number } => { + if (wrapper_ref.current === null) return {} + + let canvas_size = { + width: wrapper_ref.current.clientWidth, + height: wrapper_ref.current.clientHeight + } + + set_canvas_width(canvas_size.width) + set_canvas_height(canvas_size.height) + + return canvas_size + } - set_canvas_width(canvas_ref.current.clientWidth) - set_canvas_height(canvas_ref.current.clientHeight) + /** If canvas has a width and height, return it's aspect ratio */ + let canvas_aspect_ratio = (): number | undefined => { + let width = canvas_width || props.width + let height = canvas_height || props.height + + if (width === undefined) return + if (height === undefined) return + + return width / height } /** Image fallback component */ @@ -311,10 +389,6 @@ export default function ImageShader(props: ImageShaderProps) { // Reset position buffer and rerender when canvas dimensions change useEffect(() => { - if (!can_render) { - init(canvas_ref, vert_source, props.fragSource) - - } if (gl === null || pos_buffer === null) { return } @@ -339,8 +413,11 @@ export default function ImageShader(props: ImageShaderProps) { if (props.width !== undefined) set_canvas_width(props.width) if (props.height !== undefined) set_canvas_height(props.height) - window.addEventListener("resize", handle_resize) - window.requestAnimationFrame(() => handle_resize()) + window.requestAnimationFrame(() => { + window.addEventListener("resize", handle_resize) + let { width, height } = handle_resize() + init(canvas_ref, vert_source, props.fragSource, width, height, props.uniforms) + }) return () => { window.removeEventListener("resize", handle_resize) @@ -348,13 +425,13 @@ export default function ImageShader(props: ImageShaderProps) { }, []) return ( -
+
diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index 3265de4..add1174 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -6,6 +6,7 @@ import { Work } from "../../../../../types"; import SkillIcon from "../../../../common/SkillIcon"; import { Link } from "react-router-dom"; import ImageShader from "../../../../common/ImageShader"; +import { UniformProp } from "../../../../../utils/webgl/webgl"; interface WorkCardProps { work: Work; @@ -27,10 +28,11 @@ const WorkCard: FunctionComponent = (props) => { uniform sampler2D u_texture; + uniform float u_test; + void main() { - //vec4 tex_col = texture2D(u_texture, v_texcoord); - //gl_FragColor = vec4(tex_col.x + 1., 1., 1., 1.); - gl_FragColor = texture2D(u_texture, v_texcoord); + vec4 tex_col = texture2D(u_texture, v_texcoord); + gl_FragColor = tex_col + vec4(u_test, 0., 0., 0.); }` return ( @@ -66,8 +68,10 @@ const WorkCard: FunctionComponent = (props) => { type: "image/png" }, ]} - fragSource={frag_source} alt={`${props.work.title}`} + fragSource={frag_source} + uniforms={new UniformProp("u_test", 0)} + animate={true} wrapperClassName="w-full h-full object-cover group-hover/card:scale-105 transition duration-200" /> diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts index 384187b..8f5b00b 100644 --- a/src/utils/webgl/webgl.ts +++ b/src/utils/webgl/webgl.ts @@ -1,5 +1,136 @@ /** WebGL helper functions */ +/** Uniform prop class */ +export class UniformProp { + /** Uniform name */ + name: string + /** Determins if uniform is an int */ + is_int: Boolean = false + /** Location of uniform */ + location?: WebGLUniformLocation + + // Value(s) + x?: number + y?: number + z?: number + w?: number + v?: Float32List | Int32List | number[] + + constructor(name: string, x: number) + constructor(name: string, x: number, y: number) + constructor(name: string, x: number, y: number) + constructor(name: string, x: number, y: number, z: number) + constructor(name: string, x: number, y: number, z: number, w: number) + constructor(name: string, v: Float32List | Int32List | number[]) + constructor( + name: string, + x: number | Float32List | Int32List | number[], + y?: number, + z?: number, + w?: number, + ) { + this.name = name + + if (typeof x === 'number') { + this.x = x + this.y = y + this.z = z + this.w = w + } else { + this.v = x + } + } + + /** Returns self with is_int param set to value */ + with_is_int(value: boolean = true): UniformProp { + this.is_int = value + return this + } + + /** Sets uniform location in a program */ + set_location(gl: WebGLRenderingContext, program: WebGLProgram): void { + const location = gl.getUniformLocation(program, this.name) + if (location === null) throw `unable to get location for uniform ${this.name}` + this.location = location + } + + /** Set uniform to prop value */ + set_value(gl: WebGLRenderingContext): void { + console.log("Set value of " + this.name) + let location = this.location || null + let x = this.x + let y = this.y + let z = this.z + let w = this.w + let v = this.v + let is_int = this.is_int + + // Values supplied as iterable + if (v !== undefined) { + // Int values + if (v instanceof Int32Array) { + switch (v.length) { + case 1: + gl.uniform1iv(location, v) + break; + case 2: + gl.uniform2iv(location, v) + break; + case 3: + gl.uniform3iv(location, v) + break; + default: + gl.uniform4iv(location, v) + break; + } + + return + + // Float values + } else { + switch (v.length) { + case 1: + gl.uniform1fv(location, v) + break; + case 2: + gl.uniform2fv(location, v) + break; + case 3: + gl.uniform3fv(location, v) + break; + default: + gl.uniform4fv(location, v) + break; + } + + return + } + + // Values supplied as props + } else { + if (x !== undefined && y !== undefined && z !== undefined && w !== undefined) { + is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) + return + } + + if (x !== undefined && y !== undefined && z !== undefined) { + is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) + return + } + + if (x !== undefined && y !== undefined) { + is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) + return + } + + if (x !== undefined) { + is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) + return + } + } + } +} + /** Get a webgl context for a canvas ref */ export function get_gl_context( canvas: React.RefObject @@ -127,8 +258,11 @@ export function set_rect_texcoords(gl: WebGLRenderingContext, buffer: WebGLBuffe } /** Loads an image from a src and binds it to a webgl texture */ -export function load_texture(gl: WebGLRenderingContext, src: string): Promise { - return new Promise((resolve, reject) => { +export function load_texture( + gl: WebGLRenderingContext, + src: string +): Promise<{ texture: WebGLTexture, aspect_ratio: number }> { + return new Promise<{ texture: WebGLTexture, aspect_ratio: number }>((resolve, reject) => { // Create image element const image = document.createElement("img") @@ -171,7 +305,7 @@ export function load_texture(gl: WebGLRenderingContext, src: string): Promise Date: Fri, 21 Jun 2024 13:16:48 -0300 Subject: [PATCH 5/7] Added animated prop to image shader component --- src/components/common/ImageShader/index.tsx | 41 ++++++++++++++++--- .../home/sections/Work/WorkCard/index.tsx | 1 + src/utils/webgl/webgl.ts | 1 - 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index a3c40d5..054770d 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import { ImageSource, get_image_src } from "../../../utils/webgl/images" import { create_program_from_strings, @@ -62,6 +62,9 @@ export default function ImageShader(props: ImageShaderProps) { let [canvas_width, set_canvas_width] = useState(null) let [canvas_height, set_canvas_height] = useState(null) + // Animation interval id + let [anim_interval_id, set_anim_interval_id] = useState(null) + // Canvas element reference let canvas_ref = useRef(null) // Component wrapper element refrence @@ -329,6 +332,26 @@ export default function ImageShader(props: ImageShaderProps) { return width / height } + let start_animation = (frame_rate: number = 60): NodeJS.Timeout => { + // Calculate frame delay in milliseconds + let delay = Math.round((1 / frame_rate) * 1000) + + return setInterval(() => render(), delay) + } + + let set_animated = (animate: boolean, frame_rate?: number) => { + if (anim_interval_id !== null) { + clearInterval(anim_interval_id) + set_anim_interval_id(null) + } + + if (animate) { + const interval_id = start_animation(frame_rate) + set_anim_interval_id(interval_id) + } + + } + /** Image fallback component */ let Image = (props: ImageProps) => { if (props.srcSet === undefined) { @@ -385,6 +408,7 @@ export default function ImageShader(props: ImageShaderProps) { useEffect(() => { if (!can_render) return render() + set_animated(props.animate || false, props.frameRate) }, [can_render]) // Reset position buffer and rerender when canvas dimensions change @@ -408,19 +432,24 @@ export default function ImageShader(props: ImageShaderProps) { }, [canvas_width, canvas_height]) + // Start or stop animation interval for animated prop change + useEffect(() => { + set_animated(props.animate || false, props.frameRate) + }, [props.animate, props.frameRate]) + // Component did mount useEffect(() => { if (props.width !== undefined) set_canvas_width(props.width) if (props.height !== undefined) set_canvas_height(props.height) - window.requestAnimationFrame(() => { - window.addEventListener("resize", handle_resize) - let { width, height } = handle_resize() - init(canvas_ref, vert_source, props.fragSource, width, height, props.uniforms) - }) + window.addEventListener("resize", handle_resize) + let { width, height } = handle_resize() + init(canvas_ref, vert_source, props.fragSource, width, height, props.uniforms) return () => { window.removeEventListener("resize", handle_resize) + + if (anim_interval_id !== null) clearInterval(anim_interval_id) } }, []) diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index add1174..fbec11c 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -72,6 +72,7 @@ const WorkCard: FunctionComponent = (props) => { fragSource={frag_source} uniforms={new UniformProp("u_test", 0)} animate={true} + frameRate={24} wrapperClassName="w-full h-full object-cover group-hover/card:scale-105 transition duration-200" /> diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts index 8f5b00b..85b72c0 100644 --- a/src/utils/webgl/webgl.ts +++ b/src/utils/webgl/webgl.ts @@ -56,7 +56,6 @@ export class UniformProp { /** Set uniform to prop value */ set_value(gl: WebGLRenderingContext): void { - console.log("Set value of " + this.name) let location = this.location || null let x = this.x let y = this.y From 88965507c6fa22173e2308d8043eda06d228bd2a Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Tue, 25 Jun 2024 19:49:43 -0300 Subject: [PATCH 6/7] Implemented custom uniforms for shader image component --- src/components/common/ImageShader/index.tsx | 287 ++++++++-------- .../home/sections/Work/WorkCard/index.tsx | 55 ++- src/utils/hooks/useUniform.ts | 156 +++++++++ src/utils/webgl/webgl.ts | 318 ++++++++++-------- 4 files changed, 535 insertions(+), 281 deletions(-) create mode 100644 src/utils/hooks/useUniform.ts diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index 054770d..9ac6969 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -1,12 +1,13 @@ -import { useEffect, useRef, useState } from "react" +import { forwardRef, useEffect, useRef, useState } from "react" import { ImageSource, get_image_src } from "../../../utils/webgl/images" import { + UniformProp, create_program_from_strings, get_gl_context, + get_uniform_location, load_texture, - set_rect_texcoords, set_rect_vertices, - UniformProp + set_uniform_value, } from "../../../utils/webgl/webgl" interface ImageProps { @@ -25,15 +26,26 @@ interface ImageShaderProps { height?: number, alt?: string, srcSet?: ImageSource[], - uniforms?: UniformProp | UniformProp[], + customUniforms?: UniformProp | UniformProp[], animate?: boolean, frameRate?: number, className?: string, + onMouseOver?: React.MouseEventHandler, wrapperClassName?: string, } +interface CustomUniform { + name: string, + location: WebGLUniformLocation | null, + x: number, + y?: number, + z?: number, + w?: number, + is_int?: Boolean, +} + -export default function ImageShader(props: ImageShaderProps) { +const ImageShader = forwardRef((props, ref) => { // WebGl Context let [gl, set_gl] = useState(null) @@ -42,18 +54,19 @@ export default function ImageShader(props: ImageShaderProps) { // Attribute location state let [pos_location, set_pos_location] = useState(0) // Position attribute location - let [tex_coord_location, set_tex_coord_location] = useState(0) // Texcoord attribute location // Uniform location state let [res_location, set_res_location] = useState(null) // Resolution uniform let [sampler_location, set_sampler_location] = useState(null) // Sampler uniform + let [custom_uniforms, set_custom_uniforms] = useState([]) // Buffer state let [pos_buffer, set_pos_buffer] = useState(null) // Position buffer - let [tex_coord_buffer, set_tex_coord_buffer] = useState(null) // Texcoord buffer // Texture state let [texture, set_texture] = useState(null) + let [texture_width, set_texture_width] = useState(0) + let [texture_height, set_texture_height] = useState(0) // Renderer state let [can_render, set_can_render] = useState(false) // Enables webgl rendering @@ -64,6 +77,8 @@ export default function ImageShader(props: ImageShaderProps) { // Animation interval id let [anim_interval_id, set_anim_interval_id] = useState(null) + // Animataion tick + let [tick, set_tick] = useState(0) // Canvas element reference let canvas_ref = useRef(null) @@ -71,59 +86,47 @@ export default function ImageShader(props: ImageShaderProps) { let wrapper_ref = useRef(null) const vert_source = ` - attribute vec2 a_position; - attribute vec2 a_texcoord; - - uniform vec2 u_resolution; + attribute vec3 a_position; varying vec2 v_texcoord; void main() { - // convert the position from pixels to 0.0 to 1.0 - vec2 zeroToOne = a_position.xy / u_resolution; - - // convert from 0->1 to 0->2 - vec2 zeroToTwo = zeroToOne * 2.0; - - // convert from 0->2 to -1->+1 (clipspace) - vec2 clipSpace = zeroToTwo - 1.0; - - gl_Position = vec4(clipSpace, 0, 1); - v_texcoord = vec2(a_texcoord.x, 1. - a_texcoord.y); - }` + gl_Position = vec4(a_position, 1.); + } + ` /** Set the locations for shader uniforms */ let set_uniform_locations = ( gl: WebGLRenderingContext, program: WebGLProgram, - uniform_props?: UniformProp | UniformProp[] ) => { // Set resolution uniform location const resolution_unif_location = gl.getUniformLocation(program, "u_resolution") - if (resolution_unif_location === null) throw "can't get u_resolution uniform location" + if (resolution_unif_location === null) console.error("can't get u_resolution uniform location") set_res_location(resolution_unif_location) // Set texture sampler uniform location const sampler_unif_location = gl.getUniformLocation(program, "u_texture") - if (sampler_unif_location === null) throw "can't get u_texture uniform location" + if (sampler_unif_location === null) console.error("can't get u_texture uniform location") set_sampler_location(sampler_unif_location) + } - if (uniform_props === undefined) return + /** Sets the location for custom shader uniforms */ + let set_custom_uniform_locations = ( + gl: WebGLRenderingContext, + program: WebGLProgram, + ) => { + const updated_uniforms = custom_uniforms.map((uniform) => { + uniform.location = get_uniform_location(gl, program, uniform.name) + return uniform + }) - // Set locations of component prop uniforms - if (uniform_props instanceof UniformProp) { - uniform_props.set_location(gl, program) - } else { - uniform_props.forEach((prop) => { - prop.set_location(gl, program) - }) - } + set_custom_uniforms(updated_uniforms) } /** Set the values for shader uniforms */ let set_uniform_values = ( gl: WebGLRenderingContext, - uniform_props?: UniformProp | UniformProp[] ) => { // Set resolution uniform value gl.uniform2f(res_location, gl.canvas.width, gl.canvas.height) @@ -131,16 +134,49 @@ export default function ImageShader(props: ImageShaderProps) { // Set sampler uniform value gl.uniform1i(sampler_location, 0) - if (uniform_props === undefined) return - // Set uniform prop values - if (uniform_props instanceof UniformProp) { - uniform_props.set_value(gl) - } else { - uniform_props.forEach((prop) => { - prop.set_value(gl) - }) + custom_uniforms.forEach(({ location, x, y, z, w }) => { + set_uniform_value(gl, location, x, y, z, w) + }) + } + + let update_custom_uniforms = (uniforms?: UniformProp | UniformProp[]) => { + if (uniforms === undefined) { + set_custom_uniforms([]) + return + } + + if (!(uniforms instanceof Array)) { + uniforms = [uniforms] } + + let updated_uniforms: CustomUniform[] = [...custom_uniforms] + + uniforms.forEach(({ name, x, y, z, w, is_int }) => { + const u_idx = updated_uniforms.findIndex( + (u_uniform) => u_uniform.name === name + ) + + if (u_idx < 0) { + updated_uniforms.push({ + name, + x, + y, + z, + w, + is_int, + location: null + }) + } else { + updated_uniforms[u_idx].x = x + updated_uniforms[u_idx].y = y + updated_uniforms[u_idx].z = z + updated_uniforms[u_idx].w = w + updated_uniforms[u_idx].is_int = is_int + } + }) + + set_custom_uniforms(updated_uniforms) } /** Initializes webgl and shaders */ @@ -148,9 +184,6 @@ export default function ImageShader(props: ImageShaderProps) { canvas_ref: React.RefObject, vert_source: string, frag_source: string, - width?: number, - height?: number, - uniforms?: UniformProp | UniformProp[], ) => { try { if (canvas_ref.current === null) throw "can't get canvas element" @@ -170,14 +203,12 @@ export default function ImageShader(props: ImageShaderProps) { // Get position attribute location set_pos_location(ctx.getAttribLocation(program, "a_position")) - // Get texcoord attribute location - set_tex_coord_location(ctx.getAttribLocation(program, "a_texcoord")) - // // Set uniform locations // - set_uniform_locations(ctx, program, uniforms) + set_uniform_locations(ctx, program) + set_custom_uniform_locations(ctx, program) // // Bind buffers @@ -189,27 +220,16 @@ export default function ImageShader(props: ImageShaderProps) { set_pos_buffer(position_buffer) // Create verticeis - set_rect_vertices( - ctx, - position_buffer, - 0, - 0, - width || ctx.canvas.width, - height || ctx.canvas.height - ) - - // Bind texcoord buffer - var coord_buffer = ctx.createBuffer(); - if (coord_buffer === null) throw "can't create texcoord buffer" - set_tex_coord_buffer(coord_buffer) - - // Create texcoord - set_rect_texcoords(ctx, coord_buffer) + set_rect_vertices(ctx, position_buffer) // Load texture let img_src = await get_image_src(props.src, props.srcSet) - let { texture: tex, aspect_ratio } = await load_texture(ctx, img_src) + let { texture: tex, dimensions } = await load_texture(ctx, img_src) + + // Set texture state set_texture(tex) + set_texture_width(dimensions.width) + set_texture_height(dimensions.height) } catch (err) { console.error(err) @@ -221,8 +241,6 @@ export default function ImageShader(props: ImageShaderProps) { let initialized = () => { if (gl === null) return false if (shader_prog === null) return false - if (sampler_location === null) return false - if (tex_coord_buffer === null) return false if (pos_buffer === null) return false if (texture === null) return false return true @@ -255,28 +273,7 @@ export default function ImageShader(props: ImageShaderProps) { // Set position data pointer gl.vertexAttribPointer( pos_location, - 2, // 2 components per iteration - gl.FLOAT, // 32bit float data - false, // don't normalize data - 0, // move by size * sizeof(type) each iteration - 0 // offset - ) - - // - // Texcoord attribute - // - - // Enable attribute - gl?.enableVertexAttribArray(tex_coord_location); - - // Bind the texcoord buffer - gl.bindBuffer(gl.ARRAY_BUFFER, tex_coord_buffer); - - - // Set texcoord data pointer - gl.vertexAttribPointer( - tex_coord_location, - 2, // 2 components per iteration + 3, // 2 components per iteration gl.FLOAT, // 32bit float data false, // don't normalize data 0, // move by size * sizeof(type) each iteration @@ -297,13 +294,13 @@ export default function ImageShader(props: ImageShaderProps) { // Uniforms // - set_uniform_values(gl, props.uniforms) + set_uniform_values(gl) // // Draw // - gl?.drawArrays(gl.TRIANGLES, 0, 6); + gl?.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } /** Handles window resize event */ @@ -321,22 +318,10 @@ export default function ImageShader(props: ImageShaderProps) { return canvas_size } - /** If canvas has a width and height, return it's aspect ratio */ - let canvas_aspect_ratio = (): number | undefined => { - let width = canvas_width || props.width - let height = canvas_height || props.height - - if (width === undefined) return - if (height === undefined) return - - return width / height - } - let start_animation = (frame_rate: number = 60): NodeJS.Timeout => { // Calculate frame delay in milliseconds let delay = Math.round((1 / frame_rate) * 1000) - - return setInterval(() => render(), delay) + return setInterval(() => set_tick((t) => t + 1), delay) } let set_animated = (animate: boolean, frame_rate?: number) => { @@ -392,19 +377,47 @@ export default function ImageShader(props: ImageShaderProps) { // Hooks // + // Render when tick changes + useEffect(() => { + if (!can_render) return + render() + }, [tick]) + + // Update custom uniforms when props change + useEffect(() => { + update_custom_uniforms(props.customUniforms) + }, [props.customUniforms]) + + // Get any custom uniform locations that are null when custom uniforms changes + useEffect(() => { + if (gl === null || shader_prog === null) return + + let updated = false + const updated_uniforms = custom_uniforms.map((uniform) => { + if (uniform.location === null && gl !== null && shader_prog !== null) { + uniform.location = get_uniform_location(gl, shader_prog, uniform.name) + updated = true + } + + return uniform + }) + + if (updated) set_custom_uniforms(updated_uniforms) + }, [custom_uniforms, gl, shader_prog]) + + // Updated can render state useEffect(() => { set_can_render(initialized()) }, [ gl, shader_prog, - sampler_location, pos_location, pos_buffer, texture ]) - // Rerender when canvas state changes + // Rerender when can_render state changes useEffect(() => { if (!can_render) return render() @@ -418,14 +431,7 @@ export default function ImageShader(props: ImageShaderProps) { } // Rebind position buffer to canvas dimensions - set_rect_vertices( - gl, - pos_buffer, - 0, - 0, - canvas_width || gl.canvas.width, - canvas_height || gl.canvas.height - ) + set_rect_vertices(gl, pos_buffer) // rerender render() @@ -443,8 +449,8 @@ export default function ImageShader(props: ImageShaderProps) { if (props.height !== undefined) set_canvas_height(props.height) window.addEventListener("resize", handle_resize) - let { width, height } = handle_resize() - init(canvas_ref, vert_source, props.fragSource, width, height, props.uniforms) + handle_resize() + init(canvas_ref, vert_source, props.fragSource) return () => { window.removeEventListener("resize", handle_resize) @@ -454,24 +460,29 @@ export default function ImageShader(props: ImageShaderProps) { }, []) return ( -
- - - - {props.alt} +
+
+ + + + {props.alt} +
) -} +}) + +export default ImageShader diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index fbec11c..a1f0fae 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from "react"; +import { FunctionComponent, useEffect, useRef, useState } from "react"; import { addLeadingZeros } from "../../../../../utils/numberFormatting"; @@ -6,7 +6,7 @@ import { Work } from "../../../../../types"; import SkillIcon from "../../../../common/SkillIcon"; import { Link } from "react-router-dom"; import ImageShader from "../../../../common/ImageShader"; -import { UniformProp } from "../../../../../utils/webgl/webgl"; +// import { useUniform } from "../../../../../utils/hooks/useUniform"; interface WorkCardProps { work: Work; @@ -15,26 +15,62 @@ interface WorkCardProps { /** Work section work card component */ const WorkCard: FunctionComponent = (props) => { + const [mouse_x, set_mouse_x] = useState(0) + const [mouse_y, set_mouse_y] = useState(0) + + const image_shader_ref = useRef(null) + const image_flip_classes = props.flipped ? "col-start-2 col-end-4" : "col-span-2"; const work_link = (id: string) => `/work/${id.toLowerCase()}`; - const frag_source = ` - precision mediump float; + const handle_mouse_move = (event: MouseEvent) => { + if (image_shader_ref.current === null) { + set_mouse_x(0) + set_mouse_y(0) + return + } - varying highp vec2 v_texcoord; + const image_rect = image_shader_ref.current.getBoundingClientRect() + const x = event.clientX - image_rect.left + const y = event.clientY - image_rect.top + + set_mouse_x(x) + set_mouse_y(y) + } + + const frag_source = ` + precision highp float; + + uniform vec2 u_resolution; uniform sampler2D u_texture; - uniform float u_test; + uniform vec2 u_mouseCoord; void main() { - vec4 tex_col = texture2D(u_texture, v_texcoord); - gl_FragColor = tex_col + vec4(u_test, 0., 0., 0.); + vec2 uv = gl_FragCoord.xy / u_resolution; + vec2 muv = u_mouseCoord / u_resolution; + muv.x = muv.x * -1.; + // uv = uv + muv; + + vec4 tex_col = texture2D(u_texture, vec2(uv.x, 1. - uv.y)); + + gl_FragColor = tex_col; + // gl_FragColor = vec4(uv.xy, 1., 1.); }` + // Component did mount + useEffect(() => { + window.addEventListener("mousemove", handle_mouse_move) + + return () => { + window.removeEventListener("mousemove", handle_mouse_move) + } + }, []) + return (
= (props) => { className="block aspect-[16/9] xl:aspect-auto w-full h-full overflow-hidden border border-brand-gray-400 rounded" > = (props) => { ]} alt={`${props.work.title}`} fragSource={frag_source} - uniforms={new UniformProp("u_test", 0)} + // customUniforms={{ name: "u_mouseCoord", x: mouse_x, y: mouse_y }} animate={true} frameRate={24} wrapperClassName="w-full h-full object-cover group-hover/card:scale-105 transition duration-200" diff --git a/src/utils/hooks/useUniform.ts b/src/utils/hooks/useUniform.ts new file mode 100644 index 0000000..238e69d --- /dev/null +++ b/src/utils/hooks/useUniform.ts @@ -0,0 +1,156 @@ +import { useCallback, useState } from "react" + +export type UseUniformHook = { + /** Sets values of the uniform */ + setValues: UniformValueSetter + /** Sets location state to uniforms state in a program */ + setLocation: UniformLocationSetter, + /** Sets uniform value to set values */ + setUniform: UniformSetter, +} + +type UniformLocationSetter = (gl: WebGLRenderingContext, program: WebGLProgram) => void; +type UniformSetter = (gl: WebGLRenderingContext) => void; +type UniformValueSetter = { + (x: number): void, + (x: number, y: number): void, + (x: number, y: number, z: number): void, + (x: number, y: number, z: number, w: number): void, + (v: Float32List | Int32List | number[]): void, +} + +/** WebGL uniform helper hook */ +export function useUniform(name: string, x: number): UseUniformHook +export function useUniform(name: string, x: number, y: number): UseUniformHook +export function useUniform(name: string, x: number, y: number): UseUniformHook +export function useUniform(name: string, x: number, y: number, z: number): UseUniformHook +export function useUniform(name: string, x: number, y: number, z: number, w: number): UseUniformHook +export function useUniform(name: string, v: Float32List | Int32List | number[]): UseUniformHook +export function useUniform( + name: string, + x: number | Float32List | Int32List | number[], + y?: number, + z?: number, + w?: number, +): UseUniformHook { + // Uniform name + const [u_name, set_u_name] = useState(name) + // Determins whether uniform is an int + const [is_int, set_is_int] = useState(false) + // Location of uniform + const [u_location, set_u_location] = useState(null) + + // Value(s) + const [u_x, set_u_x] = useState(typeof x === "number" ? x : undefined) + const [u_y, set_u_y] = useState(y) + const [u_z, set_u_z] = useState(z) + const [u_w, set_u_w] = useState(w) + const [u_v, set_u_v] = useState( + typeof x === "number" ? undefined : x + ) + + const setValues: UniformValueSetter = ( + x: number | Float32List | Int32List | number[], + y?: number, + z?: number, + w?: number, + ) => { + if (typeof x !== "number") { + set_u_v(x) + return + } + + set_u_x(x) + set_u_y(y) + set_u_z(z) + set_u_w(w) + } + + const setLocation: UniformLocationSetter = (gl, program) => { + const location = gl.getUniformLocation(program, u_name) + + if (location === null) { + set_u_location(null) + throw `unable to get location for uniform ${u_name}` + } + + set_u_location(location) + } + + /** Set uniform in a webgl rendering context to values */ + const setUniform: UniformSetter = useCallback((gl) => { + console.log(u_x, u_y) + // Values supplied as iterable + if (u_v !== undefined) { + // Int values + if (u_v instanceof Int32Array) { + switch (u_v.length) { + case 1: + gl.uniform1iv(u_location, u_v) + break; + case 2: + gl.uniform2iv(u_location, u_v) + break; + case 3: + gl.uniform3iv(u_location, u_v) + break; + default: + gl.uniform4iv(u_location, u_v) + break; + } + + return + + // Float values + } else { + switch (u_v.length) { + case 1: + gl.uniform1fv(u_location, u_v) + break; + case 2: + gl.uniform2fv(u_location, u_v) + break; + case 3: + gl.uniform3fv(u_location, u_v) + break; + default: + gl.uniform4fv(u_location, u_v) + break; + } + + return + } + + } else { + if (u_x !== undefined && u_y !== undefined && u_z !== undefined && u_w !== undefined) { + is_int ? + gl.uniform4i(u_location, u_x, u_y, u_z, u_w) : + gl.uniform4f(u_location, u_x, u_y, u_z, u_w) + return + } + + if (u_x !== undefined && u_y !== undefined && u_z !== undefined) { + is_int ? + gl.uniform3i(u_location, u_x, u_y, u_z) : + gl.uniform3f(u_location, u_x, u_y, u_z) + return + } + + if (u_x !== undefined && u_y !== undefined) { + is_int ? gl.uniform2i(u_location, u_x, u_y) : gl.uniform2f(u_location, u_x, u_y) + return + } + + if (u_x !== undefined) { + is_int ? gl.uniform1i(u_location, u_x) : gl.uniform1f(u_location, u_x) + return + } + } + }, [u_location, u_x, u_y, u_z, u_w, u_v]) + + return { + setValues, + setLocation, + setUniform, + } +} diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts index 85b72c0..71e6555 100644 --- a/src/utils/webgl/webgl.ts +++ b/src/utils/webgl/webgl.ts @@ -1,134 +1,183 @@ /** WebGL helper functions */ -/** Uniform prop class */ -export class UniformProp { - /** Uniform name */ - name: string - /** Determins if uniform is an int */ - is_int: Boolean = false - /** Location of uniform */ - location?: WebGLUniformLocation - - // Value(s) - x?: number - y?: number - z?: number - w?: number - v?: Float32List | Int32List | number[] - - constructor(name: string, x: number) - constructor(name: string, x: number, y: number) - constructor(name: string, x: number, y: number) - constructor(name: string, x: number, y: number, z: number) - constructor(name: string, x: number, y: number, z: number, w: number) - constructor(name: string, v: Float32List | Int32List | number[]) - constructor( - name: string, - x: number | Float32List | Int32List | number[], - y?: number, - z?: number, - w?: number, - ) { - this.name = name - - if (typeof x === 'number') { - this.x = x - this.y = y - this.z = z - this.w = w - } else { - this.v = x - } - } +export interface UniformProp { + name: string, + x: number, + y?: number, + z?: number, + w?: number, + is_int?: Boolean, +} - /** Returns self with is_int param set to value */ - with_is_int(value: boolean = true): UniformProp { - this.is_int = value - return this - } +/** Get the location of a WebGL uniform in a webgl program by name */ +export function get_uniform_location( + gl: WebGLRenderingContext, + program: WebGLProgram, + name: string): WebGLUniformLocation | null { + const location = gl.getUniformLocation(program, name) + if (location === null) console.error(`unable to get location for uniform ${name}`) + return location +} - /** Sets uniform location in a program */ - set_location(gl: WebGLRenderingContext, program: WebGLProgram): void { - const location = gl.getUniformLocation(program, this.name) - if (location === null) throw `unable to get location for uniform ${this.name}` - this.location = location +/** Set a WebGL uniforms value at a uniform location */ +export function set_uniform_value( + gl: WebGLRenderingContext, + location: WebGLUniformLocation | null, + x: number, + y?: number, + z?: number, + w?: number, + is_int: Boolean = false, +): void { + if (y !== undefined && z !== undefined && w !== undefined) { + is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) + return } - /** Set uniform to prop value */ - set_value(gl: WebGLRenderingContext): void { - let location = this.location || null - let x = this.x - let y = this.y - let z = this.z - let w = this.w - let v = this.v - let is_int = this.is_int - - // Values supplied as iterable - if (v !== undefined) { - // Int values - if (v instanceof Int32Array) { - switch (v.length) { - case 1: - gl.uniform1iv(location, v) - break; - case 2: - gl.uniform2iv(location, v) - break; - case 3: - gl.uniform3iv(location, v) - break; - default: - gl.uniform4iv(location, v) - break; - } - - return - - // Float values - } else { - switch (v.length) { - case 1: - gl.uniform1fv(location, v) - break; - case 2: - gl.uniform2fv(location, v) - break; - case 3: - gl.uniform3fv(location, v) - break; - default: - gl.uniform4fv(location, v) - break; - } - - return - } + if (y !== undefined && z !== undefined) { + is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) + return + } - // Values supplied as props - } else { - if (x !== undefined && y !== undefined && z !== undefined && w !== undefined) { - is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) - return - } + if (y !== undefined) { + is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) + return + } - if (x !== undefined && y !== undefined && z !== undefined) { - is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) - return - } + is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) + return +} - if (x !== undefined && y !== undefined) { - is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) - return - } - if (x !== undefined) { - is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) - return - } - } - } -} +// /** Uniform prop class */ +// export class UniformProp { +// /** Uniform name */ +// name: string +// /** Determins if uniform is an int */ +// is_int: Boolean = false +// /** Location of uniform */ +// location?: WebGLUniformLocation +// +// // Value(s) +// x?: number +// y?: number +// z?: number +// w?: number +// v?: Float32List | Int32List | number[] +// +// constructor(name: string, x: number) +// constructor(name: string, x: number, y: number) +// constructor(name: string, x: number, y: number) +// constructor(name: string, x: number, y: number, z: number) +// constructor(name: string, x: number, y: number, z: number, w: number) +// constructor(name: string, v: Float32List | Int32List | number[]) +// constructor( +// name: string, +// x: number | Float32List | Int32List | number[], +// y?: number, +// z?: number, +// w?: number, +// ) { +// this.name = name +// +// if (typeof x === 'number') { +// this.x = x +// this.y = y +// this.z = z +// this.w = w +// } else { +// this.v = x +// } +// } +// +// /** Returns self with is_int param set to value */ +// with_is_int(value: boolean = true): UniformProp { +// this.is_int = value +// return this +// } +// +// /** Sets uniform location in a program */ +// set_location(gl: WebGLRenderingContext, program: WebGLProgram): void { +// const location = gl.getUniformLocation(program, this.name) +// if (location === null) throw `unable to get location for uniform ${this.name}` +// this.location = location +// } +// +// /** Set uniform to prop value */ +// set_value(gl: WebGLRenderingContext): void { +// let location = this.location || null +// let x = this.x +// let y = this.y +// let z = this.z +// let w = this.w +// let v = this.v +// let is_int = this.is_int +// +// // Values supplied as iterable +// if (v !== undefined) { +// // Int values +// if (v instanceof Int32Array) { +// switch (v.length) { +// case 1: +// gl.uniform1iv(location, v) +// break; +// case 2: +// gl.uniform2iv(location, v) +// break; +// case 3: +// gl.uniform3iv(location, v) +// break; +// default: +// gl.uniform4iv(location, v) +// break; +// } +// +// return +// +// // Float values +// } else { +// switch (v.length) { +// case 1: +// gl.uniform1fv(location, v) +// break; +// case 2: +// gl.uniform2fv(location, v) +// break; +// case 3: +// gl.uniform3fv(location, v) +// break; +// default: +// gl.uniform4fv(location, v) +// break; +// } +// +// return +// } +// +// // Values supplied as props +// } else { +// if (x !== undefined && y !== undefined && z !== undefined && w !== undefined) { +// is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) +// return +// } +// +// if (x !== undefined && y !== undefined && z !== undefined) { +// is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) +// return +// } +// +// if (x !== undefined && y !== undefined) { +// is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) +// return +// } +// +// if (x !== undefined) { +// is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) +// return +// } +// } +// } +// } /** Get a webgl context for a canvas ref */ export function get_gl_context( @@ -220,21 +269,17 @@ export function create_program_from_strings( export function set_rect_vertices( gl: WebGLRenderingContext, buffer: WebGLBuffer, - x: number, - y: number, - width: number, - height: number ) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer) gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ - x, y, // top left - x + width, y, // top right - x + width, y + height, // bottom right - x + width, y + height, // bottom right - x, y + height, // bottom left - x, y // top left + 1.0, 1.0, + 0.0, -1.0, + 1.0, 0.0, + 1.0, -1.0, + 0.0, -1.0, + -1.0, 0.0 ]), gl.STATIC_DRAW ); @@ -256,12 +301,17 @@ export function set_rect_texcoords(gl: WebGLRenderingContext, buffer: WebGLBuffe gl.STATIC_DRAW) } +type TextureData = { + texture: WebGLTexture, + dimensions: { width: number, height: number } +} + /** Loads an image from a src and binds it to a webgl texture */ export function load_texture( gl: WebGLRenderingContext, src: string -): Promise<{ texture: WebGLTexture, aspect_ratio: number }> { - return new Promise<{ texture: WebGLTexture, aspect_ratio: number }>((resolve, reject) => { +): Promise { + return new Promise((resolve, reject) => { // Create image element const image = document.createElement("img") @@ -304,7 +354,7 @@ export function load_texture( gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); } - resolve({ texture, aspect_ratio: image.width / image.height }) + resolve({ texture, dimensions: { width: image.width, height: image.height } }) }) // On image load failed From 1d4956811eaef4c02e3d9e63ad8b6fb9773e318b Mon Sep 17 00:00:00 2001 From: maxcaplan Date: Wed, 26 Jun 2024 16:32:55 -0300 Subject: [PATCH 7/7] Implemented image shader effect for work cards --- src/components/common/ImageShader/index.tsx | 104 ++++---------- .../home/sections/Work/WorkCard/frag.glsl | 130 +++++++++++++++++ .../home/sections/Work/WorkCard/index.tsx | 20 +-- src/utils/webgl/webgl.ts | 135 +----------------- 4 files changed, 173 insertions(+), 216 deletions(-) create mode 100644 src/components/home/sections/Work/WorkCard/frag.glsl diff --git a/src/components/common/ImageShader/index.tsx b/src/components/common/ImageShader/index.tsx index 9ac6969..9f4ebf7 100644 --- a/src/components/common/ImageShader/index.tsx +++ b/src/components/common/ImageShader/index.tsx @@ -26,7 +26,7 @@ interface ImageShaderProps { height?: number, alt?: string, srcSet?: ImageSource[], - customUniforms?: UniformProp | UniformProp[], + customUniforms?: UniformProp[], animate?: boolean, frameRate?: number, className?: string, @@ -34,14 +34,9 @@ interface ImageShaderProps { wrapperClassName?: string, } -interface CustomUniform { +interface CustomUniformLocation { name: string, location: WebGLUniformLocation | null, - x: number, - y?: number, - z?: number, - w?: number, - is_int?: Boolean, } @@ -58,7 +53,8 @@ const ImageShader = forwardRef((props, ref) => // Uniform location state let [res_location, set_res_location] = useState(null) // Resolution uniform let [sampler_location, set_sampler_location] = useState(null) // Sampler uniform - let [custom_uniforms, set_custom_uniforms] = useState([]) + let [time_location, set_time_location] = useState(null) // Time uniform + let [custom_locations, set_custom_locations] = useState([]) // Custom uniforms // Buffer state let [pos_buffer, set_pos_buffer] = useState(null) // Position buffer @@ -102,13 +98,15 @@ const ImageShader = forwardRef((props, ref) => ) => { // Set resolution uniform location const resolution_unif_location = gl.getUniformLocation(program, "u_resolution") - if (resolution_unif_location === null) console.error("can't get u_resolution uniform location") set_res_location(resolution_unif_location) // Set texture sampler uniform location const sampler_unif_location = gl.getUniformLocation(program, "u_texture") - if (sampler_unif_location === null) console.error("can't get u_texture uniform location") set_sampler_location(sampler_unif_location) + + // Set time uniform location + const time_unif_location = gl.getUniformLocation(program, "u_time") + set_time_location(time_unif_location) } /** Sets the location for custom shader uniforms */ @@ -116,12 +114,22 @@ const ImageShader = forwardRef((props, ref) => gl: WebGLRenderingContext, program: WebGLProgram, ) => { - const updated_uniforms = custom_uniforms.map((uniform) => { - uniform.location = get_uniform_location(gl, program, uniform.name) - return uniform + if (props.customUniforms === undefined) return + + let update_custom_locations = [...custom_locations] + + props.customUniforms.forEach(({ name: u_name }) => { + const lidx = custom_locations.findIndex(({ name: l_name }) => l_name === u_name) + + if (lidx < 0) { + update_custom_locations.push({ + name: u_name, + location: gl.getUniformLocation(program, u_name) + }) + } }) - set_custom_uniforms(updated_uniforms) + set_custom_locations(update_custom_locations) } /** Set the values for shader uniforms */ @@ -134,49 +142,20 @@ const ImageShader = forwardRef((props, ref) => // Set sampler uniform value gl.uniform1i(sampler_location, 0) - // Set uniform prop values - custom_uniforms.forEach(({ location, x, y, z, w }) => { - set_uniform_value(gl, location, x, y, z, w) - }) - } + // Set time uniform value + gl.uniform1f(time_location, tick) - let update_custom_uniforms = (uniforms?: UniformProp | UniformProp[]) => { - if (uniforms === undefined) { - set_custom_uniforms([]) - return - } + // Set uniform prop values - if (!(uniforms instanceof Array)) { - uniforms = [uniforms] - } + if (props.customUniforms === undefined) return - let updated_uniforms: CustomUniform[] = [...custom_uniforms] + props.customUniforms.forEach(({ name: u_name, x, y, z, w, is_int }) => { + const lidx = custom_locations.findIndex(({ name: l_name }) => l_name === u_name) - uniforms.forEach(({ name, x, y, z, w, is_int }) => { - const u_idx = updated_uniforms.findIndex( - (u_uniform) => u_uniform.name === name - ) - - if (u_idx < 0) { - updated_uniforms.push({ - name, - x, - y, - z, - w, - is_int, - location: null - }) - } else { - updated_uniforms[u_idx].x = x - updated_uniforms[u_idx].y = y - updated_uniforms[u_idx].z = z - updated_uniforms[u_idx].w = w - updated_uniforms[u_idx].is_int = is_int + if (lidx >= 0) { + set_uniform_value(gl, custom_locations[lidx].location, x, y, z, w, is_int) } }) - - set_custom_uniforms(updated_uniforms) } /** Initializes webgl and shaders */ @@ -383,29 +362,6 @@ const ImageShader = forwardRef((props, ref) => render() }, [tick]) - // Update custom uniforms when props change - useEffect(() => { - update_custom_uniforms(props.customUniforms) - }, [props.customUniforms]) - - // Get any custom uniform locations that are null when custom uniforms changes - useEffect(() => { - if (gl === null || shader_prog === null) return - - let updated = false - const updated_uniforms = custom_uniforms.map((uniform) => { - if (uniform.location === null && gl !== null && shader_prog !== null) { - uniform.location = get_uniform_location(gl, shader_prog, uniform.name) - updated = true - } - - return uniform - }) - - if (updated) set_custom_uniforms(updated_uniforms) - }, [custom_uniforms, gl, shader_prog]) - - // Updated can render state useEffect(() => { set_can_render(initialized()) diff --git a/src/components/home/sections/Work/WorkCard/frag.glsl b/src/components/home/sections/Work/WorkCard/frag.glsl new file mode 100644 index 0000000..e1acbdf --- /dev/null +++ b/src/components/home/sections/Work/WorkCard/frag.glsl @@ -0,0 +1,130 @@ +precision highp float; + +uniform vec2 u_resolution; +uniform sampler2D u_texture; + +uniform float u_time; +uniform vec2 u_mouseCoord; + +// Simplex 3D Noise +// by Ian McEwan, Stefan Gustavson (https://github.com/stegu/webgl-noise) +vec4 permute(vec4 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0 / 7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), + dot(p2, x2), dot(p3, x3))); +} + +float fractal_snoise(vec3 uv, float scale) { + float oct1 = snoise(uv * scale); + float oct2 = snoise(uv * 2. * scale); + float oct3 = snoise(uv * 4. * scale); + float oct4 = snoise(uv * 8. * scale); + + return clamp(oct1 + oct2 / 2. + oct3 / 4. + oct4 / 8., 0., 1.); +} + +vec2 offset_by_dir(vec2 v, vec2 dir, float value) { + v = v - dir * value; + v.x = clamp(v.x, 0., 1.); + v.y = clamp(v.y, 0., 1.); + return v; +} + +void main() +{ + // Noramlize frag coords from 0. to 1. + vec2 uv = gl_FragCoord.xy / u_resolution.xy; + uv.y = 1. - uv.y; // Flip coords verticaly + + // Get distance from mouse coords + vec2 uv2 = (vec2(gl_FragCoord.x, 1. - gl_FragCoord.y) - 0.5 * vec2(u_resolution.x, 1. - u_resolution.y)) / u_resolution.y; + vec2 center = (u_mouseCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; + float dist = length(center - uv2) - 0.; + + // Get texture uv offset + float height = fractal_snoise(vec3(uv, u_time * 0.01) + vec3(center, 0.), 1.); + float value = clamp(height * (1. - dist * 2.), 0., 2.); + vec2 dir = vec2(0.5 - uv.x, 0.5 - uv.y); + + // Offset texture uvs by different amounts for each color channel + vec2 tuvr = offset_by_dir(uv, dir, value * 0.33); + vec2 tuvg = offset_by_dir(uv, dir, value * 0.66); + vec2 tuvb = offset_by_dir(uv, dir, value); + + // Sample texture + float texr = texture2D(u_texture, tuvr).r; + float texg = texture2D(u_texture, tuvg).g; + float texb = texture2D(u_texture, tuvb).b; + + gl_FragColor = vec4(texr, texg, texb, 1.); +} diff --git a/src/components/home/sections/Work/WorkCard/index.tsx b/src/components/home/sections/Work/WorkCard/index.tsx index a1f0fae..2e9432e 100644 --- a/src/components/home/sections/Work/WorkCard/index.tsx +++ b/src/components/home/sections/Work/WorkCard/index.tsx @@ -6,7 +6,8 @@ import { Work } from "../../../../../types"; import SkillIcon from "../../../../common/SkillIcon"; import { Link } from "react-router-dom"; import ImageShader from "../../../../common/ImageShader"; -// import { useUniform } from "../../../../../utils/hooks/useUniform"; + +import frag_source from "./frag.glsl?raw" interface WorkCardProps { work: Work; @@ -15,8 +16,8 @@ interface WorkCardProps { /** Work section work card component */ const WorkCard: FunctionComponent = (props) => { - const [mouse_x, set_mouse_x] = useState(0) - const [mouse_y, set_mouse_y] = useState(0) + const [mouse_x, set_mouse_x] = useState(-1000) + const [mouse_y, set_mouse_y] = useState(-1000) const image_shader_ref = useRef(null) @@ -42,12 +43,13 @@ const WorkCard: FunctionComponent = (props) => { set_mouse_y(y) } - const frag_source = ` + const frag_source_old = ` precision highp float; uniform vec2 u_resolution; uniform sampler2D u_texture; + uniform float u_time; uniform vec2 u_mouseCoord; void main() { @@ -57,9 +59,11 @@ const WorkCard: FunctionComponent = (props) => { // uv = uv + muv; vec4 tex_col = texture2D(u_texture, vec2(uv.x, 1. - uv.y)); - - gl_FragColor = tex_col; - // gl_FragColor = vec4(uv.xy, 1., 1.); + + vec3 col = 0.5 + 0.5*cos(u_time * 0.01 + uv.xyx + vec3(0,2,4)); + + // gl_FragColor = tex_col; + gl_FragColor = vec4(col, 1.); }` // Component did mount @@ -107,7 +111,7 @@ const WorkCard: FunctionComponent = (props) => { ]} alt={`${props.work.title}`} fragSource={frag_source} - // customUniforms={{ name: "u_mouseCoord", x: mouse_x, y: mouse_y }} + customUniforms={[{ name: "u_mouseCoord", x: mouse_x, y: mouse_y }]} animate={true} frameRate={24} wrapperClassName="w-full h-full object-cover group-hover/card:scale-105 transition duration-200" diff --git a/src/utils/webgl/webgl.ts b/src/utils/webgl/webgl.ts index 71e6555..2316909 100644 --- a/src/utils/webgl/webgl.ts +++ b/src/utils/webgl/webgl.ts @@ -14,9 +14,7 @@ export function get_uniform_location( gl: WebGLRenderingContext, program: WebGLProgram, name: string): WebGLUniformLocation | null { - const location = gl.getUniformLocation(program, name) - if (location === null) console.error(`unable to get location for uniform ${name}`) - return location + return gl.getUniformLocation(program, name) } /** Set a WebGL uniforms value at a uniform location */ @@ -48,137 +46,6 @@ export function set_uniform_value( return } - -// /** Uniform prop class */ -// export class UniformProp { -// /** Uniform name */ -// name: string -// /** Determins if uniform is an int */ -// is_int: Boolean = false -// /** Location of uniform */ -// location?: WebGLUniformLocation -// -// // Value(s) -// x?: number -// y?: number -// z?: number -// w?: number -// v?: Float32List | Int32List | number[] -// -// constructor(name: string, x: number) -// constructor(name: string, x: number, y: number) -// constructor(name: string, x: number, y: number) -// constructor(name: string, x: number, y: number, z: number) -// constructor(name: string, x: number, y: number, z: number, w: number) -// constructor(name: string, v: Float32List | Int32List | number[]) -// constructor( -// name: string, -// x: number | Float32List | Int32List | number[], -// y?: number, -// z?: number, -// w?: number, -// ) { -// this.name = name -// -// if (typeof x === 'number') { -// this.x = x -// this.y = y -// this.z = z -// this.w = w -// } else { -// this.v = x -// } -// } -// -// /** Returns self with is_int param set to value */ -// with_is_int(value: boolean = true): UniformProp { -// this.is_int = value -// return this -// } -// -// /** Sets uniform location in a program */ -// set_location(gl: WebGLRenderingContext, program: WebGLProgram): void { -// const location = gl.getUniformLocation(program, this.name) -// if (location === null) throw `unable to get location for uniform ${this.name}` -// this.location = location -// } -// -// /** Set uniform to prop value */ -// set_value(gl: WebGLRenderingContext): void { -// let location = this.location || null -// let x = this.x -// let y = this.y -// let z = this.z -// let w = this.w -// let v = this.v -// let is_int = this.is_int -// -// // Values supplied as iterable -// if (v !== undefined) { -// // Int values -// if (v instanceof Int32Array) { -// switch (v.length) { -// case 1: -// gl.uniform1iv(location, v) -// break; -// case 2: -// gl.uniform2iv(location, v) -// break; -// case 3: -// gl.uniform3iv(location, v) -// break; -// default: -// gl.uniform4iv(location, v) -// break; -// } -// -// return -// -// // Float values -// } else { -// switch (v.length) { -// case 1: -// gl.uniform1fv(location, v) -// break; -// case 2: -// gl.uniform2fv(location, v) -// break; -// case 3: -// gl.uniform3fv(location, v) -// break; -// default: -// gl.uniform4fv(location, v) -// break; -// } -// -// return -// } -// -// // Values supplied as props -// } else { -// if (x !== undefined && y !== undefined && z !== undefined && w !== undefined) { -// is_int ? gl.uniform4i(location, x, y, z, w) : gl.uniform4f(location, x, y, z, w) -// return -// } -// -// if (x !== undefined && y !== undefined && z !== undefined) { -// is_int ? gl.uniform3i(location, x, y, z) : gl.uniform3f(location, x, y, z) -// return -// } -// -// if (x !== undefined && y !== undefined) { -// is_int ? gl.uniform2i(location, x, y) : gl.uniform2f(location, x, y) -// return -// } -// -// if (x !== undefined) { -// is_int ? gl.uniform1i(location, x) : gl.uniform1f(location, x) -// return -// } -// } -// } -// } - /** Get a webgl context for a canvas ref */ export function get_gl_context( canvas: React.RefObject