├── .nvmrc ├── .husky └── pre-commit ├── .github ├── CODEOWNERS ├── workflows │ ├── lint.yml │ ├── build.yml │ ├── publish-package.yml │ └── docs.yml └── dependabot.yml ├── src ├── vite-env.d.ts ├── main.vert ├── lib │ ├── shaders.d.ts │ ├── recipes │ │ ├── FragmentShader │ │ │ ├── default-shader-vert.vert │ │ │ ├── default-shader-frag.frag │ │ │ └── index.ts │ │ └── ParticleSimulation │ │ │ ├── default-shader-frag.frag │ │ │ ├── default-shader-vert.vert │ │ │ └── index.ts │ ├── geometry │ │ ├── Triangle.ts │ │ ├── PointCloud.ts │ │ ├── Plane.ts │ │ ├── Sphere.ts │ │ ├── Box.ts │ │ ├── GeometryAttribute.ts │ │ └── Geometry.ts │ ├── index.ts │ ├── core │ │ ├── Drawable.ts │ │ ├── TransformFeedback.ts │ │ ├── RenderTarget.ts │ │ ├── Uniform.ts │ │ ├── Object.ts │ │ ├── Mesh.ts │ │ ├── Camera.ts │ │ ├── Program.ts │ │ ├── Texture.ts │ │ ├── DollyCamera.ts │ │ └── Renderer.ts │ ├── ext │ │ └── Framebuffer.ts │ └── types.ts ├── style.css ├── main.ts └── main.frag ├── public └── noise.png ├── .prettierignore ├── demos ├── videoTex │ ├── smoke.mp4 │ ├── main.vert │ ├── index.html │ ├── main.frag │ └── index.js ├── framebuffer │ ├── main.vert │ ├── index.html │ ├── render.frag │ ├── main.frag │ └── index.js ├── Camera-and-instancing │ ├── post.vert │ ├── index.html │ ├── main.frag │ ├── main.vert │ ├── post.frag │ └── index.js └── style.css ├── typedoc.json ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .npmignore ├── types └── glsl.d.ts ├── README.md ├── index.html ├── .eslintrc.yml ├── .gitignore ├── tsconfig.json ├── vite.config.js ├── LICENSE └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @wethegit/reviewers 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wethegit/wtc-gl/HEAD/public/noise.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.glsl 2 | *.wgsl 3 | *.vert 4 | *.frag 5 | *.vs 6 | *.fs 7 | docs/**/* -------------------------------------------------------------------------------- /demos/videoTex/smoke.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wethegit/wtc-gl/HEAD/demos/videoTex/smoke.mp4 -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["src/lib/index.ts"], 4 | "out": "docs" 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "slevesque.shader" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | docs/ 3 | .vscode 4 | .husky 5 | .github 6 | vite.config.js 7 | tsconfig.json 8 | index.html 9 | .prettierrc 10 | .prettieriignore 11 | .eslintrc.yml -------------------------------------------------------------------------------- /demos/framebuffer/main.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec3 position; 3 | in vec2 uv; 4 | out vec2 v_uv; 5 | void main() { 6 | gl_Position = vec4(position, 1.0); 7 | v_uv = uv; 8 | } -------------------------------------------------------------------------------- /demos/videoTex/main.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec3 position; 3 | in vec2 uv; 4 | out vec2 v_uv; 5 | void main() { 6 | gl_Position = vec4(position, 1.0); 7 | v_uv = uv; 8 | } -------------------------------------------------------------------------------- /types/glsl.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.glsl'; 2 | declare module '*.wgsl'; 3 | declare module '*.vert'; 4 | declare module '*.frag'; 5 | declare module '*.vs'; 6 | declare module '*.fs'; -------------------------------------------------------------------------------- /demos/Camera-and-instancing/post.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec3 position; 3 | in vec2 uv; 4 | out vec2 v_uv; 5 | void main() { 6 | gl_Position = vec4(position, 1.0); 7 | v_uv = uv; 8 | } -------------------------------------------------------------------------------- /src/main.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec3 position; 4 | in vec2 uv; 5 | out vec2 v_uv; 6 | 7 | void main() { 8 | gl_Position = vec4(position, 1.0); 9 | v_uv = uv; 10 | } -------------------------------------------------------------------------------- /src/lib/shaders.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.frag' { 2 | const value: string 3 | export default value 4 | } 5 | 6 | declare module '*.vert' { 7 | const value: string 8 | export default value 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/recipes/FragmentShader/default-shader-vert.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | attribute vec2 uv; 3 | 4 | varying vec2 v_uv; 5 | 6 | void main() { 7 | gl_Position = vec4(position, 1.0); 8 | v_uv = uv; 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wtc-gl 2 | 3 | ES6 Web GL library for simple WebGL work. Much of this 1.0 version has been adapted from and inspired by Three.js and OGL. 4 | 5 | See [the documentation](https://wethegit.github.io/wtc-gl/) for details. 6 | -------------------------------------------------------------------------------- /demos/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #333; 3 | color: #fff; 4 | font-family: sans-serif; 5 | } 6 | 7 | body, 8 | html { 9 | margin: 0; 10 | overflow: hidden; 11 | padding: 0; 12 | } 13 | 14 | canvas { 15 | width: 100%; 16 | height: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #333; 3 | color: #fff; 4 | font-family: sans-serif; 5 | } 6 | 7 | body, 8 | html { 9 | margin: 0; 10 | overflow: hidden; 11 | padding: 0; 12 | } 13 | 14 | canvas { 15 | width: 100%; 16 | height: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wtc-gl 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demos/framebuffer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wtc-gl 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demos/videoTex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wtc-gl 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - eslint:recommended 6 | - plugin:@typescript-eslint/recommended 7 | - prettier 8 | parser: '@typescript-eslint/parser' 9 | parserOptions: 10 | ecmaVersion: latest 11 | sourceType: module 12 | plugins: 13 | - '@typescript-eslint' 14 | rules: {} 15 | ignorePatterns: 16 | - docs/**/* 17 | -------------------------------------------------------------------------------- /src/lib/recipes/FragmentShader/default-shader-frag.frag: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform vec2 u_resolution; 4 | uniform float u_time; 5 | uniform vec2 u_mouse; 6 | uniform sampler2D s_noise; 7 | 8 | uniform sampler2D b_noise; 9 | 10 | varying vec2 v_uv; 11 | 12 | void main() { 13 | gl_FragColor = vec4(vec3(cos(length(v_uv+u_time))*.5+.5, sin(v_uv+u_time)*.5+.5),1); 14 | } -------------------------------------------------------------------------------- /demos/Camera-and-instancing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wtc-gl - dolly camera demo 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demos/framebuffer/render.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform float u_time; 6 | uniform vec2 u_mouse; 7 | uniform sampler2D s_noise; 8 | uniform sampler2D b_render; 9 | 10 | in vec2 v_uv; 11 | 12 | out vec4 colour; 13 | 14 | 15 | /* Utilities */ 16 | /* ---------- */ 17 | 18 | void main() { 19 | colour = texture(b_render, gl_FragCoord.xy / u_resolution ); 20 | } -------------------------------------------------------------------------------- /src/lib/recipes/ParticleSimulation/default-shader-frag.frag: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform vec2 u_mouse; 6 | uniform float u_time; 7 | 8 | varying vec3 v_colour; 9 | 10 | void main() { 11 | vec2 uv = gl_PointCoord.xy - .5; 12 | 13 | gl_FragColor = vec4(0, 0, 0, 1); 14 | 15 | float l = length(uv); 16 | float c = smoothstep(.5, 0., l); 17 | float opacity = c; 18 | 19 | gl_FragColor = vec4(v_colour, 1.); 20 | } -------------------------------------------------------------------------------- /demos/Camera-and-instancing/main.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | // A basic webgl frag shader to draw v_c to screen 3 | precision highp float; 4 | 5 | uniform vec2 u_resolution; 6 | uniform float u_time; 7 | 8 | in vec2 v_uv; 9 | in vec3 v_c; 10 | in vec3 v_pos; 11 | 12 | out vec4 colour; 13 | 14 | vec2 getScreenSpaceCoords(vec2 uv) { 15 | return (uv - .5 * u_resolution) / min(u_resolution.x, u_resolution.y); 16 | } 17 | void main() { 18 | vec2 p = getScreenSpaceCoords(gl_FragCoord.xy); 19 | colour = vec4(v_c, .3); 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | node_modules 9 | dist 10 | dist-ssr 11 | *.local 12 | .vscode/* 13 | !.vscode/extensions.json 14 | .idea 15 | .DS_Store 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | .sass-cache 22 | **/*/.sass-cache 23 | **/*/.sass-cache 24 | *.sublime-project 25 | *.sublime-workspace 26 | **/*.sublime-workspace 27 | **/node_modules 28 | **/*/node_modules 29 | bower_components 30 | **/bower_components 31 | **/*/bower_components 32 | **/*/*.log 33 | docs/ -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, reopened, synchronize] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version-file: '.nvmrc' 21 | 22 | - name: Install Dependencies 23 | run: npm ci 24 | 25 | - name: Lint 26 | run: npm run lint 27 | -------------------------------------------------------------------------------- /demos/videoTex/main.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform float u_time; 6 | uniform vec2 u_mouse; 7 | uniform sampler2D s_noise; 8 | uniform sampler2D s_smoke; 9 | 10 | in vec2 v_uv; 11 | 12 | out vec4 colour; 13 | 14 | vec2 getScreenSpace() { 15 | vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; 16 | 17 | return uv; 18 | } 19 | void main() { 20 | vec2 uv = getScreenSpace(); 21 | 22 | uv *= 1.; 23 | 24 | vec3 c = texture(s_smoke, uv-.5).rgb; 25 | c *= .8-uv.y; 26 | 27 | colour = vec4(1)*c.x; 28 | // colour = vec4(.5); 29 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, reopened, synchronize] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version-file: '.nvmrc' 21 | 22 | - name: Install Dependencies 23 | run: npm i 24 | 25 | - name: Build 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM and Github Packages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - src/** 8 | - types/** 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version-file: '.nvmrc' 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm ci 20 | - run: npm run build 21 | - run: npm publish --access public 22 | env: 23 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 24 | -------------------------------------------------------------------------------- /src/lib/recipes/ParticleSimulation/default-shader-vert.vert: -------------------------------------------------------------------------------- 1 | attribute vec2 reference; 2 | attribute vec2 position; 3 | attribute float property; 4 | 5 | uniform vec2 u_resolution; 6 | uniform vec2 u_screen; 7 | 8 | uniform sampler2D b_velocity; 9 | uniform sampler2D b_position; 10 | 11 | varying vec3 v_colour; 12 | varying float v_fogDepth; 13 | 14 | void main() { 15 | vec2 position = texture2D(b_position, reference).xy; 16 | gl_PointSize = 2.; 17 | vec2 p = position/u_resolution; 18 | v_colour = property == 1. ? vec3(1,0,0) : vec3(1,1,1); 19 | vec4 pos = vec4(position / u_resolution * 2. - 1., 0., 1.); 20 | gl_Position = vec4(pos.xy, 0., 1.); 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[javascript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll": "explicit" 9 | }, 10 | "eslint.format.enable": true, 11 | "eslint.nodePath": "", 12 | "files.associations": { 13 | "*.js": "javascriptreact", 14 | "*.jsx": "javascriptreact", 15 | "*.json": "json" 16 | }, 17 | "javascript.updateImportsOnFileMove.enabled": "always", 18 | "stylelint.validate": ["css", "scss"], 19 | "typescript.tsdk": "node_modules/typescript/lib" 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'weekly' 12 | labels: 13 | - 'dependencies' 14 | ignore: 15 | - dependency-name: '*' 16 | update-types: ['version-update:semver-major'] 17 | reviewers: 18 | - 'wethegit/developers' 19 | -------------------------------------------------------------------------------- /demos/Camera-and-instancing/main.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | in vec3 position; 3 | in vec3 colour; 4 | in vec2 uv; 5 | in vec3 normal; 6 | in mat4 transformation; 7 | uniform mat4 u_viewMatrix; 8 | uniform mat4 u_modelMatrix; 9 | uniform mat4 u_modelViewMatrix; 10 | uniform mat4 u_projectionMatrix; 11 | uniform mat3 u_normalMatrix; 12 | out vec2 v_uv; 13 | out vec3 v_n; 14 | out vec3 v_pos; 15 | out vec3 v_c; 16 | 17 | void main() { 18 | v_n = normalize(u_normalMatrix * normal); 19 | v_uv = uv; 20 | vec4 camPos = u_projectionMatrix * u_modelViewMatrix * (transformation * vec4(position, 1.0)); 21 | 22 | gl_Position = camPos; 23 | 24 | v_pos = (u_modelMatrix * vec4(position, 1.0)).xyz; 25 | 26 | v_c = colour; 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "strictPropertyInitialization": false 20 | }, 21 | "include": ["src", "types"], 22 | "exclude": ["docs/", "dist/", "node_modules/", "demos"], 23 | "typedocOptions": { 24 | "out": "docs", 25 | "entryPoints": "src/lib/index.ts", 26 | "exclude": ["dist/*+.ts*"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/geometry/Triangle.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WTCGLGeometryAttributeCollection, 3 | WTCGLRenderingContext 4 | } from '../types' 5 | 6 | import { Geometry } from './Geometry' 7 | import { GeometryAttribute } from './GeometryAttribute' 8 | 9 | export interface TriangleOptions { 10 | attributes: WTCGLGeometryAttributeCollection 11 | } 12 | 13 | export class Triangle extends Geometry { 14 | constructor( 15 | gl: WTCGLRenderingContext, 16 | { attributes = {} }: Partial = {} 17 | ) { 18 | Object.assign(attributes, { 19 | position: new GeometryAttribute({ 20 | size: 2, 21 | data: new Float32Array([-1, -1, 3, -1, -1, 3]) 22 | }), 23 | uv: new GeometryAttribute({ 24 | size: 2, 25 | data: new Float32Array([0, 0, 2, 0, 0, 2]) 26 | }) 27 | }) 28 | 29 | super(gl, attributes) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { FragmentShader, Texture, Uniform } from './lib' 2 | 3 | import './style.css' 4 | 5 | import fragment from './main.frag' 6 | import vertex from './main.vert' 7 | 8 | async function init() { 9 | // Create the fragment shader wrapper 10 | const { gl, uniforms } = new FragmentShader({ 11 | fragment: fragment, 12 | vertex 13 | }) 14 | 15 | // Load the image into the uniform 16 | const image: HTMLImageElement = await new Promise((resolve, reject) => { 17 | const img = new Image() 18 | 19 | img.src = '/noise.png' 20 | img.onload = () => resolve(img) 21 | img.onerror = reject 22 | }) 23 | 24 | // Create the texture 25 | const texture = new Texture(gl, { 26 | wrapS: gl.REPEAT, 27 | wrapT: gl.REPEAT, 28 | image: image 29 | }) 30 | 31 | uniforms.s_noise = new Uniform({ 32 | name: 'noise', 33 | value: texture, 34 | kind: 'texture' 35 | }) 36 | } 37 | 38 | init() 39 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | 3 | export * from './core/Renderer' 4 | export * from './core/Mesh' 5 | export * from './core/Drawable' 6 | export * from './core/Object' 7 | export * from './core/Program' 8 | export * from './core/Uniform' 9 | export * from './core/Texture' 10 | export * from './core/Camera' 11 | export * from './core/DollyCamera' 12 | export * from './core/RenderTarget' 13 | export * from './core/TransformFeedback' 14 | 15 | export * from './geometry/Geometry' 16 | export * from './geometry/GeometryAttribute' 17 | export * from './geometry/Triangle' 18 | export * from './geometry/Plane' 19 | export * from './geometry/Box' 20 | export * from './geometry/PointCloud' 21 | 22 | export * from './ext/Framebuffer' 23 | 24 | export * from './recipes/FragmentShader' 25 | export * from './recipes/ParticleSimulation' 26 | 27 | /** 28 | * Including everything in wtc-math. See https://wethegit.github.io/wtc-math/ for more information. 29 | */ 30 | export * from 'wtc-math' 31 | -------------------------------------------------------------------------------- /demos/Camera-and-instancing/post.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform sampler2D s_z; 6 | uniform sampler2D b_p; 7 | uniform sampler2D b_render; 8 | uniform vec2 u_bd; 9 | uniform float u_f; 10 | 11 | in vec2 v_uv; 12 | 13 | out vec4 colour; 14 | 15 | 16 | /* Utilities */ 17 | /* ---------- */ 18 | #define T(x) texture(x, gl_FragCoord.xy / u_resolution ) 19 | void main() { 20 | vec2 uv=gl_FragCoord.xy / u_resolution; 21 | float d = 12.; 22 | 23 | vec4 sum; 24 | vec2 px = 1./u_resolution; 25 | 26 | float offset[13] = float[](0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, -1.0, -2.0, -3.0, -4.0, -5.0, -6.0); 27 | float weight[13] = float[]( 28 | 0.227027, 0.1895946, 0.1216216, 0.054054, 0.016216, 0.018108, 0.004054, 29 | 0.1895946, 0.1216216, 0.054054, 0.016216, 0.018108, 0.004054); 30 | 31 | for (int i = 0; i < 13; i++) { 32 | vec2 samplePos = uv + u_bd * offset[i] * px * d; 33 | sum += texture(b_render, samplePos) * weight[i]; 34 | } 35 | colour = sum; 36 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | import glsl from 'vite-plugin-glsl' 4 | import dts from 'vite-plugin-dts' 5 | 6 | // eslint-disable-next-line no-undef 7 | const resolvePath = (str) => resolve(__dirname, str) 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | glsl(), 13 | dts({ 14 | include: 'src/lib/**/*.ts' 15 | }) 16 | ], 17 | build: { 18 | copyPublicDir: false, 19 | lib: { 20 | // Could also be a dictionary or array of multiple entry points 21 | entry: resolvePath('src/lib/index.ts'), 22 | name: 'WTCGL' 23 | }, 24 | rollupOptions: { 25 | // make sure to externalize deps that shouldn't be bundled 26 | // into your library 27 | external: ['wtc-math'], 28 | output: { 29 | // Provide global variables to use in the UMD build 30 | // for externalized deps 31 | globals: { 32 | 'wtc-math': 'wtcMath' 33 | } 34 | } 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/main.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform float u_time; 6 | uniform vec2 u_mouse; 7 | uniform sampler2D s_noise; 8 | 9 | uniform sampler2D b_noise; 10 | 11 | in vec2 v_uv; 12 | 13 | out vec4 colour; 14 | 15 | vec2 getScreenSpace() { 16 | vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); 17 | 18 | return uv; 19 | } 20 | 21 | vec2 rot2D (vec2 q, float a) { 22 | return q * cos(a) + q.yx * sin(a) * vec2 (-1., 1.); 23 | } 24 | #define TAU 6.28318530718 25 | vec2 r2(vec2 q, float a) { 26 | a*=TAU; 27 | float c = cos(a); 28 | float s = sin(a); 29 | return vec2(c*q.x+s*q.y, c*q.y-s*q.x); 30 | } 31 | 32 | void main() { 33 | vec2 uv = getScreenSpace(); 34 | 35 | uv *= 1.; 36 | 37 | vec2 a = abs(uv); 38 | a = r2(vec2(max(a.x,a.y), min(a.x,a.y)), .0625); 39 | float id = floor(atan(uv.x,uv.y)*8./TAU+4.)/3.; 40 | 41 | float f = length(a-vec2(.3, 0))-.1*(cos(u_time*20.+id)*.5+.5); 42 | float m = smoothstep(0., .002, f); 43 | 44 | colour = vec4(vec3(m),1); 45 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 We The Collective 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demos/videoTex/index.js: -------------------------------------------------------------------------------- 1 | import { FragmentShader, Texture, Uniform } from '../../src/lib' 2 | 3 | import '../style.css' 4 | 5 | import fragment from './main.frag' 6 | import vertex from './main.vert' 7 | 8 | const initWebgl = (video) => { 9 | // Create the fragment shader wrapper 10 | const FSWrapper = new FragmentShader({ 11 | fragment, 12 | vertex, 13 | rendererProps: { alpha: true, premultipliedAlpha: false }, 14 | onBeforeRender: () => { 15 | videoTexture.needsUpdate = true 16 | } 17 | }) 18 | 19 | const { gl, uniforms } = FSWrapper 20 | 21 | const videoTexture = new Texture(gl, { 22 | wrapS: gl.REPEAT, 23 | wrapT: gl.REPEAT, 24 | image: video, 25 | generateMipmaps: false 26 | }) 27 | 28 | uniforms.s_smoke = new Uniform({ 29 | name: 'smoke', 30 | value: videoTexture, 31 | kind: 'texture' 32 | }) 33 | } 34 | 35 | // Create the video element 36 | const video = document.createElement('video') 37 | video.autoplay = true 38 | video.loop = true 39 | video.muted = true 40 | video.crossOrigin = true 41 | video.src = 'https://assets.codepen.io/982762/smoke.mp4' 42 | video.addEventListener('canplaythrough', () => { 43 | video.play() 44 | initWebgl(video) 45 | // videoTexture.image = video; 46 | }) 47 | -------------------------------------------------------------------------------- /src/lib/geometry/PointCloud.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | import { TransformFeedback } from '../core/TransformFeedback' 3 | 4 | import { Geometry } from './Geometry' 5 | import { GeometryAttribute } from './GeometryAttribute' 6 | 7 | export interface PointCloudOptions { 8 | particles: number 9 | dimensions: number 10 | fillFunction: (p: Float32Array, d: number) => void 11 | attributes: object 12 | transformFeedbacks: TransformFeedback 13 | } 14 | 15 | export class PointCloud extends Geometry { 16 | constructor( 17 | gl: WTCGLRenderingContext, 18 | { 19 | particles = 128, 20 | dimensions = 3, 21 | fillFunction = (points, dimensions) => { 22 | for (let i = 0; i < points.length; i += dimensions) { 23 | for (let j = 0; j < dimensions; j++) { 24 | points[i + j] = Math.random() // n position 25 | } 26 | } 27 | }, 28 | attributes = {}, 29 | transformFeedbacks 30 | }: Partial = {} 31 | ) { 32 | const points = new Float32Array(particles * dimensions).fill(0) // The point position 33 | 34 | fillFunction(points, dimensions) 35 | 36 | const attr = Object.assign({}, attributes, { 37 | position: new GeometryAttribute({ size: dimensions, data: points }) 38 | }) 39 | 40 | super(gl, attr, transformFeedbacks) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 12 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 13 | concurrency: 14 | group: 'pages' 15 | cancel-in-progress: false 16 | 17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 18 | permissions: 19 | contents: read 20 | pages: write 21 | id-token: write 22 | 23 | jobs: 24 | docs: 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - name: Checkout Repo 33 | uses: actions/checkout@v4 34 | 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version-file: '.nvmrc' 39 | 40 | - name: Install Dependencies 41 | run: npm install 42 | 43 | - name: Build Docs 44 | run: npm run document 45 | 46 | - name: Setup Pages 47 | uses: actions/configure-pages@v3 48 | 49 | - name: Upload artifact 50 | uses: actions/upload-pages-artifact@v2 51 | with: 52 | path: './docs' 53 | 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v2 57 | -------------------------------------------------------------------------------- /demos/framebuffer/main.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision highp float; 3 | 4 | uniform vec2 u_resolution; 5 | uniform float u_time; 6 | uniform float u_frame; 7 | uniform vec2 u_mouse; 8 | uniform vec2 u_blurdir; 9 | uniform sampler2D s_noise; 10 | uniform sampler2D b_render; 11 | 12 | in vec2 v_uv; 13 | 14 | out vec4 colour; 15 | 16 | vec2 getScreenSpace() { 17 | vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); 18 | 19 | return uv; 20 | } 21 | void main() { 22 | vec2 uv = getScreenSpace(); 23 | 24 | float scale = 20.; 25 | vec2 ID = floor(uv*scale); 26 | vec2 suv = mod(uv*scale, 1.)-.5; 27 | 28 | float l = 1.; 29 | 30 | if(u_frame <= 1000.) { 31 | if(ID==vec2(0,0))l=0.; 32 | else if(ID==vec2(1,0))l=0.; 33 | else if(ID==vec2(2,0))l=0.; 34 | else if(ID==vec2(2,1))l=0.; 35 | else if(ID==vec2(1,2))l=0.; 36 | l = smoothstep(0., .01, length(uv)-.3); 37 | } else { 38 | l = texture(b_render,gl_FragCoord.xy/u_resolution*scale).r; 39 | } 40 | 41 | float m = u_time*10.; 42 | float c = smoothstep(0.01,0.,length(uv+vec2(cos(m),sin(m))*.3)-.1); 43 | 44 | vec4 t = texture(b_render,floor((gl_FragCoord.xy+scale*.5)/scale)/u_resolution*scale); 45 | vec4 tr = texture(b_render,gl_FragCoord.xy/u_resolution,10.); 46 | 47 | // l = smoothstep(0., .01, length(suv)-.3); 48 | 49 | colour = vec4(vec3(l,tr.x,l),1.); 50 | colour = vec4(vec3(c)*.1+texture(b_render,gl_FragCoord.xy/u_resolution, 5.).rgb*.995,1); 51 | // colour = texture(s_noise, uv, -1.); 52 | // colour = a; 53 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtc-gl", 3 | "version": "1.2.0-beta.2", 4 | "description": "Typescript simple Web GL library.", 5 | "type": "module", 6 | "files": [ 7 | "dist" 8 | ], 9 | "types": "./dist/index.d.ts", 10 | "main": "./dist/wtc-gl.umd.cjs", 11 | "module": "./dist/wtc-gl.js", 12 | "exports": { 13 | "import": { 14 | "types": "./dist/index.d.ts", 15 | "default": "./dist/wtc-gl.js" 16 | }, 17 | "require": { 18 | "types": "./dist/index.d.ts", 19 | "default": "./dist/wtc-gl.umd.cjs" 20 | } 21 | }, 22 | "scripts": { 23 | "prepare": "husky", 24 | "dev": "vite", 25 | "build": "rm -rf dist; tsc && vite build", 26 | "lint": "eslint --fix --ext .ts --ignore-path .gitignore ./src/**/*", 27 | "preview": "vite preview", 28 | "document": "npx typedoc --entryPointStrategy Expand" 29 | }, 30 | "devDependencies": { 31 | "@types/webgl2": "0.0.11", 32 | "@typescript-eslint/eslint-plugin": "~7.18.0", 33 | "@typescript-eslint/parser": "~7.18.0", 34 | "eslint": "^8.38.0", 35 | "eslint-config-prettier": "~8.10.0", 36 | "husky": "~9.1.6", 37 | "lint-staged": "^13.2.2", 38 | "prettier": "^3.2.5", 39 | "typedoc": "~0.26.5", 40 | "typescript": "~5.6.2", 41 | "vite": "^5.2.11", 42 | "vite-plugin-dts": "~3.9.1", 43 | "vite-plugin-glsl": "~1.3.0" 44 | }, 45 | "dependencies": { 46 | "wtc-math": "^1.0.20" 47 | }, 48 | "peerDependencies": { 49 | "wtc-math": "^1.0.20" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/wethegit/wtc-gl.git" 54 | }, 55 | "keywords": [], 56 | "homepage": "https://github.com/wethegit/wtc-gl#readme", 57 | "author": "Liam Egan (http://wethecollective.com)", 58 | "license": "MIT", 59 | "publishConfig": { 60 | "access": "public" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "lint-staged": { 68 | "src/**/*.{ts,js}": "eslint --fix", 69 | "src/**/*": "prettier -w -u" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/core/Drawable.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | 3 | import { Obj } from './Object' 4 | import { Program } from './Program' 5 | 6 | let ID = 0 7 | 8 | export interface DrawableOptions { 9 | frustumCulled: boolean 10 | renderOrder: number 11 | } 12 | 13 | /** 14 | * Class representing A drawable object. A drawable object is one that is actually rendered to screen, this does not include cameras and groups. 15 | * @extends Obj 16 | **/ 17 | export class Drawable extends Obj { 18 | /** 19 | * The unique ID of the Geometry. 20 | */ 21 | id: number 22 | /** 23 | * The WTCGL rendering context. 24 | */ 25 | gl: WTCGLRenderingContext 26 | /** 27 | * The program that is rendering this object 28 | */ 29 | program: Program 30 | /** 31 | * Whether to apply frustum culling to this object 32 | * @default true 33 | */ 34 | frustumCulled: boolean 35 | /** 36 | * Apply a specific render order, overriding calculated z-depth 37 | * @default 0 38 | */ 39 | renderOrder: number = 0 40 | /** 41 | * The z-depth of the object, calculated at render time. 42 | * @default 0 43 | */ 44 | zDepth: number = 0 45 | 46 | /** 47 | * Create a drawable object. This should never be instanciated directly 48 | * @param {WTCGLRenderingContext} gl - The WTCGL Rendering context 49 | * @param {Object} __namedParameters - The parameters to be used for the camera 50 | * @param {boolean} frustumCulled - Whether to apply culling to this object 51 | * @param {number} renderOrder - The explicit render order of the object. If this is zero, then it will be instead calculated at render time. 52 | */ 53 | constructor( 54 | gl: WTCGLRenderingContext, 55 | { frustumCulled = true, renderOrder = 0 }: Partial = {} 56 | ) { 57 | super() 58 | 59 | if (!gl.canvas) 60 | console.error( 61 | 'WTCGLRenderingContext should be passed as the first argument to Object' 62 | ) 63 | 64 | this.gl = gl 65 | this.id = ID++ 66 | 67 | this.frustumCulled = frustumCulled 68 | 69 | this.renderOrder = renderOrder 70 | } 71 | 72 | /** 73 | * Draw placeholder. The draw function is responsible for drawing the element. This function simply provides a signature for extension. 74 | **/ 75 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any 76 | draw(..._args: any[]): void {} 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/geometry/Plane.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | 3 | import { Geometry } from './Geometry' 4 | import { GeometryAttribute } from './GeometryAttribute' 5 | 6 | export interface PlaneOptions { 7 | width: number 8 | height: number 9 | widthSegments: number 10 | heightSegments: number 11 | attributes: object 12 | } 13 | 14 | export class Plane extends Geometry { 15 | constructor( 16 | gl: WTCGLRenderingContext, 17 | { 18 | width = 1, 19 | height = 1, 20 | widthSegments = 1, 21 | heightSegments = 1, 22 | attributes = {} 23 | }: Partial = {} 24 | ) { 25 | const wSegs = widthSegments 26 | const hSegs = heightSegments 27 | 28 | // Determine length of arrays 29 | const num = (wSegs + 1) * (hSegs + 1) 30 | const numIndices = wSegs * hSegs * 6 31 | 32 | // Generate empty arrays once 33 | const position = new Float32Array(num * 3) 34 | const normal = new Float32Array(num * 3) 35 | const uv = new Float32Array(num * 2) 36 | const index = 37 | numIndices > 65536 38 | ? new Uint32Array(numIndices) 39 | : new Uint16Array(numIndices) 40 | 41 | Plane.buildPlane( 42 | position, 43 | normal, 44 | uv, 45 | index, 46 | width, 47 | height, 48 | 0, 49 | wSegs, 50 | hSegs 51 | ) 52 | 53 | const attr = Object.assign({}, attributes, { 54 | position: new GeometryAttribute({ size: 3, data: position }), 55 | normal: new GeometryAttribute({ size: 3, data: normal }), 56 | uv: new GeometryAttribute({ size: 2, data: uv }), 57 | index: new GeometryAttribute({ data: index }) 58 | }) 59 | 60 | super(gl, attr) 61 | } 62 | 63 | static buildPlane( 64 | position: Float32Array, 65 | normal: Float32Array, 66 | uv: Float32Array, 67 | index: Uint32Array | Uint16Array, 68 | width: number, 69 | height: number, 70 | depth: number, 71 | wSegs: number, 72 | hSegs: number, 73 | u: number = 0, 74 | v: number = 1, 75 | w: number = 2, 76 | uDir: number = 1, 77 | vDir: number = -1, 78 | i: number = 0, 79 | ii: number = 0 80 | ) { 81 | const io = i 82 | const segW = width / wSegs 83 | const segH = height / hSegs 84 | 85 | for (let iy = 0; iy <= hSegs; iy++) { 86 | const y = iy * segH - height / 2 87 | for (let ix = 0; ix <= wSegs; ix++, i++) { 88 | const x = ix * segW - width / 2 89 | 90 | position[i * 3 + u] = x * uDir 91 | position[i * 3 + v] = y * vDir 92 | position[i * 3 + w] = depth / 2 93 | 94 | normal[i * 3 + u] = 0 95 | normal[i * 3 + v] = 0 96 | normal[i * 3 + w] = depth >= 0 ? 1 : -1 97 | 98 | uv[i * 2] = ix / wSegs 99 | uv[i * 2 + 1] = 1 - iy / hSegs 100 | 101 | if (iy === hSegs || ix === wSegs) continue 102 | const a = io + ix + iy * (wSegs + 1) 103 | const b = io + ix + (iy + 1) * (wSegs + 1) 104 | const c = io + ix + (iy + 1) * (wSegs + 1) + 1 105 | const d = io + ix + iy * (wSegs + 1) + 1 106 | 107 | index[ii * 6] = a 108 | index[ii * 6 + 1] = b 109 | index[ii * 6 + 2] = d 110 | index[ii * 6 + 3] = b 111 | index[ii * 6 + 4] = c 112 | index[ii * 6 + 5] = d 113 | ii++ 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/core/TransformFeedback.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | 3 | import { Program } from './Program' 4 | 5 | const createBuffer = ( 6 | gl: WTCGLRenderingContext, 7 | data: Float32Array, 8 | usage: GLenum = gl.STATIC_DRAW, 9 | type: GLenum = gl.ARRAY_BUFFER 10 | ): WebGLBuffer => { 11 | const buffer: WebGLBuffer = gl.createBuffer()! 12 | 13 | gl.bindBuffer(type, buffer) 14 | gl.bufferData(type, data, usage) 15 | 16 | return buffer 17 | } 18 | 19 | export interface TransformFeedbackAttribute { 20 | size: number 21 | type?: GLenum 22 | normalize?: boolean 23 | stride?: number 24 | offset?: number 25 | buffer?: WebGLBuffer 26 | data: Float32Array 27 | varying: string 28 | usage?: GLenum 29 | buffertype?: GLenum 30 | } 31 | 32 | export interface TransformFeedbackOptions { 33 | program: Program 34 | transformFeedbacks: { 35 | [key: string]: TransformFeedbackAttribute 36 | } 37 | } 38 | 39 | export type BufferRef = { i: number; buffer: WebGLBuffer | null } 40 | 41 | export type BufferRecord = Record 42 | 43 | /** 44 | * To-Do 45 | * Update this class to take care of its own internal state (like render targets) rather than relying on geo to control state 46 | */ 47 | export class TransformFeedback { 48 | VAOs: [WebGLVertexArrayObject, WebGLVertexArrayObject] 49 | TFBs: [WebGLTransformFeedback, WebGLTransformFeedback] 50 | BufferRefs: BufferRecord[] 51 | 52 | constructor( 53 | gl: WTCGLRenderingContext, 54 | { program, transformFeedbacks }: TransformFeedbackOptions 55 | ) { 56 | this.VAOs = [gl.createVertexArray(), gl.createVertexArray()] 57 | this.TFBs = [gl.createTransformFeedback()!, gl.createTransformFeedback()!] 58 | this.BufferRefs = [] 59 | const names = Object.keys(transformFeedbacks) 60 | 61 | this.VAOs.forEach((vao: WebGLVertexArrayObject, i: number) => { 62 | gl.bindVertexArray(vao) 63 | 64 | const buffers: (WebGLBuffer | null)[] = [] 65 | const bufferRef: BufferRecord = {} 66 | 67 | for (let i = 0; i < names.length; i++) { 68 | const tf = transformFeedbacks[names[i]] 69 | 70 | const { 71 | size = 1, 72 | type = gl.FLOAT, 73 | normalize = false, 74 | stride = 0, 75 | offset = 0, 76 | data, 77 | usage = gl.STATIC_DRAW, 78 | buffertype = gl.ARRAY_BUFFER, 79 | buffer: defaultBuffer = null 80 | } = tf 81 | 82 | const buffer = 83 | data && !defaultBuffer 84 | ? createBuffer(gl, data, usage, buffertype) 85 | : defaultBuffer 86 | 87 | bufferRef[names[i]] = { i, buffer } 88 | 89 | gl.bindAttribLocation(program, i, names[i]) 90 | gl.enableVertexAttribArray(i) 91 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer) 92 | gl.vertexAttribPointer(i, size, type, normalize, stride, offset) 93 | 94 | buffers.push(buffer) 95 | } 96 | 97 | gl.bindBuffer(gl.ARRAY_BUFFER, null) 98 | 99 | // TO DO Try putting these inside the loop 100 | gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.TFBs[i]) 101 | 102 | buffers.forEach((b, i) => { 103 | gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, i, b) 104 | }) 105 | 106 | gl.bindVertexArray(null) 107 | 108 | this.BufferRefs.push(bufferRef) 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /demos/framebuffer/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vec2, 3 | FragmentShader, 4 | Texture, 5 | Uniform, 6 | Triangle, 7 | Program, 8 | Mesh, 9 | Framebuffer 10 | } from '../../src/lib' 11 | 12 | import '../style.css' 13 | 14 | import fragment from './main.frag' 15 | import vertex from './main.vert' 16 | import renderFragment from './render.frag' 17 | 18 | async function init() { 19 | console.clear() 20 | 21 | const div = 1 22 | let dir = [0, 1] 23 | 24 | let it = 0 25 | let mainFBO = null 26 | let onBeforeRender = function () { 27 | // if(it++>20) FSWrapper.playing = false; 28 | uniforms.u_frame.value += 1 29 | if (mainFBO) { 30 | const res = [...this.uniforms[`u_resolution`].value] 31 | this.uniforms[`u_resolution`].value = [res[0] / div, res[1] / div] 32 | this.uniforms[`u_blurdir`].value = dir.reverse() 33 | this.uniforms[`b_render`].value = mainFBO.read.texture 34 | mainFBO.render(this.renderer, { scene: mainMesh }) 35 | // this.uniforms[`u_blurdir`].value = dir.reverse(); 36 | // this.uniforms[`b_render`].value = mainFBO.read.texture; 37 | // mainFBO.render(this.renderer, { scene: mainMesh }); 38 | this.uniforms[`u_resolution`].value = res 39 | } 40 | } 41 | let resizeTimer 42 | window.addEventListener('resize', (e) => { 43 | clearTimeout(resizeTimer) 44 | resizeTimer = setTimeout(() => { 45 | uniforms.u_frame.value = 0 46 | mainFBO.resize( 47 | FSWrapper.dimensions.width / div, 48 | FSWrapper.dimensions.height / div 49 | ) 50 | }, 10) 51 | }) 52 | 53 | // Create the fragment shader wrapper 54 | const FSWrapper = new FragmentShader({ 55 | fragment, 56 | vertex, 57 | onBeforeRender, 58 | rendererProps: { dpr: 2 }, 59 | uniforms: { 60 | b_render: new Uniform({ 61 | name: 'render', 62 | value: null, 63 | kind: 'texture' 64 | }) 65 | } 66 | }) 67 | 68 | const { gl, uniforms, renderer, dimensions } = FSWrapper 69 | 70 | uniforms.u_frame = new Uniform({ 71 | name: 'frame', 72 | value: 0, 73 | kind: 'float' 74 | }) 75 | uniforms.u_blurdir = new Uniform({ 76 | name: 'blurdir', 77 | value: dir, 78 | kind: 'vec2' 79 | }) 80 | 81 | const geometry = new Triangle(gl) 82 | const mainProgram = new Program(gl, { 83 | vertex, 84 | fragment, 85 | uniforms: uniforms 86 | }) 87 | const mainMesh = new Mesh(gl, { geometry, program: mainProgram }) 88 | mainFBO = new Framebuffer(gl, { 89 | dpr: renderer.dpr, 90 | name: 'render', 91 | width: dimensions.width / div, 92 | height: dimensions.height / div, 93 | texdepth: Framebuffer.TEXTYPE_FLOAT, 94 | tiling: Framebuffer.IMAGETYPE_MIRROR, 95 | type: gl.FLOAT, 96 | minFilter: gl.NEAREST_MIPMAP_LINEAR, 97 | generateMipmaps: true 98 | }) 99 | 100 | // Create the texture 101 | // const texture = new Texture(gl, { 102 | // wrapS: gl.REPEAT, 103 | // wrapT: gl.REPEAT, 104 | // generateMipmaps: false 105 | // }); 106 | // // Load the image into the uniform 107 | // const img = new Image(); 108 | // img.crossOrigin = "anonymous"; 109 | // img.src = "/public/noise.png"; 110 | // img.onload = () => (texture.image = img); 111 | 112 | // uniforms.s_noise = new Uniform({ 113 | // name: "noise", 114 | // value: texture, 115 | // kind: "texture" 116 | // }); 117 | } 118 | 119 | init() 120 | -------------------------------------------------------------------------------- /src/lib/geometry/Sphere.ts: -------------------------------------------------------------------------------- 1 | import { Vec3 } from 'wtc-math' 2 | 3 | import type { 4 | WTCGLGeometryAttributeCollection, 5 | WTCGLRenderingContext 6 | } from '../types' 7 | 8 | import { Geometry } from './Geometry' 9 | import { GeometryAttribute } from './GeometryAttribute' 10 | 11 | export interface SphereOptions { 12 | radius: number 13 | widthSegments: number 14 | heightSegments: number 15 | phiStart: number 16 | phiLength: number 17 | thetaStart: number 18 | thetaLength: number 19 | attributes: WTCGLGeometryAttributeCollection 20 | } 21 | 22 | export class Sphere extends Geometry { 23 | constructor( 24 | gl: WTCGLRenderingContext, 25 | { 26 | radius = 0.5, 27 | widthSegments = 16, 28 | heightSegments = Math.ceil(widthSegments * 0.5), 29 | phiStart = 0, 30 | phiLength = Math.PI * 2, 31 | thetaStart = 0, 32 | thetaLength = Math.PI, 33 | attributes = {} 34 | }: Partial = {} 35 | ) { 36 | const wSegs = widthSegments 37 | const hSegs = heightSegments 38 | const pStart = phiStart 39 | const pLength = phiLength 40 | const tStart = thetaStart 41 | const tLength = thetaLength 42 | 43 | const num = (wSegs + 1) * (hSegs + 1) 44 | const numIndices = wSegs * hSegs * 6 45 | 46 | const position = new Float32Array(num * 3) 47 | const normal = new Float32Array(num * 3) 48 | const uv = new Float32Array(num * 2) 49 | const index = 50 | num > 65536 ? new Uint32Array(numIndices) : new Uint16Array(numIndices) 51 | 52 | let i = 0 53 | let iv = 0 54 | let ii = 0 55 | const te = tStart + tLength 56 | const grid = [] 57 | 58 | const n = new Vec3() 59 | 60 | for (let iy = 0; iy <= hSegs; iy++) { 61 | const vRow = [] 62 | const v = iy / hSegs 63 | for (let ix = 0; ix <= wSegs; ix++, i++) { 64 | const u = ix / wSegs 65 | const x = 66 | -radius * 67 | Math.cos(pStart + u * pLength) * 68 | Math.sin(tStart + v * tLength) 69 | const y = radius * Math.cos(tStart + v * tLength) 70 | const z = 71 | radius * 72 | Math.sin(pStart + u * pLength) * 73 | Math.sin(tStart + v * tLength) 74 | 75 | position[i * 3] = x 76 | position[i * 3 + 1] = y 77 | position[i * 3 + 2] = z 78 | 79 | n.reset(x, y, z).normalise() 80 | normal[i * 3] = n.x 81 | normal[i * 3 + 1] = n.y 82 | normal[i * 3 + 2] = n.z 83 | 84 | uv[i * 2] = u 85 | uv[i * 2 + 1] = 1 - v 86 | 87 | vRow.push(iv++) 88 | } 89 | 90 | grid.push(vRow) 91 | } 92 | 93 | for (let iy = 0; iy < hSegs; iy++) { 94 | for (let ix = 0; ix < wSegs; ix++) { 95 | const a = grid[iy][ix + 1] 96 | const b = grid[iy][ix] 97 | const c = grid[iy + 1][ix] 98 | const d = grid[iy + 1][ix + 1] 99 | 100 | if (iy !== 0 || tStart > 0) { 101 | index[ii * 3] = a 102 | index[ii * 3 + 1] = b 103 | index[ii * 3 + 2] = d 104 | ii++ 105 | } 106 | if (iy !== hSegs - 1 || te < Math.PI) { 107 | index[ii * 3] = b 108 | index[ii * 3 + 1] = c 109 | index[ii * 3 + 2] = d 110 | ii++ 111 | } 112 | } 113 | } 114 | 115 | Object.assign(attributes, { 116 | position: new GeometryAttribute({ size: 3, data: position }), 117 | normal: new GeometryAttribute({ size: 3, data: normal }), 118 | uv: new GeometryAttribute({ size: 2, data: uv }), 119 | index: new GeometryAttribute({ data: index }) 120 | }) 121 | 122 | super(gl, attributes) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/lib/geometry/Box.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | 3 | import { Geometry } from './Geometry' 4 | import { GeometryAttribute } from './GeometryAttribute' 5 | import { Plane } from './Plane' 6 | 7 | export interface BoxOptions { 8 | width: number 9 | height: number 10 | depth: number 11 | widthSegments: number 12 | heightSegments: number 13 | depthSegments: number 14 | attributes: object 15 | } 16 | 17 | export class Box extends Geometry { 18 | constructor( 19 | gl: WTCGLRenderingContext, 20 | { 21 | width = 1, 22 | height = 1, 23 | depth = 1, 24 | widthSegments = 1, 25 | heightSegments = 1, 26 | depthSegments = 1, 27 | attributes = {} 28 | }: Partial = {} 29 | ) { 30 | const wSegs = widthSegments 31 | const hSegs = heightSegments 32 | const dSegs = depthSegments 33 | 34 | const num = 35 | (wSegs + 1) * (hSegs + 1) * 2 + 36 | (wSegs + 1) * (dSegs + 1) * 2 + 37 | (hSegs + 1) * (dSegs + 1) * 2 38 | const numIndices = 39 | (wSegs * hSegs * 2 + wSegs * dSegs * 2 + hSegs * dSegs * 2) * 6 40 | 41 | const position = new Float32Array(num * 3) 42 | const normal = new Float32Array(num * 3) 43 | const uv = new Float32Array(num * 2) 44 | const index = 45 | num > 65536 ? new Uint32Array(numIndices) : new Uint16Array(numIndices) 46 | 47 | let i = 0 48 | let ii = 0 49 | 50 | // left, right 51 | Plane.buildPlane( 52 | position, 53 | normal, 54 | uv, 55 | index, 56 | depth, 57 | height, 58 | width, 59 | dSegs, 60 | hSegs, 61 | 2, 62 | 1, 63 | 0, 64 | -1, 65 | -1, 66 | i, 67 | ii 68 | ) 69 | i += (dSegs + 1) * (hSegs + 1) 70 | ii += dSegs * hSegs 71 | 72 | Plane.buildPlane( 73 | position, 74 | normal, 75 | uv, 76 | index, 77 | depth, 78 | height, 79 | -width, 80 | dSegs, 81 | hSegs, 82 | 2, 83 | 1, 84 | 0, 85 | 1, 86 | -1, 87 | i, 88 | ii 89 | ) 90 | i += (dSegs + 1) * (hSegs + 1) 91 | ii += dSegs * hSegs 92 | 93 | // top, bottom 94 | Plane.buildPlane( 95 | position, 96 | normal, 97 | uv, 98 | index, 99 | width, 100 | depth, 101 | height, 102 | dSegs, 103 | wSegs, 104 | 0, 105 | 2, 106 | 1, 107 | 1, 108 | 1, 109 | i, 110 | ii 111 | ) 112 | i += (wSegs + 1) * (dSegs + 1) 113 | ii += wSegs * dSegs 114 | 115 | Plane.buildPlane( 116 | position, 117 | normal, 118 | uv, 119 | index, 120 | width, 121 | depth, 122 | -height, 123 | dSegs, 124 | wSegs, 125 | 0, 126 | 2, 127 | 1, 128 | 1, 129 | -1, 130 | i, 131 | ii 132 | ) 133 | i += (wSegs + 1) * (dSegs + 1) 134 | ii += wSegs * dSegs 135 | 136 | // front, back 137 | Plane.buildPlane( 138 | position, 139 | normal, 140 | uv, 141 | index, 142 | width, 143 | height, 144 | -depth, 145 | wSegs, 146 | hSegs, 147 | 0, 148 | 1, 149 | 2, 150 | -1, 151 | -1, 152 | i, 153 | ii 154 | ) 155 | i += (wSegs + 1) * (hSegs + 1) 156 | ii += wSegs * hSegs 157 | 158 | Plane.buildPlane( 159 | position, 160 | normal, 161 | uv, 162 | index, 163 | width, 164 | height, 165 | depth, 166 | wSegs, 167 | hSegs, 168 | 0, 169 | 1, 170 | 2, 171 | 1, 172 | -1, 173 | i, 174 | ii 175 | ) 176 | 177 | const attr = Object.assign({}, attributes, { 178 | position: new GeometryAttribute({ size: 3, data: position }), 179 | normal: new GeometryAttribute({ size: 3, data: normal }), 180 | uv: new GeometryAttribute({ size: 2, data: uv }), 181 | index: new GeometryAttribute({ data: index }) 182 | }) 183 | 184 | super(gl, attr) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/lib/geometry/GeometryAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext, WTCGLGeometryAttribute } from '../types' 2 | 3 | export interface GeometryAttributeOptions { 4 | size?: number 5 | stride?: number 6 | offset?: number 7 | count?: number 8 | instanced?: number 9 | type?: GLenum 10 | normalized?: boolean 11 | data: Float32Array | Float64Array | Uint16Array | Uint32Array 12 | } 13 | 14 | /** 15 | * Class representing a geometry attribute. A discrete piece of data used to render some geometry. 16 | **/ 17 | export class GeometryAttribute implements WTCGLGeometryAttribute { 18 | /** 19 | * The size of each element in the attribute. For example if you're describing 3D vectors, this would be 3. 20 | * */ 21 | size: number = 1 22 | /** 23 | * How big a stride should this attribute have. Should be 0 for all attributes that are uncombined. 24 | */ 25 | stride: number = 0 26 | /** 27 | * How many bytes to offset when passing in the buffer 28 | */ 29 | offset: number = 0 30 | /** 31 | * the number of elements in the attribute 32 | */ 33 | count: number 34 | /** 35 | * The divisor, used in instanced attributes 36 | */ 37 | divisor: number = 0 38 | /** 39 | * The number of instances for this attribute 40 | */ 41 | instanced: number 42 | 43 | /** 44 | * A typed array of data for the attribute 45 | */ 46 | data: Float32Array | Float64Array | Uint16Array | Uint32Array 47 | /** 48 | * The WebGL buffer containing the static attribute data 49 | */ 50 | buffer: WebGLBuffer 51 | 52 | /** 53 | * default gl.UNSIGNED_SHORT for 'index', gl.FLOAT for others 54 | */ 55 | type: GLenum 56 | /** 57 | * gl.ELEMENT_ARRAY_BUFFER or gl.ARRAY_BUFFER depending on whether this is an index attribute or not 58 | */ 59 | target: GLenum 60 | 61 | /** 62 | * whether integer data values should be normalized into a certain range when being cast to a float. 63 | */ 64 | normalized: boolean 65 | 66 | /** 67 | * whether this attribute needs an update. Set after the attribute changes to have it recast to memory 68 | */ 69 | needsUpdate: boolean = false 70 | 71 | /** 72 | * Create a geometry attribute 73 | * @param {Object} __namedParameters - The parameters for the attribute 74 | * @param {number} size - The size of the attribute elements, this will determine the type of attribute in the shader. 75 | * @param {number} stride - How many bytes to stride by in instances of combined attributes 76 | * @param {number} offset - How many bytes to offset the attribute. 77 | * @param {number} instanced - The number of instances this attribute has 78 | * @param {GLenum} type - FLOAT, UNSIGNED_SHORT, or UNSIGNED_INT 79 | * @param {boolean} normalized - whether integer data values should be normalized into a certain range when being cast to a float. 80 | */ 81 | constructor({ 82 | size = 1, 83 | stride = 0, 84 | offset = 0, 85 | instanced = 0, 86 | count, 87 | type, 88 | normalized = false, 89 | data 90 | }: GeometryAttributeOptions) { 91 | this.size = size 92 | this.stride = stride 93 | this.offset = offset 94 | this.instanced = instanced 95 | this.data = data 96 | this.type = 97 | type || 98 | (this.data.constructor === Float32Array 99 | ? window.WebGLRenderingContext.FLOAT 100 | : this.data.constructor === Uint16Array 101 | ? window.WebGLRenderingContext.UNSIGNED_SHORT 102 | : window.WebGLRenderingContext.UNSIGNED_INT) 103 | this.normalized = normalized 104 | 105 | this.count = count || stride ? data.byteLength / stride : data.length / size 106 | this.divisor = instanced || 0 107 | } 108 | /** 109 | * Udpate an attribute for rendering 110 | * @param {WTCGLRenderingContext} gl - The WTCGL rendering context. 111 | */ 112 | updateAttribute(gl: WTCGLRenderingContext): void { 113 | if (gl.renderer.state.boundBuffer !== this.buffer) { 114 | gl.bindBuffer(this.target, this.buffer) 115 | gl.renderer.state.boundBuffer = this.buffer 116 | } 117 | 118 | gl.bufferData(this.target, this.data, gl.STATIC_DRAW) 119 | 120 | this.needsUpdate = false 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/lib/recipes/FragmentShader/index.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from 'wtc-math' 2 | 3 | import type { WTCGLRenderingContext, WTCGLUniformArray } from '../../types' 4 | import type { Framebuffer } from '../../ext/Framebuffer' 5 | import { Renderer } from '../../core/Renderer' 6 | import { Program } from '../../core/Program' 7 | import { Mesh } from '../../core/Mesh' 8 | import { Triangle } from '../../geometry/Triangle' 9 | import { Uniform } from '../../core/Uniform' 10 | 11 | import defaultShaderF from './default-shader-frag.frag' 12 | import defaultShaderV from './default-shader-vert.vert' 13 | 14 | export interface FragmentShaderOptions { 15 | vertex: string 16 | fragment: string 17 | dimensions: Vec2 18 | container: HTMLElement 19 | autoResize: boolean 20 | uniforms: WTCGLUniformArray 21 | onInit: (renderer: Renderer) => void 22 | onBeforeRender: (delta: number) => void 23 | onAfterRender: (delta: number) => void 24 | rendererProps: object 25 | } 26 | 27 | const hasWindow = typeof window !== 'undefined' 28 | 29 | export class FragmentShader { 30 | uniforms: WTCGLUniformArray 31 | dimensions: Vec2 32 | autoResize: boolean = true 33 | onBeforeRender: (t: number) => void 34 | onAfterRender: (t: number) => void 35 | 36 | u_time: Uniform 37 | u_resolution: Uniform 38 | 39 | gl: WTCGLRenderingContext 40 | renderer: Renderer 41 | program: Program 42 | mesh: Mesh 43 | 44 | lastTime: number = 0 45 | 46 | constructor({ 47 | vertex = defaultShaderV, 48 | fragment = defaultShaderF, 49 | dimensions = hasWindow 50 | ? new Vec2(window.innerWidth, window.innerHeight) 51 | : new Vec2(500, 500), 52 | container = document.body, 53 | autoResize = true, 54 | uniforms = {}, 55 | onInit = () => {}, 56 | onBeforeRender = () => {}, 57 | onAfterRender = () => {}, 58 | rendererProps = {} 59 | }: Partial = {}) { 60 | this.onBeforeRender = onBeforeRender.bind(this) 61 | this.onAfterRender = onAfterRender.bind(this) 62 | this.render = this.render.bind(this) 63 | this.resize = this.resize.bind(this) 64 | this.autoResize = autoResize 65 | 66 | this.dimensions = dimensions 67 | 68 | this.u_time = new Uniform({ name: 'time', value: 0, kind: 'float' }) 69 | this.u_resolution = new Uniform({ 70 | name: 'resolution', 71 | value: this.dimensions.array, 72 | kind: 'float_vec2' 73 | }) 74 | 75 | this.uniforms = Object.assign({}, uniforms, { 76 | u_time: this.u_time, 77 | u_resolution: this.u_resolution 78 | }) 79 | 80 | this.renderer = new Renderer(rendererProps) 81 | onInit(this.renderer) 82 | this.gl = this.renderer.gl 83 | container.appendChild(this.gl.canvas) 84 | this.gl.clearColor(1, 1, 1, 1) 85 | 86 | if (this.autoResize && hasWindow) { 87 | window.addEventListener('resize', this.resize, false) 88 | this.resize() 89 | } else { 90 | this.renderer.dimensions = dimensions 91 | this.u_resolution.value = this.dimensions.scaleNew( 92 | this.renderer.dpr 93 | ).array 94 | } 95 | 96 | const geometry = new Triangle(this.gl) 97 | 98 | this.program = new Program(this.gl, { 99 | vertex, 100 | fragment, 101 | uniforms: this.uniforms 102 | }) 103 | 104 | this.mesh = new Mesh(this.gl, { geometry, program: this.program }) 105 | 106 | this.playing = true 107 | } 108 | 109 | resize() { 110 | this.dimensions = hasWindow 111 | ? new Vec2(window.innerWidth, window.innerHeight) 112 | : new Vec2(500, 500) 113 | this.u_resolution.value = this.dimensions.scaleNew(this.renderer.dpr).array 114 | this.renderer.dimensions = this.dimensions 115 | } 116 | 117 | resetTime() { 118 | this.lastTime = 0 119 | } 120 | 121 | render(t: number) { 122 | const diff = t - this.lastTime 123 | this.lastTime = t 124 | 125 | if (this.playing) { 126 | requestAnimationFrame(this.render) 127 | } 128 | 129 | const v: number = this.u_time.value as number 130 | this.u_time.value = v + diff * 0.00005 131 | 132 | this.onBeforeRender(t) 133 | 134 | if (this.post) this.post.render(this.renderer, { scene: this.mesh }) 135 | else this.renderer.render({ scene: this.mesh }) 136 | 137 | this.onAfterRender(t) 138 | } 139 | 140 | #post: Framebuffer 141 | set post(p) { 142 | this.#post = p 143 | } 144 | get post() { 145 | return this.#post || null 146 | } 147 | 148 | #playing: boolean = false 149 | set playing(v: boolean) { 150 | if (this.#playing !== true && v === true) { 151 | requestAnimationFrame(this.render) 152 | this.#playing = true 153 | } else if (v == false) { 154 | this.lastTime = 0 155 | this.#playing = false 156 | } 157 | } 158 | get playing(): boolean { 159 | return this.#playing === true 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /demos/Camera-and-instancing/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Renderer, 3 | Uniform, 4 | Triangle, 5 | DollyCamera, 6 | Vec2, 7 | Program, 8 | Mesh, 9 | GeometryAttribute, 10 | Drawable, 11 | Vec3, 12 | Mat4, 13 | Framebuffer 14 | } from '../../src/lib' 15 | 16 | import '../style.css' 17 | 18 | import fragment from './main.frag' 19 | import vertex from './main.vert' 20 | import screenspace from './post.vert' 21 | import post from './post.frag' 22 | 23 | const numT = 3 24 | const dimensions = new Vec2(window.innerWidth, window.innerHeight) 25 | 26 | const initBlurBuffer = (renderer, u) => { 27 | const gl = renderer.gl 28 | const uniforms = { 29 | u_bd: new Uniform({ name: 'bd', value: [0, 1], kind: '2fv' }), 30 | u_time: u.u_time, 31 | u_resolution: u.u_resolution, 32 | b_render: new Uniform({ 33 | name: 'render', 34 | value: null, 35 | kind: 'texture' 36 | }) 37 | } 38 | 39 | const geometry = new Triangle(gl) 40 | const mainProgram = new Program(gl, { 41 | vertex: screenspace, 42 | fragment: post, 43 | uniforms: uniforms 44 | }) 45 | const mainMesh = new Mesh(gl, { geometry, program: mainProgram }) 46 | const mainFBO = new Framebuffer(gl, { 47 | dpr: renderer.dpr, 48 | name: 'render', 49 | width: dimensions.width, 50 | height: dimensions.height 51 | }) 52 | 53 | const passes = 4 54 | const render = (d) => { 55 | for (let i = 0; i < passes; i++) { 56 | uniforms['u_bd'].value = [0, 1] 57 | uniforms[`b_render`].value = mainFBO.read.texture 58 | 59 | mainFBO.render(renderer, { scene: mainMesh }) 60 | 61 | uniforms['u_bd'].value = [1, 0] 62 | uniforms[`b_render`].value = mainFBO.read.texture 63 | 64 | if (i == passes - 1) renderer.render({ scene: mainMesh }) 65 | else mainFBO.render(renderer, { scene: mainMesh }) 66 | } 67 | } 68 | 69 | return { mainFBO, render } 70 | } 71 | 72 | const initWebgl = () => { 73 | const rendererProps = { antialias: true, premultipliedAlpha: true, dpr: 2 } 74 | 75 | const mats = [] 76 | const transformationsa = new Float32Array(numT * 16) 77 | for (let i = 0; i < numT; i++) { 78 | mats.push(new Mat4()) 79 | transformationsa.set([...mats[i]], i * 16) 80 | } 81 | const attributes = { 82 | colour: new GeometryAttribute({ 83 | size: 3, 84 | data: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) 85 | }), 86 | transformation: new GeometryAttribute({ 87 | instanced: 1, 88 | size: 16, 89 | data: transformationsa 90 | }) 91 | } 92 | 93 | const dpr = 2 94 | const uniforms = { 95 | u_time: new Uniform({ name: 'time', value: 0.0, kind: 'float' }), 96 | u_resolution: new Uniform({ 97 | name: 'resolution', 98 | value: [...dimensions], 99 | kind: '2fv' 100 | }), 101 | u_transform: new Uniform({ 102 | name: 'transform', 103 | value: [...new Mat4()], 104 | kind: 'Matrix4fv' 105 | }) 106 | } 107 | 108 | const renderer = new Renderer(rendererProps) 109 | const gl = renderer.gl 110 | document.body.appendChild(gl.canvas) 111 | gl.clearColor(0.1, 0.15, 0.2, 1) 112 | gl.enable(gl.BLEND) 113 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE) 114 | gl.colorMask(true, true, true, false) 115 | 116 | const scene = new Drawable(gl) 117 | 118 | const geometry = new Triangle(gl, { 119 | attributes 120 | }) 121 | const program = new Program(gl, { 122 | fragment, 123 | vertex, 124 | uniforms, 125 | cullFace: null, 126 | transparent: true, 127 | depthTest: true, 128 | depthWrite: false 129 | }) 130 | program.setBlendFunc(gl.SRC_ALPHA, gl.ONE) 131 | const mesh = new Mesh(gl, { geometry, program }) 132 | 133 | mesh.setParent(scene) 134 | 135 | const camera = new DollyCamera() 136 | camera.setPosition(0.5, 5.5, 5.85) 137 | camera.fov = 90 138 | camera.lookAt(new Vec3(0, 0, 0)) 139 | 140 | let playing = true 141 | 142 | let { mainFBO, render: renderFramebuffer } = initBlurBuffer( 143 | renderer, 144 | uniforms 145 | ) 146 | 147 | const resize = () => { 148 | dimensions.reset(window.innerWidth, window.innerHeight) 149 | uniforms.u_resolution.value = [...dimensions.scaleNew(dpr)] 150 | renderer.dimensions = dimensions 151 | mainFBO.resize(dimensions.width, dimensions.height) 152 | } 153 | window.addEventListener('resize', resize, false) 154 | resize() 155 | 156 | const run = (d) => { 157 | camera.update() 158 | uniforms.u_time.value += d * 0.0001 159 | 160 | mats[0].rotate(0.01, [0, 1, 0]) 161 | transformationsa.set([...mats[0]], 0) 162 | mats[1].rotate(0.012, [0, 0, 1]) 163 | transformationsa.set([...mats[1]], 16) 164 | mats[2].rotate(0.014, [1, 0, 0]) 165 | transformationsa.set([...mats[2]], 2 * 16) 166 | attributes.transformation.updateAttribute(gl) 167 | 168 | mainFBO.render(renderer, { scene, camera }) 169 | renderFramebuffer(d) 170 | 171 | if (playing) requestAnimationFrame(run) 172 | } 173 | requestAnimationFrame(run) 174 | } 175 | 176 | initWebgl() 177 | -------------------------------------------------------------------------------- /src/lib/ext/Framebuffer.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | import type { Renderer, RenderOptions } from '../core/Renderer' 3 | import { RenderTarget } from '../core/RenderTarget' 4 | 5 | export interface FramebufferOptions { 6 | name: string 7 | width: number 8 | height: number 9 | dpr: number 10 | tiling: number 11 | texdepth: number 12 | minFilter: GLenum 13 | magFilter: GLenum 14 | premultiplyAlpha: boolean 15 | data: Float32Array | null 16 | depth: boolean 17 | generateMipmaps: boolean 18 | } 19 | 20 | export class Framebuffer { 21 | static TEXTYPE_FLOAT = 0 22 | static TEXTYPE_UNSIGNED_BYTE = 1 23 | static TEXTYPE_HALF_FLOAT_OES = 2 24 | static IMAGETYPE_REGULAR = 0 25 | static IMAGETYPE_TILING = 1 26 | static IMAGETYPE_MIRROR = 2 27 | 28 | gl: WTCGLRenderingContext 29 | 30 | name: string 31 | 32 | depth: boolean 33 | 34 | #readFB: RenderTarget 35 | #writeFB: RenderTarget 36 | #width: number 37 | #height: number 38 | #pxRatio: number 39 | #tiling: number = Framebuffer.IMAGETYPE_REGULAR 40 | #texdepth: number = Framebuffer.TEXTYPE_UNSIGNED_BYTE 41 | #data: Float32Array | null 42 | 43 | minFilter 44 | magFilter 45 | premultiplyAlpha 46 | generateMipmaps 47 | 48 | constructor( 49 | gl: WTCGLRenderingContext, 50 | { 51 | name = 'FBO', 52 | width = 512, 53 | height = 512, 54 | dpr = Math.min(window.devicePixelRatio, 2), 55 | tiling = Framebuffer.IMAGETYPE_REGULAR, 56 | texdepth = Framebuffer.TEXTYPE_UNSIGNED_BYTE, 57 | minFilter = gl.LINEAR, 58 | magFilter = minFilter, 59 | premultiplyAlpha = false, 60 | data = null, 61 | depth = true, 62 | generateMipmaps = false 63 | }: Partial = {} 64 | ) { 65 | this.gl = gl 66 | 67 | this.name = name 68 | this.dpr = dpr 69 | this.tiling = tiling 70 | this.texdepth = texdepth 71 | this.data = data 72 | this.depth = depth 73 | 74 | this.minFilter = minFilter 75 | this.magFilter = magFilter 76 | this.premultiplyAlpha = premultiplyAlpha 77 | 78 | this.generateMipmaps = generateMipmaps 79 | 80 | this.resize(width, height) 81 | } 82 | resize(width: number, height: number) { 83 | this.width = width 84 | this.height = height 85 | this.#readFB = this.createFrameBuffer() 86 | this.#writeFB = this.createFrameBuffer() 87 | } 88 | createFrameBuffer() { 89 | const t = this.type 90 | 91 | const internalFormat = t === this.gl.FLOAT ? this.gl.RGBA32F : this.gl.RGBA 92 | 93 | const FB = new RenderTarget(this.gl, { 94 | data: this.data, 95 | width: this.width * this.dpr, 96 | height: this.height * this.dpr, 97 | minFilter: this.minFilter, 98 | magFilter: this.magFilter, 99 | wrapS: this.wrap, 100 | wrapT: this.wrap, 101 | type: this.type, 102 | internalFormat: internalFormat, 103 | premultiplyAlpha: this.premultiplyAlpha, 104 | depth: this.depth, 105 | generateMipmaps: this.generateMipmaps 106 | }) 107 | return FB 108 | } 109 | 110 | swap() { 111 | const temp = this.#readFB 112 | this.#readFB = this.#writeFB 113 | this.#writeFB = temp 114 | } 115 | 116 | render( 117 | renderer: Renderer, 118 | { scene, camera, update = true, clear, viewport }: RenderOptions 119 | ) { 120 | renderer.render({ 121 | scene, 122 | camera, 123 | target: this.#writeFB, 124 | update, 125 | clear, 126 | viewport 127 | }) 128 | if (this.generateMipmaps) { 129 | // this.gl.bindTexture(this.gl.TEXTURE_2D, this.#writeFB.texture) 130 | this.gl.generateMipmap(this.gl.TEXTURE_2D) 131 | } 132 | 133 | this.swap() 134 | } 135 | 136 | get wrap() { 137 | switch (this.#tiling) { 138 | case Framebuffer.IMAGETYPE_REGULAR: 139 | return this.gl.CLAMP_TO_EDGE 140 | case Framebuffer.IMAGETYPE_TILING: 141 | return this.gl.MIRRORED_REPEAT 142 | case Framebuffer.IMAGETYPE_MIRROR: 143 | return this.gl.REPEAT 144 | } 145 | } 146 | 147 | get type() { 148 | switch (this.#texdepth) { 149 | case Framebuffer.TEXTYPE_FLOAT: 150 | return this.gl.FLOAT 151 | case Framebuffer.TEXTYPE_UNSIGNED_BYTE: 152 | return this.gl.UNSIGNED_BYTE 153 | case Framebuffer.TEXTYPE_HALF_FLOAT_OES: { 154 | const e = this.gl.getExtension('OES_texture_half_float') 155 | // console.log(e.HALF_FLOAT_OES) 156 | // t = this.renderer.isWebgl2 ? this.ctx.HALF_FLOAT : e.HALF_FLOAT_OES; 157 | return e?.HALF_FLOAT_OES || this.gl.HALF_FLOAT 158 | } 159 | } 160 | } 161 | 162 | get read() { 163 | return this.#readFB 164 | } 165 | 166 | get write() { 167 | return this.#writeFB 168 | } 169 | 170 | set data(value) { 171 | if (value instanceof Float32Array) this.#data = value 172 | } 173 | get data() { 174 | return this.#data || null 175 | } 176 | 177 | set width(value) { 178 | if (value > 0) this.#width = value 179 | } 180 | get width() { 181 | return this.#width || 1 182 | } 183 | 184 | set height(value) { 185 | if (value > 0) this.#height = value 186 | } 187 | get height() { 188 | return this.#height || 1 189 | } 190 | 191 | set pxRatio(value) { 192 | if (value > 0) this.#pxRatio = value 193 | } 194 | get pxRatio() { 195 | return this.#pxRatio || 1 196 | } 197 | 198 | set dpr(value) { 199 | if (value > 0) this.#pxRatio = value 200 | } 201 | get dpr() { 202 | return this.#pxRatio || 1 203 | } 204 | 205 | set tiling(value) { 206 | if ( 207 | [ 208 | Framebuffer.IMAGETYPE_REGULAR, 209 | Framebuffer.IMAGETYPE_TILING, 210 | Framebuffer.IMAGETYPE_MIRROR 211 | ].indexOf(value) > -1 212 | ) 213 | this.#tiling = value 214 | } 215 | get tiling() { 216 | return this.#tiling 217 | } 218 | 219 | set texdepth(value) { 220 | if ( 221 | [ 222 | Framebuffer.TEXTYPE_FLOAT, 223 | Framebuffer.TEXTYPE_UNSIGNED_BYTE, 224 | Framebuffer.TEXTYPE_HALF_FLOAT_OES 225 | ].indexOf(value) > -1 226 | ) 227 | this.#texdepth = value 228 | } 229 | get texdepth() { 230 | return this.#texdepth 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/lib/core/RenderTarget.ts: -------------------------------------------------------------------------------- 1 | import type { WTCGLRenderingContext } from '../types' 2 | 3 | import { Texture } from './Texture' 4 | 5 | export interface RenderTargetOptions { 6 | data: Float32Array | null 7 | width: number 8 | height: number 9 | target: GLenum 10 | colour: number 11 | depth: boolean 12 | stencil: boolean 13 | depthTexture: Texture | null | boolean 14 | wrapS: GLenum 15 | wrapT: GLenum 16 | minFilter: GLenum 17 | magFilter: GLenum 18 | type: GLenum 19 | format: GLenum 20 | internalFormat: GLenum 21 | unpackAlignment: 1 | 2 | 4 | 8 22 | premultiplyAlpha: boolean 23 | generateMipmaps: boolean 24 | } 25 | 26 | /** 27 | * Create a render target. A render target allows you to render a scene to a texture, instead of to screen. And can be used to either render a different view for composition to a scene, or to create advanced post processing effects. 28 | */ 29 | export class RenderTarget { 30 | /** 31 | * The WTCGL rendering context. 32 | */ 33 | gl: WTCGLRenderingContext 34 | /** 35 | * The WebGL frame buffer object to render to. 36 | */ 37 | buffer: WebGLFramebuffer 38 | /** 39 | * The texture array. If you supply only one colour, you can just output this as you normally would with gl_FragColor, otherwise you need to output one at a time with gl_FragData[x] 40 | */ 41 | textures: Texture[] 42 | /** 43 | * The depth buffer. 44 | */ 45 | depthBuffer: WebGLFramebuffer 46 | /** 47 | * The stencil buffer. 48 | */ 49 | stencilBuffer: WebGLFramebuffer 50 | /** 51 | * The depth-stencil buffer. 52 | */ 53 | depthStencilBuffer: WebGLFramebuffer 54 | /** 55 | * The depth texture. 56 | */ 57 | depthTexture: Texture | null 58 | 59 | /** 60 | * The width of the target. 61 | */ 62 | width: number 63 | /** 64 | * The height of the target. 65 | */ 66 | height: number 67 | 68 | /** 69 | * Whether to render a depth buffer. 70 | */ 71 | depth: boolean 72 | /** 73 | * Whether to render a stencil buffer. 74 | */ 75 | stencil: boolean 76 | 77 | /** 78 | * A GLEnum representing the binding point for the texture / buffer. 79 | * @default gl.FRAMEBUFFER 80 | */ 81 | target: GLenum 82 | 83 | /** 84 | * Create a render target object. 85 | * @param {WTCGLRenderingContext} gl - The WTCGL Rendering context 86 | * @param __namedParameters - The parameters to initialise the renderer. 87 | * @param width - The width of the render target. 88 | * @param height - The height of the render target. 89 | * @param target - The binding point for the frame buffers. 90 | * @param colour - The number of colour attachments to create. 91 | * @param depth - Whether to create a depth buffer 92 | * @param stencil - Whether to create a stencil buffer 93 | * @param wrapS - Wrapping 94 | * @param wrapT - Wrapping 95 | * @param minFilter - The filter to use when rendering smaller 96 | * @param magFilter - The filter to use when enlarging 97 | * @param type - The texture type. Typically one of gl.UNSIGNED_BYTE, gl.FLOAT, ext.HALF_FLOAT_OES 98 | * @param format - The texture format. 99 | * @param internalFormat - the texture internalFormat. 100 | * @param unpackAlignment - The unpack alignment for pixel stores. 101 | * @param premultiplyAlpha - Whether to use premultiplied alpha for stored textures. 102 | */ 103 | constructor( 104 | gl: WTCGLRenderingContext, 105 | { 106 | data, 107 | width = gl.canvas.width, 108 | height = gl.canvas.height, 109 | target = gl.FRAMEBUFFER, 110 | colour = 1, 111 | depth = true, 112 | wrapS = gl.CLAMP_TO_EDGE, 113 | wrapT = gl.CLAMP_TO_EDGE, 114 | minFilter = gl.LINEAR, 115 | magFilter = minFilter, 116 | type = gl.UNSIGNED_BYTE, 117 | format = gl.RGBA, 118 | internalFormat = format, 119 | unpackAlignment = 4, 120 | premultiplyAlpha = false, 121 | generateMipmaps = false 122 | }: Partial = {} 123 | ) { 124 | this.gl = gl 125 | this.width = width 126 | this.height = height 127 | this.depth = depth 128 | this.buffer = this.gl.createFramebuffer()! 129 | this.target = target 130 | this.gl.bindFramebuffer(this.target, this.buffer) 131 | 132 | const e = gl.getExtension('OES_texture_half_float') 133 | if (type === e?.HALF_FLOAT_OES || type === this.gl.HALF_FLOAT) { 134 | if (gl?.renderer?.isWebgl2) { 135 | internalFormat = this.gl.RGBA16F 136 | } 137 | } 138 | 139 | this.textures = [] 140 | const drawBuffers = [] 141 | 142 | // create and attach required num of color textures 143 | for (let i = 0; i < colour; i++) { 144 | this.textures.push( 145 | new Texture(gl, { 146 | data, 147 | width, 148 | height, 149 | wrapS, 150 | wrapT, 151 | minFilter, 152 | magFilter, 153 | type, 154 | format, 155 | internalFormat, 156 | unpackAlignment, 157 | premultiplyAlpha, 158 | flipY: false, 159 | generateMipmaps 160 | }) 161 | ) 162 | this.textures[i].update() 163 | this.gl.framebufferTexture2D( 164 | this.target, 165 | this.gl.COLOR_ATTACHMENT0 + i, 166 | this.gl.TEXTURE_2D, 167 | this.textures[i].texture, 168 | 0 /* level */ 169 | ) 170 | drawBuffers.push(this.gl.COLOR_ATTACHMENT0 + i) 171 | } 172 | 173 | // For multi-render targets shader access 174 | if (drawBuffers.length > 1) this.gl.renderer.drawBuffers(drawBuffers) 175 | 176 | if ( 177 | depth && 178 | (this.gl.renderer.isWebgl2 || 179 | this.gl.renderer.getExtension('WEBGL_depth_texture')) 180 | ) { 181 | this.depthTexture = new Texture(gl, { 182 | width, 183 | height, 184 | minFilter: this.gl.NEAREST, 185 | magFilter: this.gl.NEAREST, 186 | format: this.gl.DEPTH_COMPONENT, 187 | internalFormat: gl.renderer.isWebgl2 188 | ? this.gl.DEPTH_COMPONENT16 189 | : this.gl.DEPTH_COMPONENT, 190 | type: this.gl.UNSIGNED_INT 191 | }) 192 | this.depthTexture.update() 193 | this.gl.framebufferTexture2D( 194 | this.target, 195 | this.gl.DEPTH_ATTACHMENT, 196 | this.gl.TEXTURE_2D, 197 | this.depthTexture.texture, 198 | 0 /* level */ 199 | ) 200 | } 201 | 202 | this.gl.bindFramebuffer(this.target, null) 203 | } 204 | 205 | /** 206 | * Returns the first texture, for the majority os use cases. 207 | */ 208 | get texture() { 209 | return this.textures[0] 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/lib/core/Uniform.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WTCGLRenderingContext, 3 | WTCGLUniformValue, 4 | WTCGLActiveInfo 5 | } from '../types' 6 | 7 | import { Texture } from './Texture' 8 | import { Program } from './Program' 9 | 10 | type Kind = 11 | | 'int' 12 | | 'float' 13 | | 'boolean' 14 | | 'texture' 15 | | 'texture_array' 16 | | 'float_vec2' 17 | | 'float_vec3' 18 | | 'float_vec4' 19 | | 'int_vec2' 20 | | 'int_vec3' 21 | | 'int_vec4' 22 | | 'mat2' 23 | | 'mat3' 24 | | 'mat4' 25 | 26 | export interface UniformOptions { 27 | name: string 28 | value: WTCGLUniformValue 29 | kind: Kind 30 | } 31 | 32 | /** 33 | * A uniform is just a basic container for simple uniform information. 34 | */ 35 | export class Uniform { 36 | /** 37 | * The uniform name. Currently unused, but for future use in auto-parsing name 38 | */ 39 | name: string 40 | /** 41 | * The uniform value. 42 | */ 43 | value: WTCGLUniformValue 44 | /** 45 | * The uniform kind. Currently unused but useful in future for testing against supplied bind type. 46 | */ 47 | kind: Kind 48 | /** 49 | * Create a unform object 50 | * @param __namedParameters 51 | * @param name - The name of the uniform. 52 | * @param value - The value for the uniform. 53 | * @param kind - The type of uniform. 54 | */ 55 | constructor({ 56 | name = 'uniform', 57 | value = [1, 1], 58 | kind = 'float_vec2' 59 | }: Partial = {}) { 60 | this.name = name 61 | this.value = value 62 | this.kind = kind 63 | } 64 | /** 65 | * Set the uniform in the stated program. 66 | * @param gl - The WTCGL rendering context object. 67 | * @param type - A GLEnum representing the passed uniform type. 68 | * @param location - The uniform location (just an address to a uniform in program space) 69 | * @param value - The value of the uniform 70 | */ 71 | setUniform( 72 | gl: WTCGLRenderingContext, 73 | type: GLenum, 74 | location: WebGLUniformLocation, 75 | value: WTCGLUniformValue = this.value 76 | ): void { 77 | const setValue = gl.renderer.state.uniformLocations.get(location) 78 | 79 | if (value instanceof Array) { 80 | if ( 81 | setValue === undefined || 82 | (setValue instanceof Array && setValue.length !== value.length) 83 | ) { 84 | // clone array to store as cache 85 | gl.renderer.state.uniformLocations.set(location, value.slice(0)) 86 | } else if (setValue instanceof Array) { 87 | if (arraysEqual(setValue, value)) return 88 | 89 | // Update cached array values 90 | setArray(setValue, value) 91 | gl.renderer.state.uniformLocations.set(location, setValue) 92 | } 93 | } else { 94 | if (setValue === value) return 95 | gl.renderer.state.uniformLocations.set(location, value) 96 | } 97 | 98 | // Doing this to get around typescript's nonsense 99 | const val = value as Float32List 100 | 101 | switch (type) { 102 | case 5126: { 103 | // FLOAT 104 | if (val instanceof Array) { 105 | return gl.uniform1fv(location, val) 106 | } else if (typeof val === 'number') { 107 | gl.uniform1f(location, val) 108 | } 109 | 110 | return 111 | } 112 | case 35664: 113 | // FLOAT_VEC2 114 | return gl.uniform2fv(location, val) 115 | case 35665: 116 | // FLOAT_VEC3 117 | return gl.uniform3fv(location, val) 118 | case 35666: 119 | // FLOAT_VEC4 120 | return gl.uniform4fv(location, val) 121 | case 35670: // BOOL 122 | case 5124: // INT 123 | case 35678: // SAMPLER_2D 124 | case 35680: 125 | // SAMPLER_CUBE 126 | return val?.length 127 | ? gl.uniform1iv(location, val) 128 | : gl.uniform1i(location, val as unknown as number) 129 | case 35671: // BOOL_VEC2 130 | case 35667: 131 | // INT_VEC2 132 | return gl.uniform2iv(location, val) 133 | case 35672: // BOOL_VEC3 134 | case 35668: 135 | // INT_VEC3 136 | return gl.uniform3iv(location, val) 137 | case 35673: // BOOL_VEC4 138 | case 35669: 139 | // INT_VEC4 140 | return gl.uniform4iv(location, val) 141 | case 35674: 142 | // FLOAT_MAT2 143 | return gl.uniformMatrix2fv(location, false, val) 144 | case 35675: 145 | // FLOAT_MAT3 146 | return gl.uniformMatrix3fv(location, false, val) 147 | case 35676: 148 | if (val === null) return 149 | // FLOAT_MAT4 150 | return gl.uniformMatrix4fv(location, false, val) 151 | } 152 | } 153 | /** 154 | * Binds the uniform to the program. 155 | * @param program - The program to which to bind the uniform. 156 | * @param location - A flag representing the uniform's location in memory. 157 | * @param activeUniform - Representes an extention of the standard Web GL active info for uniform. 158 | */ 159 | bind( 160 | program: Program, 161 | location: WebGLUniformLocation, 162 | activeUniform: WTCGLActiveInfo 163 | ) { 164 | if (this.value === undefined) { 165 | console.warn(`${this.name} uniform is missing a value`) 166 | return 167 | } 168 | 169 | if (this.kind === 'texture' && this.value instanceof Texture) { 170 | const textureUnit = ++program.textureUnit 171 | 172 | // Check if texture needs to be updated 173 | this.value.update(textureUnit) 174 | 175 | return this.setUniform( 176 | program.gl, 177 | activeUniform.type, 178 | location, 179 | textureUnit 180 | ) 181 | } 182 | 183 | // For texture arrays, set uniform as an array of texture units instead of just one 184 | if (this.kind === 'texture_array') { 185 | if (this.value instanceof Array && this.value[0] instanceof Texture) { 186 | const textureUnits: Array = [] 187 | 188 | this.value.forEach((value) => { 189 | const textureUnit = ++program.textureUnit 190 | if (value instanceof Texture) value.update(textureUnit) 191 | textureUnits.push(textureUnit) 192 | }) 193 | 194 | return this.setUniform( 195 | program.gl, 196 | activeUniform.type, 197 | location, 198 | textureUnits as WTCGLUniformValue 199 | ) 200 | } 201 | } 202 | 203 | this.setUniform(program.gl, activeUniform.type, location) 204 | } 205 | } 206 | 207 | function arraysEqual( 208 | a: Texture[] | number[], 209 | b: Texture[] | number[] 210 | ): boolean { 211 | if (a.length !== b.length) return false 212 | for (let i = 0, l = a.length; i < l; i++) { 213 | if (a[i] !== b[i]) return false 214 | } 215 | return true 216 | } 217 | 218 | function setArray(a: Texture[] | number[], b: Texture[] | number[]): void { 219 | for (let i = 0, l = a.length; i < l; i++) { 220 | a[i] = b[i] 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/lib/core/Object.ts: -------------------------------------------------------------------------------- 1 | import { Vec3, Quat, Mat4 } from 'wtc-math' 2 | 3 | /** 4 | * Class representing an object. This provides basic transformations for sub-objects and shouldn't be instanciated directly. 5 | **/ 6 | export class Obj { 7 | /** 8 | * The parent of this object. 9 | */ 10 | parent?: Obj | null 11 | /** 12 | * The children of this object 13 | */ 14 | children: Obj[] 15 | /** 16 | * Whether this objec is visible or not. This will stop the renderer from trying to render this object 17 | */ 18 | visible: boolean 19 | 20 | /** 21 | * A matrix representing the translation, rotation and scale of this object. 22 | */ 23 | matrix: Mat4 24 | /** 25 | * The world matrix represents the function of all ancestor matrices and the matrix of this object 26 | */ 27 | worldMatrix: Mat4 28 | /** 29 | * Whether to automatically calculate the object matrix each time updateWorldMatrix is called. Convenient, but potentially costly. 30 | */ 31 | matrixAutoUpdate: boolean 32 | /** 33 | * A boolean indicating whether the world matrix requires updating. 34 | */ 35 | worldMatrixNeedsUpdate: boolean = false 36 | 37 | /** 38 | * Object position 39 | */ 40 | position: Vec3 41 | /** 42 | * Object rotation, expressed as a quaternion 43 | */ 44 | quaternion: Quat 45 | /** 46 | * Object scale 47 | */ 48 | scale: Vec3 49 | /** 50 | * Object rotation, expressed as a 3D Euler rotation 51 | */ 52 | rotation: Vec3 53 | /** 54 | * The up unit vector 55 | */ 56 | up: Vec3 57 | 58 | /** 59 | * Create an object. Normally only called from an extending class. 60 | */ 61 | constructor() { 62 | this.parent = null 63 | this.children = [] 64 | this.visible = true 65 | 66 | this.matrix = new Mat4() 67 | this.worldMatrix = new Mat4() 68 | this.matrixAutoUpdate = true 69 | 70 | this.position = new Vec3() 71 | this.quaternion = new Quat() 72 | this.scale = new Vec3(1, 1, 1) 73 | this.rotation = new Vec3() 74 | this.up = new Vec3(0, 1, 0) 75 | } 76 | 77 | /** 78 | * UpdateRotation should be called whenever a change to the rotation variable is made. This will update the quaternion in response to the change in the rotation. 79 | * For example: 80 | * ```js 81 | * const Box = new Mesh(gl, { geometry: BoxGeo, program }); 82 | * Box.rotation.x = Math.PI*.25; 83 | * Box.updateRotation(); 84 | * console.log(Box.quaternion) // {_x: 0.3826834323650898, _y: 0, _z: 0, _w: 0.9238795325112867} 85 | * ``` 86 | */ 87 | updateRotation(): void { 88 | this.quaternion = Quat.fromEuler(this.rotation) || new Quat() 89 | } 90 | 91 | /** 92 | * UpdateRotation should be called whenever a change to the quaternion variable is made. This will update the rotation in response to the change in the quaternion. 93 | * For example: 94 | * ```js 95 | * const Box = new Mesh(gl, { geometry: BoxGeo, program }); 96 | * Box.quaternion = Quat.fromAxisAngle(new Vec3(1,0,0), Math.PI*-.25); 97 | * Box.updateQuaternion(); 98 | * console.log(Box.rotation) // {_x: 0.7853981633974483, _y: 0, _z: 0} 99 | * ``` 100 | */ 101 | updateQuaternion(): void { 102 | const rotMat = Mat4.fromQuat(this.quaternion) 103 | this.rotation = Vec3.fromRotationMatrix(rotMat) || new Vec3() 104 | } 105 | 106 | /** 107 | * Sets the parent of this object and indicates whether to set a child relationship on the parent. 108 | * @param parent - The parent object to use 109 | * @param notifyParent - Whether to set the full parent-child relationsjip 110 | */ 111 | setParent(parent: Obj | null, notifyParent: boolean = true): void { 112 | if (this.parent && parent !== this.parent) 113 | this.parent.removeChild(this, false) 114 | this.parent = parent 115 | if (notifyParent && parent) parent.addChild(this, false) 116 | } 117 | 118 | /** 119 | * Adds a child object to this and indicates whether to notify the child 120 | * @param child - The object to add as a child of this one 121 | * @param notifyChild - Whether to set the parent of the indicated child to this 122 | */ 123 | addChild(child: Obj, notifyChild: boolean = true): void { 124 | if (!~this.children.indexOf(child)) this.children.push(child) 125 | if (notifyChild) child.setParent(this, false) 126 | } 127 | 128 | /** 129 | * Remove a child from this object. 130 | * @param child - The child to remove 131 | * @param notifyChild - Whether to notify the child of the removal 132 | */ 133 | removeChild(child: Obj, notifyChild: boolean = true): void { 134 | if (~this.children.indexOf(child)) 135 | this.children.splice(this.children.indexOf(child), 1) 136 | if (notifyChild) child.setParent(null, false) 137 | } 138 | 139 | /** 140 | * Update the fill world matrix of this object basically used for recursively looping down the hierarchy when a change in the RTS matrix is changed on this. 141 | * @param force Even is this is not set to world matrix update, force it. Used when looping down past the first iteration. 142 | */ 143 | updateMatrixWorld(force?: boolean): void { 144 | if (this.matrixAutoUpdate) this.updateMatrix() 145 | if (this.worldMatrixNeedsUpdate || force) { 146 | if (this.parent === null || this.parent === undefined) 147 | this.worldMatrix = this.matrix.clone() 148 | else this.worldMatrix = this.parent.worldMatrix.multiplyNew(this.matrix) 149 | this.worldMatrixNeedsUpdate = false 150 | force = true 151 | } 152 | 153 | for (let i = 0, l = this.children.length; i < l; i++) { 154 | this.children[i].updateMatrixWorld(force) 155 | } 156 | } 157 | 158 | /** 159 | * Updates the object matrix based on the rotation, translation and scale , then runs an update. 160 | */ 161 | updateMatrix(): void { 162 | this.matrix = Mat4.fromRotationTranslationScale( 163 | this.quaternion, 164 | this.position, 165 | this.scale 166 | ) 167 | this.worldMatrixNeedsUpdate = true 168 | } 169 | 170 | /** 171 | * Traverses the object tree and runs a given function against the object. If true is returned, the traversal is stopped. 172 | * This is useful for testing things like visibility etc. 173 | * @param callback - The function to call, it should return true or null based on some condition 174 | */ 175 | traverse(callback: (node: Obj) => boolean | null): void { 176 | // Return true in callback to stop traversing children 177 | if (callback(this)) return 178 | for (let i = 0, l = this.children.length; i < l; i++) { 179 | this.children[i].traverse(callback) 180 | } 181 | } 182 | 183 | /** 184 | * Decomposes the object matrix into the various components necessary to populate this object. 185 | */ 186 | decompose(): void { 187 | this.position = this.matrix.translation 188 | this.quaternion = this.matrix.rotation 189 | this.scale = this.matrix.scaling 190 | const rotMat = Mat4.fromQuat(this.quaternion) 191 | this.rotation = Vec3.fromRotationMatrix(rotMat) || new Vec3() 192 | } 193 | 194 | /** 195 | * Makes the object look at a particular target, useful for cameras 196 | * @param target - The point to look at 197 | * @param invert - Look away from, if true 198 | */ 199 | lookAt(target: Vec3, invert: boolean = false): void { 200 | if (invert) this.matrix = Mat4.targetTo(this.position, target, this.up) 201 | else this.matrix = Mat4.targetTo(target, this.position, this.up) 202 | this.quaternion = this.matrix.rotation 203 | const rotMat = Mat4.fromQuat(this.quaternion) 204 | this.rotation = Vec3.fromRotationMatrix(rotMat) || new Vec3() 205 | this.updateRotation() 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/lib/recipes/ParticleSimulation/index.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from 'wtc-math' 2 | 3 | import type { WTCGLRenderingContext, WTCGLUniformArray } from '../../types' 4 | import { Renderer } from '../../core/Renderer' 5 | import { Program } from '../../core/Program' 6 | import { Mesh } from '../../core/Mesh' 7 | import { Uniform } from '../../core/Uniform' 8 | import { PointCloud } from '../../geometry/PointCloud' 9 | import { GeometryAttribute } from '../../geometry/GeometryAttribute' 10 | import { Camera } from '../../core/Camera' 11 | import { Framebuffer } from '../../ext/Framebuffer' 12 | 13 | import defaultShaderF from './default-shader-frag.frag' 14 | import defaultShaderV from './default-shader-vert.vert' 15 | 16 | const hasWindow = typeof window !== 'undefined' 17 | 18 | export interface ParticleSimulationOptions { 19 | vertex: string 20 | fragment: string 21 | dimensions: Vec2 22 | container: HTMLElement 23 | autoResize: boolean 24 | uniforms: WTCGLUniformArray 25 | onBeforeRender: (delta: number) => void 26 | onAfterRender: (delta: number) => void 27 | textureSize: number 28 | simDimensions: number 29 | createGeometry: () => void 30 | rendererProps: object 31 | } 32 | 33 | export class ParticleSimulation { 34 | uniforms 35 | dimensions 36 | autoResize = true 37 | onBeforeRender 38 | onAfterRender 39 | 40 | u_time 41 | u_resolution 42 | 43 | gl: WTCGLRenderingContext 44 | renderer: Renderer 45 | program: Program 46 | mesh: Mesh 47 | 48 | textureSize: number 49 | particles: number 50 | textureArraySize: number 51 | simDimensions: number 52 | 53 | references: Float32Array 54 | 55 | cloud: PointCloud 56 | 57 | lastTime = 0 58 | 59 | constructor({ 60 | vertex = defaultShaderV, 61 | fragment = defaultShaderF, 62 | dimensions = hasWindow 63 | ? new Vec2(window.innerWidth, window.innerHeight) 64 | : new Vec2(500, 500), 65 | container = document.body, 66 | autoResize = true, 67 | uniforms = {}, 68 | onBeforeRender = () => {}, 69 | onAfterRender = () => {}, 70 | textureSize = 128, 71 | simDimensions = 3, 72 | createGeometry, 73 | rendererProps = {} 74 | }: Partial = {}) { 75 | this.onBeforeRender = onBeforeRender.bind(this) 76 | this.onAfterRender = onAfterRender.bind(this) 77 | this.render = this.render.bind(this) 78 | this.resize = this.resize.bind(this) 79 | this.autoResize = autoResize 80 | 81 | this.dimensions = dimensions 82 | 83 | this.u_time = new Uniform({ name: 'time', value: 0, kind: 'float' }) 84 | this.u_resolution = new Uniform({ 85 | name: 'resolution', 86 | value: this.dimensions.array, 87 | kind: 'float_vec2' 88 | }) 89 | 90 | this.uniforms = Object.assign({}, uniforms, { 91 | u_time: this.u_time, 92 | u_resolution: this.u_resolution 93 | }) 94 | 95 | this.renderer = new Renderer(rendererProps) 96 | this.gl = this.renderer.gl 97 | this.gl.clearColor(0, 0, 0, 1) 98 | 99 | if (this.autoResize && hasWindow) { 100 | window.addEventListener('resize', this.resize, false) 101 | this.resize() 102 | } else { 103 | this.renderer.dimensions = dimensions 104 | } 105 | 106 | this.textureSize = textureSize 107 | this.particles = Math.pow(this.textureSize, 2) 108 | this.textureArraySize = this.particles * 4 109 | this.simDimensions = simDimensions 110 | 111 | this.references = new Float32Array(this.particles * 2).fill(0) // The references - used for texture lookups 112 | 113 | if (createGeometry && typeof createGeometry === 'function') { 114 | createGeometry.bind(this)() 115 | } else { 116 | this.createGeometry() 117 | } 118 | 119 | this.program = new Program(this.gl, { 120 | vertex, 121 | fragment, 122 | uniforms: this.uniforms 123 | }) 124 | 125 | this.mesh = new Mesh(this.gl, { 126 | mode: this.gl.POINTS, 127 | geometry: this.cloud, 128 | program: this.program 129 | }) 130 | 131 | this.playing = true 132 | 133 | container.appendChild(this.gl.canvas) 134 | } 135 | 136 | createGeometry() { 137 | this.cloud = new PointCloud(this.gl, { 138 | fillFunction: (points, dimensions) => { 139 | for (let i = 0; i < points.length; i += 2) { 140 | const index = i / 2 141 | 142 | const pos = new Vec2( 143 | index % this.textureSize, 144 | Math.floor(index / this.textureSize) 145 | ) 146 | 147 | this.references[i] = pos.x / this.textureSize // x position of the texture particle representing this ant 148 | this.references[i + 1] = pos.y / this.textureSize // y position of the texture particle representing this ant 149 | } 150 | for (let i = 0; i < points.length; i += dimensions) { 151 | for (let j = 0; j < dimensions; j++) { 152 | points[i + j] = Math.random() * 400 - 200 // n position 153 | } 154 | } 155 | }, 156 | particles: this.particles, 157 | dimensions: this.simDimensions, 158 | attributes: { 159 | reference: new GeometryAttribute({ size: 2, data: this.references }) 160 | } 161 | }) 162 | } 163 | 164 | render(t: number) { 165 | const diff = t - this.lastTime 166 | this.lastTime = t 167 | 168 | if (this.playing) { 169 | requestAnimationFrame(this.render) 170 | } 171 | 172 | const v = this.u_time.value as number 173 | this.u_time.value = v + diff * 0.00005 174 | 175 | this.onBeforeRender(t) 176 | 177 | if (this.post) 178 | this.post.render(this.renderer, { 179 | scene: this.mesh, 180 | camera: this.camera, 181 | update: this.update, 182 | sort: this.sort, 183 | frustumCull: this.frustumCull, 184 | clear: this.clear, 185 | viewport: this.viewport 186 | }) 187 | else 188 | this.renderer.render({ 189 | scene: this.mesh, 190 | camera: this.camera, 191 | update: this.update, 192 | sort: this.sort, 193 | frustumCull: this.frustumCull, 194 | clear: this.clear, 195 | viewport: this.viewport 196 | }) 197 | 198 | this.onAfterRender(t) 199 | } 200 | 201 | resize() { 202 | this.dimensions = hasWindow 203 | ? new Vec2(window.innerWidth, window.innerHeight) 204 | : new Vec2(500, 500) 205 | this.u_resolution.value = this.dimensions.scaleNew(this.renderer.dpr).array 206 | this.renderer.dimensions = this.dimensions 207 | } 208 | 209 | #post: Framebuffer 210 | set post(p) { 211 | this.#post = p 212 | } 213 | get post() { 214 | return this.#post || null 215 | } 216 | 217 | #playing = false 218 | set playing(v) { 219 | if (this.#playing !== true && v === true) { 220 | requestAnimationFrame(this.render) 221 | this.#playing = true 222 | } else if (v == false) { 223 | this.#playing = false 224 | } 225 | } 226 | get playing() { 227 | return this.#playing === true 228 | } 229 | 230 | // Getters and setters for renderer 231 | 232 | #camera: undefined | Camera 233 | set camera(v) { 234 | if (v == null || v instanceof Camera) { 235 | this.#camera = v 236 | } 237 | } 238 | get camera() { 239 | return this.#camera 240 | } 241 | 242 | #update: boolean = true 243 | set update(v) { 244 | this.#update = v === true 245 | } 246 | get update() { 247 | return this.#update 248 | } 249 | 250 | #sort: boolean = true 251 | set sort(v) { 252 | this.#sort = v === true 253 | } 254 | get sort() { 255 | return this.#sort 256 | } 257 | 258 | #frustumCull: boolean = true 259 | set frustumCull(v) { 260 | this.#frustumCull = v === true 261 | } 262 | get frustumCull() { 263 | return this.#frustumCull 264 | } 265 | 266 | #clear: boolean | undefined 267 | set clear(v) { 268 | this.#clear = v === true 269 | } 270 | get clear() { 271 | return this.#clear 272 | } 273 | 274 | #viewport: [Vec2, Vec2] | undefined 275 | set viewport(v) { 276 | if ( 277 | (v instanceof Array && v[0] instanceof Vec2 && v[1] instanceof Vec2) || 278 | v == null 279 | ) 280 | this.#viewport = v 281 | } 282 | get viewport() { 283 | return this.#viewport 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/lib/core/Mesh.ts: -------------------------------------------------------------------------------- 1 | import { Mat3, Mat4 } from 'wtc-math' 2 | 3 | import type { WTCGLRenderingContext } from '../types' 4 | import { Geometry } from '../geometry/Geometry' 5 | 6 | import { Drawable } from './Drawable' 7 | import { Program } from './Program' 8 | import { Camera } from './Camera' 9 | import { Uniform } from './Uniform' 10 | 11 | type BaseMeshOptions = { mesh: Mesh; camera: Camera } 12 | 13 | type MeshCallback = (props: BaseMeshOptions) => void 14 | 15 | export interface MeshOptions { 16 | geometry: Geometry 17 | program: Program 18 | mode?: GLenum 19 | frustumCulled?: boolean 20 | renderOrder?: number 21 | } 22 | 23 | /** 24 | * Class representing a mesh. A mesh is a binding point between geometry and a program. 25 | * @extends Drawable 26 | **/ 27 | export class Mesh extends Drawable { 28 | /** 29 | * The geometry to render. 30 | */ 31 | geometry: Geometry 32 | 33 | /** 34 | * The mode to use to draw this mesh. Can be one of: 35 | * - gl.POINTS: Draws a single dot for each vertex 36 | * - gl.LINE_STRIP: Draws a straight line between the vertices 37 | * - gl.LINE_LOOP: As above, but loops back. 38 | * - gl.LINES: Draws many lines between each vertex pair 39 | * - gl.TRIANGLE_STRIP: Draws a series of triangles between each vertice trio 40 | * - gl.TRIANGLE_FAN: Draws a series of triangles between each vertice trio, treating the first vertex as the origin of each triangle 41 | * - gl.TRIANGLES: Draws a triangle for a group of three vertices 42 | */ 43 | mode: GLenum 44 | 45 | /** 46 | * The world matrix projected by the camera's view matrix 47 | */ 48 | modelViewMatrix: Mat4 49 | /** 50 | * The model-view normal matrix 51 | */ 52 | normalMatrix: Mat3 53 | /** 54 | * Any callbacks to run before render 55 | */ 56 | beforeRenderCallbacks: { (props: BaseMeshOptions): void }[] 57 | /** 58 | * Any callbacks to run after render 59 | */ 60 | afterRenderCallbacks: { (props: BaseMeshOptions): void }[] 61 | 62 | /** 63 | * Create a mesh 64 | * @param {WTCGLRenderingContext} gl - The WTCGL Rendering context 65 | * @param {Object} __namedParameters - The parameters for the attribute 66 | * @param {Geometry} geometry - The geometry for the mesh to render 67 | * @param {Program} program - The program - shaders and uniforms - used to render the geometry 68 | * @param {GLenum} mode - The mode to render using 69 | * @param {boolean} frustumCulled - Whether to apply culling to this object 70 | * @param {number} renderOrder - The explicit render order of the object. If this is zero, then it will be instead calculated at render time. 71 | */ 72 | constructor( 73 | gl: WTCGLRenderingContext, 74 | { 75 | geometry, 76 | program, 77 | mode = gl.TRIANGLES, 78 | frustumCulled = true, 79 | renderOrder = 0 80 | }: MeshOptions 81 | ) { 82 | super(gl, { frustumCulled, renderOrder }) 83 | 84 | if (!gl.canvas) console.error('gl not passed as first argument to Mesh') 85 | 86 | this.geometry = geometry 87 | this.program = program 88 | this.mode = mode 89 | 90 | // Override sorting to force an order 91 | this.modelViewMatrix = new Mat4() 92 | this.normalMatrix = new Mat3() 93 | this.beforeRenderCallbacks = [] 94 | this.afterRenderCallbacks = [] 95 | } 96 | 97 | /** 98 | * Add a before render callback. 99 | * @param {function} f - The function to call before render. Expected shape ({ mesh: this, camera })=>void. 100 | **/ 101 | addBeforeRender(f: MeshCallback) { 102 | this.beforeRenderCallbacks.push(f) 103 | return this 104 | } 105 | 106 | /** 107 | * Remove a before render callback. 108 | * @param {function} f - The function to call before render. Expected shape ({ mesh: this, camera })=>void. 109 | **/ 110 | removeBeforeRender(f: MeshCallback) { 111 | this.beforeRenderCallbacks.forEach((_f, i) => { 112 | if (_f == f) this.beforeRenderCallbacks.splice(i, 1) 113 | }) 114 | } 115 | 116 | /** 117 | * Remove all before render callbacks 118 | */ 119 | removeAllBeforeRender() { 120 | this.beforeRenderCallbacks = [] 121 | } 122 | 123 | /** 124 | * Add an after render callback. 125 | * @param {function} f - The function to call before render. Expected shape ({ mesh: this, camera })=>void. 126 | **/ 127 | addAfterRender(f: MeshCallback) { 128 | this.afterRenderCallbacks.push(f) 129 | return this 130 | } 131 | 132 | /** 133 | * Remove an after render callback. 134 | * @param {function} f - The function to call before render. Expected shape ({ mesh: this, camera })=>void. 135 | **/ 136 | removeAfterRender(f: MeshCallback) { 137 | this.afterRenderCallbacks.forEach((_f, i) => { 138 | if (_f == f) this.afterRenderCallbacks.splice(i, 1) 139 | }) 140 | } 141 | 142 | /** 143 | * Remove all afyer render callbacks 144 | */ 145 | removeAllAfterRender() { 146 | this.afterRenderCallbacks = [] 147 | } 148 | 149 | /** 150 | * Drw the mesh. If a camera is supplied to the draw call, its various matrices will be added to the program uniforms 151 | * @param {Camera} camera - The camera to use to supply transformation matrices 152 | */ 153 | draw({ camera }: { camera: Camera }): void { 154 | this.beforeRenderCallbacks.forEach((f) => f && f({ mesh: this, camera })) 155 | 156 | if (camera) { 157 | // Add empty matrix uniforms to program if unset 158 | if (!this.program.uniforms.modelMatrix) { 159 | Object.assign(this.program.uniforms, { 160 | u_modelMatrix: new Uniform({ 161 | name: 'modelMatrix', 162 | value: undefined, 163 | kind: 'mat4' 164 | }), 165 | u_viewMatrix: new Uniform({ 166 | name: 'viewMatrix', 167 | value: undefined, 168 | kind: 'mat4' 169 | }), 170 | u_modelViewMatrix: new Uniform({ 171 | name: 'modelViewMatrix', 172 | value: undefined, 173 | kind: 'mat4' 174 | }), 175 | u_normalMatrix: new Uniform({ 176 | name: 'normalMatrix', 177 | value: undefined, 178 | kind: 'mat3' 179 | }), 180 | u_projectionMatrix: new Uniform({ 181 | name: 'projectionMatrix', 182 | value: undefined, 183 | kind: 'mat4' 184 | }), 185 | u_cameraPosition: new Uniform({ 186 | name: 'cameraPosition', 187 | value: undefined, 188 | kind: 'float_vec3' 189 | }), 190 | u_objectPosition: new Uniform({ 191 | name: 'objectPosition', 192 | value: undefined, 193 | kind: 'float_vec3' 194 | }) 195 | }) 196 | } 197 | 198 | // Set the matrix uniforms 199 | this.program.uniforms.u_projectionMatrix.value = 200 | camera.projectionMatrix.array 201 | this.program.uniforms.u_cameraPosition.value = camera.worldPosition.array 202 | this.program.uniforms.u_objectPosition.value = this.position.array 203 | this.program.uniforms.u_viewMatrix.value = camera.viewMatrix.array 204 | this.modelViewMatrix = camera.viewMatrix.multiplyNew(this.worldMatrix) 205 | this.normalMatrix = Mat3.fromMat4(this.modelViewMatrix) 206 | this.program.uniforms.u_modelMatrix.value = this.worldMatrix.array 207 | this.program.uniforms.u_modelViewMatrix.value = this.modelViewMatrix.array 208 | this.program.uniforms.u_normalMatrix.value = this.normalMatrix.array 209 | } else { 210 | // Add empty matrix uniforms to program if unset 211 | if (!this.program.uniforms.modelMatrix) { 212 | Object.assign(this.program.uniforms, { 213 | u_objectPosition: new Uniform({ 214 | name: 'objectPosition', 215 | value: undefined, 216 | kind: 'float_vec3' 217 | }) 218 | }) 219 | } 220 | 221 | this.program.uniforms.u_objectPosition.value = this.position.array 222 | } 223 | 224 | // determine if faces need to be flipped - when mesh scaled negatively 225 | const flipFaces = !!( 226 | this.program.cullFace && this.worldMatrix.determinant < 0 227 | ) 228 | 229 | this.program.use({ flipFaces }) 230 | this.geometry.draw({ mode: this.mode, program: this.program }) 231 | this.afterRenderCallbacks.forEach((f) => f && f({ mesh: this, camera })) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { Vec3 } from 'wtc-math' 2 | 3 | import { Renderer } from './core/Renderer' 4 | import { Texture } from './core/Texture' 5 | import { Uniform } from './core/Uniform' 6 | 7 | export interface WTCGLUniformArray { 8 | [index: string]: Uniform 9 | } 10 | 11 | /** 12 | * Represents a collection of all of the properties that make up a complete blending function. 13 | * @interface 14 | */ 15 | export interface WTCGLBlendFunction { 16 | /** 17 | * The source blend function 18 | */ 19 | src: GLenum 20 | /** 21 | * The destination blend function 22 | */ 23 | dst: GLenum 24 | /** 25 | * The source blend function for alpha blending 26 | */ 27 | srcAlpha?: GLenum 28 | /** 29 | * The destination blend function for alpha blending 30 | */ 31 | dstAlpha?: GLenum 32 | } 33 | 34 | /** 35 | * Represents a collection of modes used to make up a blend equation. 36 | * @interface 37 | */ 38 | export interface WTCGLBlendEquation { 39 | /** 40 | * The mode to blend when using RGB 41 | */ 42 | modeRGB: GLenum 43 | /** 44 | * The mode to blend when using RGBA 45 | */ 46 | modeAlpha: GLenum 47 | } 48 | 49 | /** 50 | * Representes an extention of the standard Web GL active info for uniforms and attributes. 51 | * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGLActiveInfo) for more information on WebGLActiveInfo. 52 | * @interface 53 | * @extends WebGLActiveInfo 54 | */ 55 | export interface WTCGLActiveInfo extends WebGLActiveInfo { 56 | /** 57 | * The uniform name, used for associating active info with WTCGLUniform objects 58 | */ 59 | uniformName?: string 60 | /** 61 | * If a uniform location points to a structure array. 62 | */ 63 | isStructArray?: boolean 64 | /** 65 | * If the uniform points to a structure. 66 | */ 67 | isStruct?: boolean 68 | /** 69 | * The index within the structure. 70 | */ 71 | structIndex?: number 72 | /** 73 | * The property within the structure to target. 74 | */ 75 | structProperty?: string 76 | } 77 | 78 | /** 79 | * Represents a value that can be bound to a uniform. 80 | */ 81 | export type WTCGLUniformValue = 82 | | Texture 83 | | Texture[] 84 | | number[] 85 | | string 86 | | number 87 | | boolean 88 | | Float32Array 89 | /** 90 | * Represents a map of uniforms returned from a program. 91 | */ 92 | export type WTCGLUniformMap = Map 93 | /** 94 | * Represents a map of uniform locations to values. Used for caching uniforms in renderer state. 95 | */ 96 | export type WTCGLRendererUniformMap = Map< 97 | WebGLUniformLocation, 98 | WTCGLUniformValue 99 | > 100 | /** 101 | * Represents an map of attributes to attribute locations. 102 | */ 103 | export type WTCGLAttributeMap = Map 104 | 105 | /** 106 | * Represents the cached state of the renderer. All of these properties can be considered "in use". 107 | * @interface 108 | */ 109 | export interface WTCGLRendererState { 110 | /** 111 | * The blend function 112 | */ 113 | blendFunc: WTCGLBlendFunction 114 | /** 115 | * The blend equation 116 | */ 117 | blendEquation: WTCGLBlendEquation 118 | /** 119 | * Which face to cull. gl.CULL_FACE or null 120 | */ 121 | cullFace: GLenum | null 122 | /** 123 | * A GLEnum representing the order to face vertices to use to determine whether what the "front" face of a polygon is. 124 | * Eother gl.CCW or gl.CW 125 | */ 126 | frontFace: GLenum | null 127 | /** 128 | * Whether to write depth information. 129 | */ 130 | depthMask: boolean 131 | /** 132 | * The function to use when depth testing. 133 | */ 134 | depthFunc: GLenum | null 135 | /** 136 | * Whether to use premultiplied alpha. 137 | */ 138 | premultiplyAlpha: boolean 139 | /** 140 | * Whether to flip the Y component of loaded textures in memory 141 | */ 142 | flipY: boolean 143 | /** 144 | * The unpack alignment for pixel stores. 145 | */ 146 | unpackAlignment: number 147 | /** 148 | * The currently bound framebuffer. Null if writing to screen. 149 | */ 150 | framebuffer: unknown // TO DO Update with better type 151 | /** 152 | * An object representing the current viewport 153 | */ 154 | viewport: { 155 | x: number | null 156 | y: number | null 157 | width: number | null 158 | height: number | null 159 | } 160 | /** 161 | * The store of all texture units currently in memory and use. 162 | */ 163 | textureUnits: number[] 164 | /** 165 | * The active texture unit being written to - used when initialising textures 166 | */ 167 | activeTextureUnit: number 168 | /** 169 | * The current attribute buffer being written to. 170 | */ 171 | boundBuffer: unknown // TO DO Update with better type 172 | /** 173 | * The cached uniform location map. 174 | */ 175 | uniformLocations: WTCGLRendererUniformMap 176 | } 177 | 178 | /** 179 | * Texture state, contains the WebGL texture properties for a given texture. 180 | * @interface 181 | */ 182 | export interface WTCGLTextureState { 183 | /** 184 | * The filter to use when rendering smaller. 185 | */ 186 | minFilter: GLenum 187 | /** 188 | * The filter to use when enlarging. 189 | */ 190 | magFilter: GLenum 191 | /** 192 | * Wrapping. 193 | */ 194 | wrapS: GLenum 195 | /** 196 | * Wrapping. 197 | */ 198 | wrapT: GLenum 199 | /** 200 | * The anistropic filtering level for the texture. 201 | */ 202 | anisotropy: number 203 | } 204 | 205 | /** 206 | * An object defining the boundary of the geometry. 207 | */ 208 | export type WTCGLBounds = { 209 | /** 210 | * A vector representing the minimum positions for each axis. 211 | */ 212 | min: Vec3 213 | /** 214 | * A vector representing the maximum positions for each axis. 215 | */ 216 | max: Vec3 217 | /** 218 | * A vector representing the avg center of the object. 219 | */ 220 | center: Vec3 221 | /** 222 | * A vector representing the the scale of the object as defined by `max - min`. 223 | */ 224 | scale: Vec3 225 | /** 226 | * The radius of the boundary of the object. 227 | */ 228 | radius: number 229 | } 230 | 231 | /** 232 | * The collection of supplied attributes that define a geometry. 233 | */ 234 | export type WTCGLGeometryAttributeCollection = { 235 | [key: string]: WTCGLGeometryAttribute 236 | } 237 | /** 238 | * Represents a geometry attribute. 239 | * @interface 240 | */ 241 | export interface WTCGLGeometryAttribute { 242 | /** 243 | * The size of each element in the attribute. For example if you're describing 3D vectors, this would be 3. 244 | */ 245 | size: number 246 | /** 247 | * How big a stride should this attribute have. Should be 0 for all attributes that are uncombined. 248 | */ 249 | stride: number 250 | /** 251 | * How many bytes to offset when passing in the buffer. 252 | */ 253 | offset: number 254 | /** 255 | * the number of elements in the attribute 256 | */ 257 | count: number 258 | /** 259 | * The divisor, used in instanced attributes. 260 | */ 261 | divisor: number 262 | /** 263 | * The number of instances for this attribute. If zero this object is determined to be non-instanced. 264 | */ 265 | instanced: number 266 | 267 | /** 268 | * A typed array of data for the attribute. 269 | */ 270 | data: Float32Array | Float64Array | Uint16Array | Uint32Array 271 | /** 272 | * The WebGL buffer containing the static attribute data. 273 | */ 274 | buffer: WebGLBuffer 275 | 276 | /** 277 | * default gl.UNSIGNED_SHORT for 'index', gl.FLOAT for others. 278 | */ 279 | type: GLenum 280 | /** 281 | * gl.ELEMENT_ARRAY_BUFFER or gl.ARRAY_BUFFER depending on whether this is an index attribute or not. 282 | */ 283 | target: GLenum 284 | 285 | /** 286 | * Whether integer data values should be normalized into a certain range when being cast to a float. 287 | */ 288 | normalized: boolean 289 | 290 | /** 291 | * Whether this attribute needs an update. Set after the attribute changes to have it recast to memory. 292 | */ 293 | needsUpdate: boolean 294 | 295 | /** 296 | * Udpate an attribute for rendering 297 | *@param {WTCGLRenderingContext} gl - The WTCGL rendering context. 298 | */ 299 | updateAttribute(gl: WTCGLRenderingContext): void 300 | } 301 | 302 | /** 303 | * A simple extension of [WebGLRenderingContext](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) that supplies a couple of convenient variables. 304 | * @interface 305 | */ 306 | export type WTCGLRenderingContext = Omit< 307 | WebGL2RenderingContext, 308 | 'createVertexArray' | 'bindVertexArray' 309 | > & { 310 | /** 311 | * The WTCGL Renderer object 312 | */ 313 | renderer: Renderer 314 | /** 315 | * The HTML canvas element. Supplied here because the in-built interface doesn't contain it. 316 | */ 317 | canvas: HTMLCanvasElement 318 | 319 | HALF_FLOAT: number 320 | RGBA16F: number 321 | RGBA32F: number 322 | TRANSFORM_FEEDBACK: GLenum 323 | TRANSFORM_FEEDBACK_BUFFER: GLenum 324 | SEPARATE_ATTRIBS: GLenum 325 | 326 | createVertexArray(): WebGLVertexArrayObject 327 | bindVertexArray(vertexArray?: WebGLVertexArrayObject | null): void 328 | } 329 | 330 | /** 331 | * A list of enabled extenions. 332 | * @interface 333 | */ 334 | export interface WTCGLExtensions { 335 | EXT_color_buffer_float?: string 336 | OES_texture_float_linear?: string 337 | OES_texture_float?: string 338 | OES_texture_half_float?: string 339 | OES_texture_half_float_linear?: string 340 | OES_element_index_uint?: string 341 | OES_standard_derivatives?: string 342 | EXT_sRGB?: string 343 | WEBGL_depth_texture?: string 344 | WEBGL_draw_buffers?: string 345 | } 346 | 347 | /** 348 | * A list of hardware limit values. 349 | */ 350 | export interface WTCGLRendererParams { 351 | maxTextureUnits: number 352 | maxAnisotropy: number 353 | } 354 | -------------------------------------------------------------------------------- /src/lib/core/Camera.ts: -------------------------------------------------------------------------------- 1 | import { Vec3, Mat4 } from 'wtc-math' 2 | 3 | import { Obj } from './Object' 4 | import { Mesh } from './Mesh' 5 | import { Drawable } from './Drawable' 6 | 7 | export interface CameraOptions { 8 | near?: number 9 | far?: number 10 | fov?: number 11 | aspect?: number 12 | zoom?: number 13 | left: number 14 | right: number 15 | top: number 16 | bottom: number 17 | } 18 | 19 | export interface OrtographicOptions { 20 | near: number 21 | far: number 22 | left: number 23 | right: number 24 | bottom: number 25 | top: number 26 | zoom: number 27 | } 28 | 29 | export interface PerspectiveOptions { 30 | near: number 31 | far: number 32 | fov: number 33 | aspect: number 34 | } 35 | 36 | /** 37 | * Class representing some Geometry. 38 | * @extends Obj 39 | **/ 40 | export class Camera extends Obj { 41 | /** 42 | * The near point of the perspective matrix 43 | * @default .1 44 | */ 45 | near: number 46 | /** 47 | * The far point of the perspective matrix 48 | * @default 100 49 | */ 50 | far: number 51 | /** 52 | * The field of view of the perspective matrix, in degrees 53 | * @default 45 54 | */ 55 | fov: number 56 | /** 57 | * The aspect ratio of the perspective matric - normally defined as width / height 58 | * @default 1 59 | */ 60 | aspect: number 61 | /** 62 | * The left plane of the orthagraphic view 63 | */ 64 | left: number 65 | /** 66 | * The right plane of the orthagraphic view 67 | */ 68 | right: number 69 | /** 70 | * The top plane of the orthagraphic view 71 | */ 72 | top: number 73 | /** 74 | * The bottom plane of the orthagraphic view 75 | */ 76 | bottom: number 77 | /** 78 | * The zoom level of the orthagraphic view 79 | * @default 1 80 | */ 81 | zoom: number 82 | 83 | /** 84 | * The camera projection matrix gives a vector space projection to a subspace. 85 | */ 86 | projectionMatrix: Mat4 87 | /** 88 | * The camera view matrix transforms vertices from world-space to view-space. 89 | */ 90 | viewMatrix: Mat4 91 | /** 92 | * The combined projection and view matrix. 93 | */ 94 | projectionViewMatrix: Mat4 95 | /** 96 | * The position in world space 97 | */ 98 | worldPosition: Vec3 99 | 100 | /** 101 | * The camera type - perspective or orthographic. If left / right are provided this will default to orthographic, otherwise it will default to perspective 102 | */ 103 | type: 'orthographic' | 'perspective' 104 | 105 | frustum: { [key: string]: { pos: Vec3; constant: number } } 106 | 107 | /** 108 | * Create a camera object 109 | * @param {Object} __namedParameters - The parameters to be used for the camera 110 | * @param {number} near - The near point of the perspective matrix 111 | * @param {number} far - The far point of the perspective matrix 112 | * @param {number} fov - The fov of the camera, in degrees 113 | * @param {number} aspect - The camera aspect ratio 114 | * @param {number} left - The left point of the orthographic camera 115 | * @param {number} right - The right point of the orthographic camera 116 | * @param {number} top - The top point of the orthographic camera 117 | * @param {number} bottom - The bottom point of the orthographic camera 118 | * @param {number} zoom - The zoom level of the orthographic camera 119 | */ 120 | constructor({ 121 | near = 0.1, 122 | far = 100, 123 | fov = 45, 124 | aspect = 1, 125 | left = 0, 126 | right = 0, 127 | top = 0, 128 | bottom = 0, 129 | zoom = 1 130 | }: Partial = {}) { 131 | super() 132 | 133 | this.near = near 134 | this.far = far 135 | this.fov = fov 136 | this.aspect = aspect 137 | this.left = left 138 | this.right = right 139 | this.top = top 140 | this.bottom = bottom 141 | this.zoom = zoom 142 | 143 | this.projectionMatrix = new Mat4() 144 | this.viewMatrix = new Mat4() 145 | this.projectionViewMatrix = new Mat4() 146 | this.worldPosition = new Vec3() 147 | 148 | // Use orthographic if left/right set, else default to perspective camera 149 | this.type = left || right ? 'orthographic' : 'perspective' 150 | 151 | if (this.type === 'orthographic') this.orthographic() 152 | else this.perspective() 153 | } 154 | 155 | /** 156 | * Calculate the parameters necessary for a perspective camera 157 | * @param {Object} __namedParameters - The parameters to be used for the camera 158 | * @param {number} near - The near point of the perspective matrix 159 | * @param {number} far - The far point of the perspective matrix 160 | * @param {number} fov - The fov of the camera, in degrees 161 | * @param {number} aspect - The camera aspect ratio 162 | */ 163 | perspective({ 164 | near = this.near, 165 | far = this.far, 166 | fov = this.fov, 167 | aspect = this.aspect 168 | }: Partial = {}): Camera { 169 | Object.assign(this, { near, far, fov, aspect }) 170 | 171 | this.projectionMatrix = Mat4.perspective( 172 | (fov * Math.PI) / 180, 173 | aspect, 174 | near, 175 | far 176 | ) 177 | 178 | this.type = 'perspective' 179 | 180 | return this 181 | } 182 | 183 | /** 184 | * Calculate the parameters necessary for an orthographic camera 185 | * @param {Object} __namedParameters - The parameters to be used for the camera 186 | * @param {number} near - The near point of the perspective matrix 187 | * @param {number} far - The far point of the perspective matrix 188 | * @param {number} left - The left point of the orthographic camera 189 | * @param {number} right - The right point of the orthographic camera 190 | * @param {number} top - The top point of the orthographic camera 191 | * @param {number} bottom - The bottom point of the orthographic camera 192 | * @param {number} zoom - The zoom level of the orthographic camera 193 | */ 194 | orthographic({ 195 | near = this.near, 196 | far = this.far, 197 | left = this.left, 198 | right = this.right, 199 | bottom = this.bottom, 200 | top = this.top, 201 | zoom = this.zoom 202 | }: Partial = {}): Camera { 203 | Object.assign(this, { near, far, left, right, bottom, top, zoom }) 204 | 205 | left /= zoom 206 | right /= zoom 207 | bottom /= zoom 208 | top /= zoom 209 | 210 | this.projectionMatrix = Mat4.ortho(left, right, bottom, top, near, far) 211 | 212 | this.type = 'orthographic' 213 | 214 | return this 215 | } 216 | 217 | /** 218 | * Update the world view matrices 219 | * @chainable 220 | * @returns The camera operated on to make function chainable 221 | */ 222 | updateMatrixWorld(): Camera { 223 | super.updateMatrixWorld() 224 | this.viewMatrix = this.worldMatrix.invertNew() 225 | this.worldPosition = this.worldMatrix.translation 226 | 227 | // used for sorting 228 | this.projectionViewMatrix = this.projectionMatrix.multiplyNew( 229 | this.viewMatrix 230 | ) 231 | 232 | return this 233 | } 234 | 235 | /** 236 | * Look at a position 237 | * @param {Vec3} target - The position to look at 238 | * @chainable 239 | * @returns The camera operated on to make function chainable 240 | */ 241 | lookAt(target: Vec3): Camera { 242 | super.lookAt(target, true) 243 | 244 | return this 245 | } 246 | 247 | /** 248 | * Update the frustum parameters based on the prejection view matrix 249 | */ 250 | updateFrustum(): void { 251 | if (!this.frustum) { 252 | this.frustum = { 253 | xNeg: { pos: new Vec3(), constant: 0 }, 254 | xPos: { pos: new Vec3(), constant: 0 }, 255 | yNeg: { pos: new Vec3(), constant: 0 }, 256 | yPos: { pos: new Vec3(), constant: 0 }, 257 | zNeg: { pos: new Vec3(), constant: 0 }, 258 | zPos: { pos: new Vec3(), constant: 0 } 259 | } 260 | } 261 | 262 | /* 263 | a11 a12 a13 a14 264 | 0 1 2 3 265 | a21 a22 a23 a24 266 | 4 5 6 7 267 | a31 a32 a33 a34 268 | 8 9 10 11 269 | a41 a42 a43 a44 270 | 12 13 14 15 271 | */ 272 | 273 | // this.frustum[0].set(m[3] - m[0], m[7] - m[4], m[11] - m[8]).constant = m[15] - m[12]; // -x 274 | const m = this.projectionViewMatrix 275 | this.frustum.xNeg.pos = new Vec3( 276 | m.a14 - m.a11, 277 | m.a24 - m.a21, 278 | m.a34 - m.a31 279 | ) 280 | this.frustum.xNeg.constant = m.a44 - m.a41 281 | 282 | // this.frustum[1].set(m[3] + m[0], m[7] + m[4], m[11] + m[8]).constant = m[15] + m[12]; // +x 283 | this.frustum.xPos.pos = new Vec3( 284 | m.a14 + m.a11, 285 | m.a24 + m.a21, 286 | m.a34 + m.a31 287 | ) 288 | this.frustum.xPos.constant = m.a44 + m.a41 289 | 290 | // this.frustum[2].set(m[3] + m[1], m[7] + m[5], m[11] + m[9]).constant = m[15] + m[13]; // +y 291 | this.frustum.yPos.pos = new Vec3( 292 | m.a14 + m.a12, 293 | m.a24 + m.a22, 294 | m.a34 + m.a32 295 | ) 296 | this.frustum.yPos.constant = m.a44 + m.a42 297 | 298 | // this.frustum[3].set(m[3] - m[1], m[7] - m[5], m[11] - m[9]).constant = m[15] - m[13]; // -y 299 | this.frustum.yNeg.pos = new Vec3( 300 | m.a14 - m.a12, 301 | m.a24 - m.a22, 302 | m.a34 - m.a32 303 | ) 304 | this.frustum.yNeg.constant = m.a44 - m.a42 305 | 306 | // this.frustum[4].set(m[3] - m[2], m[7] - m[6], m[11] - m[10]).constant = m[15] - m[14]; // +z (far) 307 | this.frustum.zPos.pos = new Vec3( 308 | m.a14 - m.a13, 309 | m.a24 - m.a23, 310 | m.a34 - m.a33 311 | ) 312 | this.frustum.zPos.constant = m.a44 - m.a43 313 | 314 | // this.frustum[5].set(m[3] + m[2], m[7] + m[6], m[11] + m[10]).constant = m[15] + m[14]; // -z (near) 315 | this.frustum.zNeg.pos = new Vec3( 316 | m.a14 + m.a13, 317 | m.a24 + m.a23, 318 | m.a34 + m.a33 319 | ) 320 | this.frustum.zNeg.constant = m.a44 + m.a43 321 | 322 | for (const i in this.frustum) { 323 | const invLen = 1.0 / this.frustum[i].pos.length 324 | this.frustum[i].pos.scale(invLen) 325 | this.frustum[i].constant *= invLen 326 | } 327 | } 328 | 329 | /** 330 | * Determines whether the camera frustum intersects the supplied drawable object. Used mainly for frustum culling. 331 | * @param {Drawable} node - The node to test intersection against 332 | * @returns Boolean indicating intersection 333 | */ 334 | frustumIntersects(node: Drawable): boolean { 335 | if (node instanceof Mesh) { 336 | // If no position attribute, treat as frustumCulled false 337 | if (!node.geometry.attributes.position) return true 338 | 339 | if (!node.geometry.bounds || node.geometry.bounds.radius === Infinity) 340 | node.geometry.computeBoundingSphere() 341 | 342 | if (!node.geometry.bounds) return true 343 | 344 | const center = node.geometry.bounds.center.clone() 345 | center.transformByMat4(node.worldMatrix) 346 | 347 | const radius = 348 | node.geometry.bounds.radius * this.maxAxisScale(node.worldMatrix) 349 | 350 | return this.frustumIntersectsSphere(center, radius) 351 | } 352 | 353 | return false 354 | } 355 | 356 | /** 357 | * Determines the largist axis scale of a matrix 358 | * @param {Mat4} m - The matrix to find the max axis of 359 | * @returns The max axis scale 360 | */ 361 | maxAxisScale(m: Mat4): number { 362 | const x = m.a11 * m.a11 + m.a12 * m.a12 + m.a13 * m.a13 363 | const y = m.a21 * m.a21 + m.a22 * m.a22 + m.a23 * m.a23 364 | const z = m.a31 * m.a31 + m.a32 * m.a32 + m.a33 * m.a33 365 | 366 | return Math.sqrt(Math.max(x, y, z)) 367 | } 368 | 369 | /** 370 | * Determines whether the frustum intersects a sphere 371 | * @param {Vec3} center - The center of the sphere to test. 372 | * @param {number} radius - The radius of the sphere to test. 373 | * @returns Boolean indicating intersection 374 | */ 375 | frustumIntersectsSphere(center: Vec3, radius: number): boolean { 376 | for (const i in this.frustum) { 377 | const plane = this.frustum[i] 378 | const dist = plane.pos.clone().dot(center) + plane.constant 379 | if (dist < -radius) return false 380 | } 381 | 382 | return true 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/lib/core/Program.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WTCGLRenderingContext, 3 | WTCGLBlendFunction, 4 | WTCGLBlendEquation, 5 | WTCGLUniformMap, 6 | WTCGLAttributeMap, 7 | WTCGLActiveInfo, 8 | WTCGLUniformArray 9 | } from '../types' 10 | 11 | let ID = 1 12 | 13 | export interface ProgramOptions { 14 | vertex: string 15 | fragment: string 16 | uniforms?: WTCGLUniformArray 17 | transparent?: boolean 18 | cullFace?: GLenum 19 | frontFace?: GLenum 20 | depthTest?: boolean 21 | depthWrite?: boolean 22 | depthFunc?: GLenum 23 | transformFeedbackVaryings?: string[] 24 | } 25 | 26 | /** 27 | * The program class prvides the rendering setup, internal logic, and state for rendering an object. 28 | */ 29 | export class Program { 30 | /** 31 | * The ID of the program. Simple auto-incremening. Used for identifying the program for setup. 32 | */ 33 | id: number 34 | /** 35 | * The WTCGL rendering context object 36 | */ 37 | gl: WTCGLRenderingContext 38 | 39 | /** 40 | * The array uf uniforms for this program 41 | */ 42 | uniforms: WTCGLUniformArray 43 | 44 | /** 45 | * Whether to render transparency. 46 | * @default false 47 | */ 48 | transparent: boolean 49 | /** 50 | * Whether to depth test objects in this program 51 | * @default true 52 | */ 53 | depthTest: boolean 54 | /** 55 | * Whether to write depth information 56 | * @default true 57 | */ 58 | depthWrite: boolean 59 | /** 60 | * Face culling used. 61 | * @default gl.BACK 62 | */ 63 | cullFace: GLenum 64 | /** 65 | * How to determine if a face is front-facing, whether it's points are drawn clockwise or counter-clockwise. Default is counter-clockwise. 66 | * @default gl.CCW 67 | */ 68 | frontFace: GLenum 69 | /** 70 | * The depth function to use when determinging the current pixel against the depth buffer 71 | * @default gl.LESS 72 | */ 73 | depthFunc: GLenum 74 | 75 | /** 76 | * The blend function to use 77 | */ 78 | blendFunc: WTCGLBlendFunction 79 | /** 80 | * The blend equation to use 81 | */ 82 | blendEquation: WTCGLBlendEquation 83 | 84 | /** 85 | * The webgl program store 86 | */ 87 | program: WebGLProgram 88 | 89 | /** 90 | * A map of uniform locations in use within the program shaders 91 | */ 92 | uniformLocations: WTCGLUniformMap 93 | /** 94 | * A map of attribute locations in use within the program shaders 95 | */ 96 | attributeLocations: WTCGLAttributeMap 97 | /** 98 | * A join of the found attributes. Used for addressing vertex array objects on the geometry. 99 | */ 100 | attributeOrder: string 101 | 102 | /** 103 | * The texture unit. Used for addressing texture units in-program. 104 | */ 105 | textureUnit: number = -1 106 | 107 | /** 108 | * Create a Program 109 | * @param gl - The WTCGL Rendering context 110 | * @param __namedParameters - The parameters for the Program 111 | * @param vertex - The vertext shader 112 | * @param fragment - The fragment shader 113 | * @param uniforms - An object of uniforms for use in the program 114 | * @param transparent - Whether to render the program with transparency 115 | * @param cullFace - What method to use for determining face culling 116 | * @param frontFace - What method to use for determining the front of a face 117 | * @param depthTest - Whether to test the depth of a fragment against the depth buffer 118 | * @param depthWrite - Whether to write the depth information 119 | * @param depthFunc - The function to use when depth testing 120 | */ 121 | constructor( 122 | gl: WTCGLRenderingContext, 123 | { 124 | vertex, 125 | fragment, 126 | uniforms = {}, 127 | 128 | transparent = false, 129 | cullFace = gl.BACK, 130 | frontFace = gl.CCW, 131 | depthTest = true, 132 | depthWrite = true, 133 | depthFunc = gl.LESS, 134 | transformFeedbackVaryings 135 | }: ProgramOptions 136 | ) { 137 | if (!gl.canvas) console.error('gl not passed as first argument to Program') 138 | this.gl = gl 139 | this.uniforms = uniforms 140 | this.id = ID++ 141 | 142 | if (!vertex) console.warn('vertex shader not supplied') 143 | if (!fragment) console.warn('fragment shader not supplied') 144 | 145 | // Store program state 146 | this.transparent = transparent 147 | this.cullFace = cullFace 148 | this.frontFace = frontFace 149 | this.depthTest = depthTest 150 | this.depthWrite = depthWrite 151 | this.depthFunc = depthFunc 152 | this.blendFunc = { src: 0, dst: 0 } 153 | 154 | // set default blendFunc if transparent flagged 155 | if (this.transparent && !this.blendFunc?.src) { 156 | if (this.gl?.renderer?.premultipliedAlpha) 157 | this.setBlendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA) 158 | else this.setBlendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA) 159 | } 160 | 161 | // compile vertex shader and log errors 162 | const vertexShader = gl.createShader(gl.VERTEX_SHADER) 163 | 164 | if (vertexShader) { 165 | gl.shaderSource(vertexShader, vertex) 166 | gl.compileShader(vertexShader) 167 | if (gl.getShaderInfoLog(vertexShader) !== '') { 168 | console.warn( 169 | `${gl.getShaderInfoLog( 170 | vertexShader 171 | )}\nVertex Shader\n${addLineNumbers(vertex)}` 172 | ) 173 | } 174 | } 175 | 176 | // compile fragment shader and log errors 177 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 178 | if (fragmentShader) { 179 | gl.shaderSource(fragmentShader, fragment) 180 | gl.compileShader(fragmentShader) 181 | if (gl.getShaderInfoLog(fragmentShader) !== '') { 182 | console.warn( 183 | `${gl.getShaderInfoLog( 184 | fragmentShader 185 | )}\nFragment Shader\n${addLineNumbers(fragment)}` 186 | ) 187 | } 188 | } 189 | 190 | // compile program and log errors 191 | this.program = gl.createProgram()! 192 | if (vertexShader) gl.attachShader(this.program, vertexShader) 193 | if (fragmentShader) gl.attachShader(this.program, fragmentShader) 194 | 195 | // If we have transformFeedbackVaryings, bind them 196 | // TO DO: allow for INTERLEAVED_ATTRIBS as well 197 | if (transformFeedbackVaryings) 198 | gl.transformFeedbackVaryings?.( 199 | this.program, 200 | transformFeedbackVaryings, 201 | gl.SEPARATE_ATTRIBS 202 | ) 203 | 204 | // Finally, link the program and record any errors 205 | gl.linkProgram(this.program) 206 | if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { 207 | console.warn(gl.getProgramInfoLog(this.program)) 208 | return this 209 | } 210 | 211 | // Remove shader once linked 212 | gl.deleteShader(vertexShader) 213 | gl.deleteShader(fragmentShader) 214 | 215 | // Get active uniform locations 216 | this.uniformLocations = new Map() 217 | const numUniforms = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS) 218 | for (let uIndex = 0; uIndex < numUniforms; uIndex++) { 219 | const uniform: WTCGLActiveInfo = gl.getActiveUniform( 220 | this.program, 221 | uIndex 222 | )! 223 | 224 | this.uniformLocations.set( 225 | uniform, 226 | gl.getUniformLocation(this.program, uniform.name)! 227 | ) 228 | 229 | // split uniforms' names to separate array and struct declarations 230 | const split = uniform.name.match(/(\w+)/g) 231 | 232 | if (split?.length) { 233 | uniform.uniformName = split[0] 234 | 235 | // If a uniform location points to a structure, this is how we parse that. 236 | if (split.length === 3) { 237 | uniform.isStructArray = true 238 | uniform.structIndex = Number(split[1]) 239 | uniform.structProperty = split[2] 240 | } else if (split.length === 2 && isNaN(Number(split[1]))) { 241 | uniform.isStruct = true 242 | uniform.structProperty = split[1] 243 | } 244 | } 245 | } 246 | 247 | // Get active attribute locations 248 | this.attributeLocations = new Map() 249 | const locations = [] 250 | const numAttribs = gl.getProgramParameter( 251 | this.program, 252 | gl.ACTIVE_ATTRIBUTES 253 | ) 254 | for (let aIndex = 0; aIndex < numAttribs; aIndex++) { 255 | const attribute = gl.getActiveAttrib(this.program, aIndex)! 256 | const location = gl.getAttribLocation(this.program, attribute.name) 257 | locations[location] = attribute.name 258 | this.attributeLocations.set(attribute, location) 259 | } 260 | this.attributeOrder = locations.join('') 261 | } 262 | 263 | /** 264 | * Set the blend function based on source and destination parameters 265 | * @param src - the source blend function 266 | * @param dst - The destination blend function 267 | * @param srcAlpha - the source blend function for alpha blending 268 | * @param dstAlpha - the destination blend function for alpha blending 269 | */ 270 | setBlendFunc( 271 | src: GLenum, 272 | dst: GLenum, 273 | srcAlpha?: GLenum, 274 | dstAlpha?: GLenum 275 | ): void { 276 | this.blendFunc.src = src 277 | this.blendFunc.dst = dst 278 | this.blendFunc.srcAlpha = srcAlpha 279 | this.blendFunc.dstAlpha = dstAlpha 280 | if (src) this.transparent = true 281 | } 282 | 283 | /** 284 | * set the blend equation 285 | * @param modeRGB - The mode to blend when using RGB 286 | * @param modeAlpha - The mode to blend when using RGBA 287 | */ 288 | setBlendEquation(modeRGB: GLenum, modeAlpha: GLenum): void { 289 | this.blendEquation.modeRGB = modeRGB 290 | this.blendEquation.modeAlpha = modeAlpha 291 | } 292 | 293 | /** 294 | * Apply the program state object to the renderer 295 | */ 296 | applyState(): void { 297 | if (this.depthTest) this.gl.renderer?.enable(this.gl.DEPTH_TEST) 298 | else this.gl.renderer?.disable(this.gl.DEPTH_TEST) 299 | 300 | if (this.cullFace) this.gl.renderer?.enable(this.gl.CULL_FACE) 301 | else this.gl.renderer?.disable(this.gl.CULL_FACE) 302 | 303 | if (this.blendFunc?.src) this.gl.renderer?.enable(this.gl.BLEND) 304 | else this.gl.renderer?.disable(this.gl.BLEND) 305 | 306 | if (this.cullFace) this.gl.renderer.cullFace = this.cullFace 307 | this.gl.renderer.frontFace = this.frontFace 308 | this.gl.renderer.depthMask = this.depthWrite 309 | this.gl.renderer.depthFunc = this.depthFunc 310 | if (this.blendFunc?.src) 311 | this.gl.renderer.setBlendFunc( 312 | this.blendFunc.src, 313 | this.blendFunc.dst, 314 | this.blendFunc.srcAlpha, 315 | this.blendFunc.dstAlpha 316 | ) 317 | this.gl.renderer.setBlendEquation( 318 | this.blendEquation?.modeRGB, 319 | this.blendEquation?.modeAlpha 320 | ) 321 | } 322 | 323 | /** 324 | * Set up the program for use 325 | * @param flipFaces - Flip the faces, for when a mesh is scaled in teh negative 326 | */ 327 | use({ flipFaces = false }: { flipFaces?: boolean } = {}): void { 328 | this.textureUnit = -1 329 | const programActive = this.gl.renderer.currentProgram === this.id 330 | 331 | // Avoid gl call if program already in use 332 | if (!programActive) { 333 | this.gl.useProgram(this.program) 334 | this.gl.renderer.currentProgram = this.id 335 | } 336 | 337 | // Set only the active uniforms found in the shader 338 | this.uniformLocations.forEach((location, activeUniform) => { 339 | const name = activeUniform.uniformName 340 | 341 | if (!name) return 342 | 343 | // get supplied uniform 344 | const uniform = this.uniforms[name] 345 | 346 | if (!uniform) { 347 | return warn(`Active uniform ${name} has not been supplied`) 348 | } 349 | 350 | uniform.bind(this, location, activeUniform) 351 | }) 352 | 353 | this.applyState() 354 | if (flipFaces) 355 | this.gl.renderer.frontFace = 356 | this.frontFace === this.gl.CCW ? this.gl.CW : this.gl.CCW 357 | } 358 | 359 | /** 360 | * Delete the program 361 | */ 362 | remove() { 363 | this.gl.deleteProgram(this.program) 364 | } 365 | } 366 | 367 | function addLineNumbers(string: string) { 368 | const lines = string.split('\n') 369 | for (let i = 0; i < lines.length; i++) { 370 | lines[i] = i + 1 + ': ' + lines[i] 371 | } 372 | return lines.join('\n') 373 | } 374 | 375 | let warnCount = 0 376 | function warn(message: string) { 377 | if (warnCount > 100) return 378 | console.warn(message) 379 | warnCount++ 380 | if (warnCount > 100) 381 | console.warn('More than 100 program warnings - stopping logs.') 382 | } 383 | -------------------------------------------------------------------------------- /src/lib/core/Texture.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WTCGLRendererState, 3 | WTCGLTextureState, 4 | WTCGLRenderingContext 5 | } from '../types' 6 | 7 | const emptyPixel = new Uint8Array(4) 8 | 9 | function isPowerOf2(value: number) { 10 | return (value & (value - 1)) === 0 11 | } 12 | 13 | let ID = 1 14 | 15 | type ImageTypes = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement 16 | 17 | export interface TextureOptions { 18 | image: ImageTypes | ImageTypes[] 19 | data: Float32Array | null 20 | target: GLenum 21 | type: GLenum 22 | format: GLenum 23 | internalFormat: GLenum 24 | wrapS: GLenum 25 | wrapT: GLenum 26 | generateMipmaps: boolean 27 | minFilter: GLenum 28 | magFilter: GLenum 29 | premultiplyAlpha: boolean 30 | unpackAlignment: 1 | 2 | 4 | 8 31 | flipY: boolean 32 | anisotropy: number 33 | level: number 34 | width: number 35 | height: number 36 | } 37 | 38 | /** 39 | * A texture class contains image data for use in a shader. Along with the image data, the texture contains state variable that determine how secondary conditions, like wrapping and interpolation work. 40 | */ 41 | export class Texture { 42 | /** 43 | * A unique ID for the texture object, allows us to determine whether a texture has already been bound or not. 44 | */ 45 | id: number 46 | 47 | /** 48 | * The WTCGL rendering context. 49 | */ 50 | gl: WTCGLRenderingContext 51 | 52 | /** 53 | * The image element representing the texture. Can be an HTML image, video, or canvas 54 | */ 55 | image: ImageTypes | ImageTypes[] 56 | data: Float32Array | null 57 | 58 | /** 59 | * The WebGL texture object containing the data and state for WebGL 60 | */ 61 | texture: WebGLTexture 62 | 63 | /** 64 | * The width of the texture. 65 | */ 66 | width: number 67 | /** 68 | * The height of the texture. 69 | */ 70 | height: number 71 | 72 | /** 73 | * The binding point for the texture. 74 | * @default gl.TEXTURE_2D 75 | */ 76 | target: GLenum 77 | /** 78 | * The texture type. Typically one of gl.UNSIGNED_BYTE, gl.FLOAT, ext.HALF_FLOAT_OES 79 | * @default gl.UNSIGNED_BYTE 80 | */ 81 | type: GLenum 82 | /** 83 | * The texture format. 84 | * @default gl.RGBA 85 | */ 86 | format: GLenum 87 | /** 88 | * the texture internal format. 89 | * @default gl.RGBA 90 | */ 91 | internalFormat: GLenum 92 | /** 93 | * The filter to use when rendering smaller. 94 | */ 95 | minFilter: GLenum 96 | /** 97 | * The filter to use when rendering larger. 98 | */ 99 | magFilter: GLenum 100 | /** 101 | * Wrapping. Normally one of gl.CLAMP_TO_EDGE, gl.REPEAT, gl.MIRRORED REPEAT 102 | * @default gl.CLAMP_TO_EDGE 103 | */ 104 | wrapS: GLenum 105 | /** 106 | * Wrapping. Normally one of gl.CLAMP_TO_EDGE, gl.REPEAT, gl.MIRRORED REPEAT 107 | * @default gl.CLAMP_TO_EDGE 108 | */ 109 | wrapT: GLenum 110 | 111 | /** 112 | * Whether to generate mip maps for this texture. This requires that the texture be power of 2 in size 113 | * @default true 114 | */ 115 | generateMipmaps: boolean 116 | /** 117 | * Whether the texture is interpolated as having premultiplied alpha. 118 | * @default false 119 | */ 120 | premultiplyAlpha: boolean 121 | /** 122 | * Whether to flip the Y direction of the texture 123 | */ 124 | flipY: boolean 125 | 126 | /** 127 | * The unpack alignment for pixel stores. 128 | */ 129 | unpackAlignment: 1 | 2 | 4 | 8 130 | /** 131 | * The anistropic filtering level for the texture. 132 | * @default 0 133 | */ 134 | anisotropy: number 135 | /** 136 | * A GLint specifying the level of detail. Level 0 is the base image level and level n is the nth mipmap reduction level. Only relevant if we have mipmaps. 137 | * @default 0 138 | */ 139 | level: number 140 | 141 | /** 142 | * The image store 143 | */ 144 | store: { 145 | image?: ImageTypes | ImageTypes[] 146 | } 147 | /** 148 | * A local reference to the renderer state 149 | */ 150 | glState: WTCGLRendererState 151 | /** 152 | * Texture state, contains the basic texture properties. 153 | */ 154 | state: WTCGLTextureState 155 | 156 | /** 157 | * Whether the texture needs an update in memory 158 | */ 159 | needsUpdate: boolean 160 | 161 | /** 162 | * Create a render target object. 163 | * @param {WTCGLRenderingContext} gl - The WTCGL Rendering context 164 | * @param __namedParameters - The parameters to initialise the renderer. 165 | * @param target - The binding point for the frame buffers. 166 | * @param type - The texture type. Typically one of gl.UNSIGNED_BYTE, gl.FLOAT, ext.HALF_FLOAT_OES 167 | * @param format - The texture format. 168 | * @param internalFormat - the texture internalFormat. 169 | * @param wrapS - Wrapping 170 | * @param wrapT - Wrapping 171 | * @param generateMipmaps - Whether to generate mips for this texture 172 | * @param minFilter - The filter to use when rendering smaller 173 | * @param magFilter - The filter to use when enlarging 174 | * @param premultiplyAlpha - Whether to use premultiplied alpha for stored textures. 175 | * @param flipY - Whether to flip the Y component of the supplied image 176 | * @param anistropy - The anistropic filtering level for the texture. 177 | * @param unpackAlignment - The unpack alignment for pixel stores. 178 | * @param level - A GLint specifying the level of detail. Level 0 is the base image level and level n is the nth mipmap reduction level. Only relevant if we have mipmaps. 179 | * @param width - The width of the render target. 180 | * @param height - The height of the render target. 181 | */ 182 | constructor( 183 | gl: WTCGLRenderingContext, 184 | { 185 | image, 186 | data, 187 | target = gl.TEXTURE_2D, 188 | type = gl.UNSIGNED_BYTE, 189 | format = gl.RGBA, 190 | internalFormat = format, 191 | wrapS = gl.CLAMP_TO_EDGE, 192 | wrapT = gl.CLAMP_TO_EDGE, 193 | generateMipmaps = true, 194 | minFilter = generateMipmaps ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR, 195 | magFilter = gl.LINEAR, 196 | premultiplyAlpha = false, 197 | unpackAlignment = 4, 198 | flipY = target == gl.TEXTURE_2D ? true : false, 199 | anisotropy = 0, 200 | level = 0, 201 | width, // used for RenderTargets or Data Textures 202 | height = width 203 | }: Partial = {} 204 | ) { 205 | this.gl = gl 206 | this.id = ID++ 207 | 208 | if (image) this.image = image 209 | 210 | if (data) this.data = data 211 | 212 | this.target = target 213 | this.type = type 214 | this.format = format 215 | this.internalFormat = internalFormat 216 | this.minFilter = minFilter 217 | this.magFilter = magFilter 218 | this.wrapS = wrapS 219 | this.wrapT = wrapT 220 | this.generateMipmaps = generateMipmaps 221 | this.premultiplyAlpha = premultiplyAlpha 222 | this.unpackAlignment = unpackAlignment 223 | this.flipY = flipY 224 | this.anisotropy = Math.min( 225 | anisotropy, 226 | this.gl.renderer.parameters.maxAnisotropy 227 | ) 228 | this.level = level 229 | 230 | if (width) this.width = width 231 | 232 | if (height) this.height = height 233 | 234 | this.texture = this.gl.createTexture()! 235 | 236 | this.store = { 237 | image: undefined 238 | } 239 | 240 | // State store to avoid redundant calls for per-texture state 241 | this.state = { 242 | minFilter: this.gl.NEAREST_MIPMAP_LINEAR, 243 | magFilter: this.gl.LINEAR, 244 | wrapS: this.gl.REPEAT, 245 | wrapT: this.gl.REPEAT, 246 | anisotropy: 0 247 | } 248 | 249 | this.needsUpdate = true 250 | } 251 | 252 | /** 253 | * Bind the texture. Skips over if it's determined that the texture is already bound. 254 | */ 255 | bind() { 256 | // Already bound to active texture unit 257 | if ( 258 | this.gl.renderer.state.textureUnits[ 259 | this.gl.renderer.state.activeTextureUnit 260 | ] === this.id 261 | ) 262 | return 263 | this.gl.bindTexture(this.target, this.texture) 264 | this.gl.renderer.state.textureUnits[ 265 | this.gl.renderer.state.activeTextureUnit 266 | ] = this.id 267 | } 268 | 269 | /** 270 | * Update the texture in graphics memory to the internal image and perform various state updates on an as-needs basis. 271 | * @param textureUnit The texture unit to update against. 272 | * @returns 273 | */ 274 | update(textureUnit = 0) { 275 | const needsUpdate = !(this.image === this.store.image && !this.needsUpdate) 276 | 277 | // Make sure that texture is bound to its texture unit 278 | if ( 279 | needsUpdate || 280 | this.gl.renderer.state.textureUnits[textureUnit] !== this.id 281 | ) { 282 | // set active texture unit to perform texture functions 283 | this.gl.renderer.activeTexture = textureUnit 284 | this.bind() 285 | } 286 | 287 | if (!needsUpdate) return 288 | this.needsUpdate = false 289 | 290 | if (this.flipY !== this.gl.renderer.state.flipY) { 291 | this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, this.flipY) 292 | this.gl.renderer.state.flipY = this.flipY 293 | } 294 | 295 | if (this.premultiplyAlpha !== this.gl.renderer.state.premultiplyAlpha) { 296 | this.gl.pixelStorei( 297 | this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 298 | this.premultiplyAlpha 299 | ) 300 | this.gl.renderer.state.premultiplyAlpha = this.premultiplyAlpha 301 | } 302 | 303 | if (this.unpackAlignment !== this.gl.renderer.state.unpackAlignment) { 304 | this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, this.unpackAlignment) 305 | this.gl.renderer.state.unpackAlignment = this.unpackAlignment 306 | } 307 | 308 | if (this.minFilter !== this.state.minFilter) { 309 | this.gl.texParameteri( 310 | this.target, 311 | this.gl.TEXTURE_MIN_FILTER, 312 | this.minFilter 313 | ) 314 | this.state.minFilter = this.minFilter 315 | } 316 | 317 | if (this.magFilter !== this.state.magFilter) { 318 | this.gl.texParameteri( 319 | this.target, 320 | this.gl.TEXTURE_MAG_FILTER, 321 | this.magFilter 322 | ) 323 | this.state.magFilter = this.magFilter 324 | } 325 | 326 | if (this.wrapS !== this.state.wrapS) { 327 | this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_S, this.wrapS) 328 | this.state.wrapS = this.wrapS 329 | } 330 | 331 | if (this.wrapT !== this.state.wrapT) { 332 | this.gl.texParameteri(this.target, this.gl.TEXTURE_WRAP_T, this.wrapT) 333 | this.state.wrapT = this.wrapT 334 | } 335 | 336 | if (this.anisotropy && this.anisotropy !== this.state.anisotropy) { 337 | this.gl.texParameterf( 338 | this.target, 339 | this.gl.renderer.getExtension('EXT_texture_filter_anisotropic') 340 | .TEXTURE_MAX_ANISOTROPY_EXT, 341 | this.anisotropy 342 | ) 343 | this.state.anisotropy = this.anisotropy 344 | } 345 | 346 | if (this.image || this.data) { 347 | if (this.image instanceof HTMLVideoElement) { 348 | this.width = this.image.videoWidth 349 | this.height = this.image.videoHeight 350 | } else if (this.image && 'width' in this.image) { 351 | this.width = this.image.width 352 | this.height = this.image.height 353 | } 354 | 355 | if ( 356 | this.target === this.gl.TEXTURE_CUBE_MAP && 357 | Array.isArray(this.image) 358 | ) { 359 | // For cube maps 360 | for (let i = 0; i < 6; i++) { 361 | this.gl.texImage2D( 362 | this.gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 363 | this.level, 364 | this.internalFormat, 365 | this.format, 366 | this.type, 367 | this.image[i] 368 | ) 369 | } 370 | } else if (ArrayBuffer.isView(this.data)) { 371 | // Data texture 372 | this.gl.texImage2D( 373 | this.target, 374 | this.level, 375 | this.internalFormat, 376 | this.width, 377 | this.height, 378 | 0, 379 | this.format, 380 | this.type, 381 | this.data 382 | ) 383 | } else if (!Array.isArray(this.image)) { 384 | // Regular texture 385 | this.gl.texImage2D( 386 | this.target, 387 | this.level, 388 | this.internalFormat, 389 | this.width, 390 | this.height, 391 | 0, 392 | this.format, 393 | this.type, 394 | this.image 395 | ) 396 | } 397 | 398 | if (this.generateMipmaps) { 399 | // For WebGL1, if not a power of 2, turn off mips, set wrapping to clamp to edge and minFilter to linear 400 | if ( 401 | !this.gl.renderer.isWebgl2 && 402 | this.image && 403 | !Array.isArray(this.image) && 404 | (!isPowerOf2(this.image.width) || !isPowerOf2(this.image.height)) 405 | ) { 406 | this.generateMipmaps = false 407 | this.wrapS = this.wrapT = this.gl.CLAMP_TO_EDGE 408 | this.minFilter = this.gl.LINEAR 409 | } else { 410 | this.gl.generateMipmap(this.target) 411 | } 412 | } 413 | } else { 414 | if (this.target === this.gl.TEXTURE_CUBE_MAP) { 415 | // Upload empty pixel for each side while no image to avoid errors while image or video loading 416 | for (let i = 0; i < 6; i++) { 417 | this.gl.texImage2D( 418 | this.gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 419 | 0, 420 | this.gl.RGBA, 421 | 1, 422 | 1, 423 | 0, 424 | this.gl.RGBA, 425 | this.gl.UNSIGNED_BYTE, 426 | emptyPixel 427 | ) 428 | } 429 | } else if (this.width) { 430 | // image intentionally left null for RenderTarget 431 | this.gl.texImage2D( 432 | this.target, 433 | this.level, 434 | this.internalFormat, 435 | this.width, 436 | this.height, 437 | 0, 438 | this.format, 439 | this.type, 440 | null 441 | ) 442 | } else { 443 | // Upload empty pixel if no image to avoid errors while image or video loading 444 | this.gl.texImage2D( 445 | this.target, 446 | 0, 447 | this.gl.RGBA, 448 | 1, 449 | 1, 450 | 0, 451 | this.gl.RGBA, 452 | this.gl.UNSIGNED_BYTE, 453 | emptyPixel 454 | ) 455 | } 456 | } 457 | this.store.image = this.image 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/lib/geometry/Geometry.ts: -------------------------------------------------------------------------------- 1 | import { Vec3 } from 'wtc-math' 2 | 3 | import type { 4 | WTCGLRendererState, 5 | WTCGLRenderingContext, 6 | WTCGLGeometryAttributeCollection, 7 | WTCGLGeometryAttribute, 8 | WTCGLBounds 9 | } from '../types' 10 | import { Program } from '../core/Program' 11 | import { TransformFeedback } from '../core/TransformFeedback' 12 | 13 | let ID = 1 14 | 15 | // To stop inifinite warnings 16 | let isBoundsWarned = false 17 | 18 | const originArrayToVec3 = function ( 19 | a: number[] | Float32Array | Float64Array | Uint16Array | Uint32Array, 20 | o: number = 0 21 | ): Vec3 { 22 | return new Vec3(a[o], a[o + 1], a[o + 2]) 23 | } 24 | 25 | /** 26 | * Class representing some Geometry. 27 | **/ 28 | export class Geometry { 29 | /** 30 | * The unique ID of the Geometry. 31 | */ 32 | id: number 33 | /** 34 | * The WTCGL rendering context. 35 | */ 36 | gl: WTCGLRenderingContext 37 | 38 | /** 39 | * The WTCGL attribute collection that describes this geometry. 40 | */ 41 | attributes: WTCGLGeometryAttributeCollection 42 | 43 | /** 44 | * An array of vertex array objects that represent the different attributes. 45 | */ 46 | VAOs: { [key: string]: WebGLVertexArrayObject } 47 | 48 | /** 49 | * Any supplied transform feedback objects 50 | */ 51 | transformFeedbacks: TransformFeedback 52 | transformFeedbackIndex: number = 0 53 | 54 | /** 55 | * The range of supplied elements to draw. 56 | */ 57 | drawRange: { start: number; count: number } 58 | /** 59 | * The number of instances to draw. 60 | */ 61 | instancedCount: number 62 | 63 | /** 64 | * A boolean indicating whether the geometry is an instanced geometry or not. 65 | */ 66 | isInstanced: boolean 67 | /** 68 | * An object defining the boundary of the geometry 69 | */ 70 | bounds: WTCGLBounds 71 | 72 | /** 73 | * An object defining all of the rendering state properties. 74 | */ 75 | glState: WTCGLRendererState 76 | 77 | /** 78 | * Create a geometry object. 79 | * @param {WTCGLRenderingContext} gl - The WTCGL Rendering context 80 | * @param {WTCGLGeometryAttributeCollection} attributes - A collection of attributes for the geometry. Typically includes vertex positions in 2 or 3d, normals etc. 81 | */ 82 | constructor( 83 | gl: WTCGLRenderingContext, 84 | attributes: WTCGLGeometryAttributeCollection = {}, 85 | transformFeedbacks?: TransformFeedback 86 | ) { 87 | if (!gl.canvas) console.error('gl not passed as first argument to Geometry') 88 | this.gl = gl 89 | this.attributes = attributes 90 | this.id = ID++ 91 | 92 | this.VAOs = {} 93 | 94 | if (transformFeedbacks) this.transformFeedbacks = transformFeedbacks 95 | 96 | // Unbind existing VAOs 97 | this.gl.renderer.bindVertexArray(null) 98 | 99 | // Initialise the draw range and instance count with default values 100 | this.drawRange = { start: 0, count: 0 } 101 | this.instancedCount = 0 102 | 103 | // Initialise the attribute buffers 104 | for (const key in attributes) { 105 | this.addAttribute(key, attributes[key]) 106 | } 107 | } 108 | 109 | /** 110 | * Add an attribute 111 | * @param {string} key - The key for the attribute 112 | * @param {WTCGLGeometryAttribute} attr - The Geometry attribute, basically the data to be injected 113 | */ 114 | addAttribute(key: string, attr: WTCGLGeometryAttribute): void { 115 | this.attributes[key] = attr 116 | 117 | // Attribute options 118 | attr.target = 119 | key === 'index' ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER 120 | attr.count = 121 | attr.count || 122 | (attr.stride 123 | ? attr.data.byteLength / attr.stride 124 | : attr.data.length / attr.size) 125 | attr.needsUpdate = false 126 | 127 | if (!attr.buffer) { 128 | attr.buffer = this.gl.createBuffer()! 129 | 130 | // Push data to the buffer 131 | attr.updateAttribute(this.gl) 132 | } 133 | 134 | // Update geometry counts. If indexed, ignore regular attributes 135 | if (attr.divisor) { 136 | this.isInstanced = true 137 | if ( 138 | this.instancedCount && 139 | this.instancedCount !== attr.count * attr.divisor 140 | ) { 141 | console.warn( 142 | 'geometry has multiple instanced buffers of different length' 143 | ) 144 | this.instancedCount = Math.min( 145 | this.instancedCount, 146 | attr.count * attr.divisor 147 | ) 148 | return 149 | } 150 | this.instancedCount = attr.count * attr.divisor 151 | } else if (key === 'index') { 152 | this.drawRange.count = attr.count 153 | } else if (!this.attributes.index) { 154 | this.drawRange.count = Math.max(this.drawRange.count, attr.count) 155 | } 156 | } 157 | 158 | /** 159 | * Sets up the draw range, used to determined which properties are drawn 160 | * @param {number} start - The start of the range 161 | * @param {number} count - The number of properties to draw 162 | */ 163 | setDrawRange(start: number, count: number): void { 164 | this.drawRange.start = start 165 | this.drawRange.count = count 166 | } 167 | 168 | /** 169 | * Sets up the number of instances to draw 170 | * @param {number} value - The number of geometry instances to draw 171 | */ 172 | setInstancedCount(value: number): void { 173 | this.instancedCount = value 174 | } 175 | 176 | /** 177 | * Create a new vertex array object bind Attributes to it 178 | * @param {Program} program - The program to use to determine attribute locations 179 | */ 180 | createVAO(program: Program): void { 181 | // The reason for keying it using the program's attributeOrder is in order to make sure that we're using the appropriate vertex array if we're sharing this geometry between multiple programs 182 | this.VAOs[program.attributeOrder] = this.gl.renderer.createVertexArray() 183 | this.gl.renderer.bindVertexArray(this.VAOs[program.attributeOrder]) 184 | this.bindAttributes(program) 185 | } 186 | 187 | /** 188 | * Binds all attributes as derived from the program to attribute objects supplied to the geometry 189 | * @param {Program} program - The program to use to determine attribute locations 190 | */ 191 | bindAttributes(program: Program): void { 192 | // Link all attributes to program using gl.vertexAttribPointer 193 | program.attributeLocations.forEach((location, { name, type }) => { 194 | // If geometry missing a required shader attribute 195 | if (!this.attributes[name]) { 196 | console.warn(`active attribute ${name} not being supplied`) 197 | return 198 | } 199 | 200 | const attr = this.attributes[name] 201 | 202 | this.gl.bindBuffer(attr.target, attr.buffer) 203 | this.gl.renderer.state.boundBuffer = attr.buffer 204 | 205 | // For matrix attributes, buffer needs to be defined per column 206 | let numLoc = 1 207 | if (type === 35674) numLoc = 2 // mat2 208 | if (type === 35675) numLoc = 3 // mat3 209 | if (type === 35676) numLoc = 4 // mat4 210 | 211 | const size = attr.size / numLoc 212 | const stride = numLoc === 1 ? 0 : numLoc * numLoc * 4 213 | const offset = numLoc === 1 ? 0 : numLoc * 4 214 | 215 | for (let i = 0; i < numLoc; i++) { 216 | this.gl.vertexAttribPointer( 217 | location + i, 218 | size, 219 | attr.type, 220 | attr.normalized, 221 | attr.stride + stride, 222 | attr.offset + i * offset 223 | ) 224 | this.gl.enableVertexAttribArray(location + i) 225 | 226 | // For instanced attributes, divisor needs to be set. 227 | // For firefox, need to set back to 0 if non-instanced drawn after instanced. Else won't render 228 | this.gl.renderer.vertexAttribDivisor(location + i, attr.divisor) 229 | } 230 | }) 231 | 232 | // Bind indices if geometry indexed 233 | if (this.attributes.index) 234 | this.gl.bindBuffer( 235 | this.gl.ELEMENT_ARRAY_BUFFER, 236 | this.attributes.index.buffer 237 | ) 238 | } 239 | 240 | bindTransformFeedbacks(): void { 241 | const i = (this.transformFeedbackIndex + 1) % 2 242 | const source = this.transformFeedbacks.VAOs[this.transformFeedbackIndex] 243 | // const dest = this.transformFeedbacks.VAOs[i] 244 | const feedbk = this.transformFeedbacks.TFBs[i] 245 | const buffer = this.transformFeedbacks.BufferRefs[i] 246 | 247 | // this.readTFB = source 248 | // this.writeTFB = dest 249 | 250 | this.gl.bindVertexArray(source) 251 | this.gl.bindTransformFeedback(this.gl.TRANSFORM_FEEDBACK, feedbk) 252 | 253 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 254 | ;(window).feedbk = feedbk 255 | 256 | for (const i in buffer) { 257 | const b = buffer[i] 258 | 259 | this.gl.bindBufferBase(this.gl.TRANSFORM_FEEDBACK_BUFFER, b.i, b.buffer) 260 | } 261 | 262 | this.transformFeedbackIndex = i 263 | } 264 | 265 | /** 266 | * Draw the geometry 267 | * @param {Object} __namedParameters - The parameters to be used for the draw command 268 | * @param {Program} program - The Program object to draw to 269 | * @param {GLenum} mode - The mode to draw the object in 270 | */ 271 | draw({ 272 | program, 273 | mode = this.gl.TRIANGLES 274 | }: { 275 | program: Program 276 | mode: GLenum 277 | }): void { 278 | if ( 279 | this.gl.renderer.currentGeometry !== 280 | `${this.id}_${program.attributeOrder}` 281 | ) { 282 | if (!this.VAOs[program.attributeOrder]) this.createVAO(program) 283 | this.gl.renderer.bindVertexArray(this.VAOs[program.attributeOrder]) 284 | this.gl.renderer.currentGeometry = `${this.id}_${program.attributeOrder}` 285 | } 286 | 287 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 288 | ;(window).transformFeedbacks = this.transformFeedbacks 289 | 290 | if (this.transformFeedbacks) { 291 | this.bindTransformFeedbacks() 292 | this.gl.beginTransformFeedback(mode) 293 | } 294 | 295 | // Check if any attributes need updating 296 | program?.attributeLocations.forEach((_, { name }) => { 297 | const attr = this.attributes[name] 298 | if (attr && attr.needsUpdate) attr.updateAttribute(this.gl) 299 | }) 300 | 301 | if (this.isInstanced) { 302 | if (this.attributes.index) { 303 | this.gl.renderer.drawElementsInstanced( 304 | mode, 305 | this.drawRange.count, 306 | this.attributes.index.type, 307 | this.attributes.index.offset + this.drawRange.start * 2, 308 | this.instancedCount 309 | ) 310 | } else { 311 | this.gl.renderer.drawArraysInstanced( 312 | mode, 313 | this.drawRange.start, 314 | this.drawRange.count, 315 | this.instancedCount 316 | ) 317 | } 318 | } else { 319 | if (this.attributes.index) { 320 | this.gl.drawElements( 321 | mode, 322 | this.drawRange.count, 323 | this.attributes.index.type, 324 | this.attributes.index.offset + this.drawRange.start * 2 325 | ) 326 | } else { 327 | this.gl.drawArrays(mode, this.drawRange.start, this.drawRange.count) 328 | } 329 | } 330 | 331 | if (this.transformFeedbacks) { 332 | this.gl.endTransformFeedback() 333 | } 334 | } 335 | 336 | /** 337 | * Returns the position atribute array 338 | * @returns {WTCGLGeometryAttribute} 339 | */ 340 | getPosition(): WTCGLGeometryAttribute | undefined { 341 | const attr = this.attributes.position 342 | if (attr.data) return attr 343 | if (isBoundsWarned) return 344 | isBoundsWarned = true 345 | console.warn('No position buffer data found to compute bounds') 346 | return 347 | } 348 | 349 | /** 350 | * Computes the bounding box of the geometry. If no attribute is provided to compue with, try to use the position attribute array by default. 351 | * @param {WTCGLGeometryAttribute} attr - The attribute array to compute the bounding box off 352 | */ 353 | computeBoundingBox(attr?: WTCGLGeometryAttribute): void { 354 | if (!attr) attr = this.getPosition()! 355 | const array = attr.data 356 | const offset = attr.offset || 0 357 | const stride = attr.stride || attr.size 358 | 359 | if (!this.bounds) { 360 | this.bounds = { 361 | min: new Vec3(), 362 | max: new Vec3(), 363 | center: new Vec3(), 364 | scale: new Vec3(), 365 | radius: Infinity 366 | } 367 | } 368 | 369 | const min = this.bounds.min 370 | const max = this.bounds.max 371 | const center = this.bounds.center 372 | const scale = this.bounds.scale 373 | 374 | min.reset(Infinity, Infinity, Infinity) 375 | max.reset(-Infinity, -Infinity, -Infinity) 376 | 377 | // TODO: check size of position (eg triangle with Vec2) 378 | for (let i = offset, l = array.length; i < l; i += stride) { 379 | const x = array[i] 380 | const y = array[i + 1] 381 | const z = array[i + 2] 382 | 383 | min.x = Math.min(x, min.x) 384 | min.y = Math.min(y, min.y) 385 | min.z = Math.min(z, min.z) 386 | 387 | max.x = Math.max(x, max.x) 388 | max.y = Math.max(y, max.y) 389 | max.z = Math.max(z, max.z) 390 | } 391 | 392 | scale.resetToVector(max.subtractNew(min)) 393 | center.resetToVector(min.addNew(max).scale(0.5)) 394 | } 395 | 396 | /** 397 | * Computes the bounding sphere of the geometry. If no attribute is provided to compue with, try to use the position attribute array by default. 398 | * @param {WTCGLGeometryAttribute} attr - The attribute array to compute the bounding box off 399 | */ 400 | computeBoundingSphere(attr?: WTCGLGeometryAttribute | null): void { 401 | if (!attr) attr = this.getPosition()! 402 | const array = attr.data 403 | const offset = attr.offset || 0 404 | const stride = attr.stride || attr.size 405 | 406 | if (!this.bounds) this.computeBoundingBox(attr) 407 | 408 | let maxRadiusSq = 0 409 | for (let i = offset, l = array.length; i < l; i += stride) { 410 | const point = originArrayToVec3(array, i) 411 | maxRadiusSq = Math.max( 412 | maxRadiusSq, 413 | this.bounds.center.subtractNew(point).lengthSquared 414 | ) 415 | } 416 | 417 | this.bounds.radius = Math.sqrt(maxRadiusSq) 418 | } 419 | 420 | /** 421 | * Remoce all of the vertex array objects and buffers from memory 422 | */ 423 | remove(): void { 424 | for (const key in this.VAOs) { 425 | this.gl.renderer.deleteVertexArray(this.VAOs[key]) 426 | delete this.VAOs[key] 427 | } 428 | for (const key in this.attributes) { 429 | this.gl.deleteBuffer(this.attributes[key].buffer) 430 | delete this.attributes[key] 431 | } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/lib/core/DollyCamera.ts: -------------------------------------------------------------------------------- 1 | import { Vec2, Vec3 } from 'wtc-math' 2 | 3 | import { Camera, CameraOptions } from './Camera' 4 | 5 | export interface DollyCameraOptions { 6 | element: HTMLElement 7 | enabled: boolean 8 | target: Vec3 9 | ease: number 10 | inertia: number 11 | enableRotate: boolean 12 | rotateSpeed: number 13 | autoRotate: boolean 14 | autoRotateSpeed: number 15 | enableZoom: boolean 16 | zoomSpeed: number 17 | zoomStyle: string 18 | enablePan: boolean 19 | panSpeed: number 20 | minPolarAngle: number 21 | maxPolarAngle: number 22 | minAzimuthAngle: number 23 | maxAzimuthAngle: number 24 | minDistance: number 25 | maxDistance: number 26 | } 27 | 28 | export class DollyCamera extends Camera { 29 | element: HTMLElement 30 | 31 | enabled: boolean 32 | target: Vec3 33 | zoomStyle: string 34 | 35 | minDistance: number 36 | maxDistance: number 37 | 38 | enableRotate: boolean 39 | autoRotate: boolean 40 | enableZoom: boolean 41 | enablePan: boolean 42 | 43 | ease: number 44 | inertia: number 45 | rotateSpeed: number 46 | autoRotateSpeed: number 47 | zoomSpeed: number 48 | panSpeed: number 49 | minPolarAngle: number 50 | maxPolarAngle: number 51 | minAzimuthAngle: number 52 | maxAzimuthAngle: number 53 | 54 | sphericalDelta: Vec3 55 | sphericalTarget: Vec3 56 | spherical: Vec3 57 | panDelta: Vec3 58 | 59 | offset: Vec3 60 | 61 | rotateStart: Vec2 62 | panStart: Vec2 63 | dollyStart: Vec2 64 | 65 | state: number 66 | mouseButtons: { ORBIT: number; ZOOM: number; PAN: number } 67 | 68 | static STATE_NONE: number = 0 69 | static STATE_ROTATE: number = 1 70 | static STATE_DOLLY: number = 2 71 | static STATE_PAN: number = 4 72 | static STATE_DOLLY_PAN: number = 8 73 | 74 | constructor( 75 | { 76 | element = document.body, 77 | enabled = true, 78 | target = new Vec3(0, 0, 0), 79 | ease = 0.25, 80 | inertia = 0.5, 81 | enableRotate = true, 82 | rotateSpeed = 0.5, 83 | autoRotate = false, 84 | autoRotateSpeed = 1.0, 85 | enableZoom = true, 86 | zoomSpeed = 1, 87 | zoomStyle = 'dolly', 88 | enablePan = true, 89 | panSpeed = 0.5, 90 | minPolarAngle = 0, 91 | maxPolarAngle = Math.PI, 92 | minAzimuthAngle = -Infinity, 93 | maxAzimuthAngle = Infinity, 94 | minDistance = 0, 95 | maxDistance = Infinity 96 | }: Partial = {}, 97 | cameraOptions: CameraOptions 98 | ) { 99 | super(cameraOptions) 100 | 101 | this.element = element 102 | 103 | this.enabled = enabled 104 | this.target = target 105 | this.zoomStyle = zoomStyle 106 | 107 | this.ease = ease || 1 108 | this.inertia = inertia || 0 109 | this.enableRotate = enableRotate 110 | this.rotateSpeed = rotateSpeed 111 | this.autoRotate = autoRotate 112 | this.autoRotateSpeed = autoRotateSpeed 113 | this.enableZoom = enableZoom 114 | this.zoomSpeed = zoomSpeed 115 | this.enablePan = enablePan 116 | this.panSpeed = panSpeed 117 | this.minPolarAngle = minPolarAngle 118 | this.maxPolarAngle = maxPolarAngle 119 | this.minAzimuthAngle = minAzimuthAngle 120 | this.maxAzimuthAngle = maxAzimuthAngle 121 | 122 | this.minDistance = minDistance 123 | this.maxDistance = maxDistance 124 | 125 | // current position in sphericalTarget coordinates 126 | this.sphericalDelta = new Vec3(1, 0, 0) // { radius: 1, phi: 0, theta: 0 }; 127 | this.sphericalTarget = new Vec3(1, 0, 0) 128 | this.spherical = new Vec3(1, 0, 0) 129 | this.panDelta = new Vec3() 130 | 131 | this.setPosition(this.position.x, this.position.y, this.position.z) 132 | 133 | this.rotateStart = new Vec2() 134 | this.panStart = new Vec2() 135 | this.dollyStart = new Vec2() 136 | 137 | this.state = DollyCamera.STATE_NONE 138 | this.mouseButtons = { ORBIT: 0, ZOOM: 1, PAN: 2 } 139 | 140 | this.onContextMenu = this.onContextMenu.bind(this) 141 | this.onMouseDown = this.onMouseDown.bind(this) 142 | this.onMouseWheel = this.onMouseWheel.bind(this) 143 | this.onTouchStart = this.onTouchStart.bind(this) 144 | this.onTouchEnd = this.onTouchEnd.bind(this) 145 | this.onTouchMove = this.onTouchMove.bind(this) 146 | this.onMouseMove = this.onMouseMove.bind(this) 147 | this.onMouseUp = this.onMouseUp.bind(this) 148 | 149 | this.addHandlers() 150 | } 151 | 152 | setPosition(x: number, y: number, z: number) { 153 | this.position.reset(x, y, z) 154 | this.offset = this.position.subtractNew(this.target) 155 | 156 | this.spherical.radius = this.sphericalTarget.radius = this.offset.length 157 | this.spherical.theta = this.sphericalTarget.theta = Math.atan2( 158 | this.offset.x, 159 | this.offset.z 160 | ) 161 | this.spherical.phi = this.sphericalTarget.phi = Math.acos( 162 | Math.min(Math.max(this.offset.y / this.sphericalTarget.radius, -1), 1) 163 | ) 164 | } 165 | 166 | update() { 167 | if (this.autoRotate) { 168 | this.handleAutoRotate() 169 | } 170 | 171 | // apply delta 172 | this.sphericalTarget.radius *= this.sphericalDelta.radius 173 | this.sphericalTarget.theta += this.sphericalDelta.theta 174 | this.sphericalTarget.phi += this.sphericalDelta.phi 175 | 176 | // apply boundaries 177 | this.sphericalTarget.theta = Math.max( 178 | this.minAzimuthAngle, 179 | Math.min(this.maxAzimuthAngle, this.sphericalTarget.theta) 180 | ) 181 | this.sphericalTarget.phi = Math.max( 182 | this.minPolarAngle, 183 | Math.min(this.maxPolarAngle, this.sphericalTarget.phi) 184 | ) 185 | this.sphericalTarget.radius = Math.max( 186 | this.minDistance, 187 | Math.min(this.maxDistance, this.sphericalTarget.radius) 188 | ) 189 | 190 | // ease values 191 | this.spherical.phi += 192 | (this.sphericalTarget.phi - this.spherical.phi) * this.ease 193 | this.spherical.theta += 194 | (this.sphericalTarget.theta - this.spherical.theta) * this.ease 195 | this.spherical.radius += 196 | (this.sphericalTarget.radius - this.spherical.radius) * this.ease 197 | 198 | // apply pan to target. As offset is relative to target, it also shifts 199 | this.target.add(this.panDelta) 200 | 201 | // apply rotation to offset 202 | const sinPhiRadius = 203 | this.spherical.radius * Math.sin(Math.max(0.000001, this.spherical.phi)) 204 | this.offset.x = sinPhiRadius * Math.sin(this.spherical.theta) 205 | this.offset.y = this.spherical.radius * Math.cos(this.spherical.phi) 206 | this.offset.z = sinPhiRadius * Math.cos(this.spherical.theta) 207 | 208 | // Apply updated values to object 209 | this.position.resetToVector(this.target.addNew(this.offset)) 210 | this.lookAt(this.target) 211 | 212 | // Apply inertia to values 213 | this.sphericalDelta.theta *= this.inertia 214 | this.sphericalDelta.phi *= this.inertia 215 | this.panDelta.scale(this.inertia) 216 | 217 | // Reset scale every frame to avoid applying scale multiple times 218 | this.sphericalDelta.radius = 1 219 | } 220 | 221 | forcePosition() { 222 | this.offset = this.position.subtractNew(this.target) 223 | this.spherical.radius = this.sphericalTarget.radius = this.offset.length 224 | this.spherical.theta = this.sphericalTarget.theta = Math.atan2( 225 | this.offset.x, 226 | this.offset.z 227 | ) 228 | this.spherical.phi = this.sphericalTarget.phi = Math.acos( 229 | Math.min(Math.max(this.offset.y / this.sphericalTarget.radius, -1), 1) 230 | ) 231 | this.lookAt(this.target) 232 | } 233 | 234 | getZoomScale() { 235 | return Math.pow(0.95, this.zoomSpeed) 236 | } 237 | 238 | panLeft(distance: number, m: number[]) { 239 | const pan = new Vec3(m[0], m[1], m[2]) 240 | pan.scale(-distance) 241 | this.panDelta.add(pan) 242 | } 243 | 244 | panUp(distance: number, m: number[]) { 245 | const pan = new Vec3(m[4], m[5], m[6]) 246 | pan.scale(distance) 247 | this.panDelta.add(pan) 248 | } 249 | 250 | pan(deltaX: number, deltaY: number) { 251 | const el = this.element 252 | const tempPos = this.position.subtractNew(this.target) 253 | let targetDistance = tempPos.length 254 | targetDistance *= Math.tan((((this.fov || 45) / 2) * Math.PI) / 180.0) 255 | 256 | this.panLeft( 257 | (2 * deltaX * targetDistance) / el.clientHeight, 258 | this.matrix.array 259 | ) 260 | 261 | this.panUp( 262 | (2 * deltaY * targetDistance) / el.clientHeight, 263 | this.matrix.array 264 | ) 265 | } 266 | 267 | dolly(dollyScale: number) { 268 | if (this.zoomStyle === 'dolly') this.sphericalDelta.radius /= dollyScale 269 | else { 270 | this.fov /= dollyScale 271 | if (this.type === 'orthographic') this.orthographic() 272 | else this.perspective() 273 | } 274 | } 275 | 276 | handleAutoRotate() { 277 | const angle = ((2 * Math.PI) / 60 / 60) * this.autoRotateSpeed 278 | this.sphericalDelta.theta -= angle 279 | } 280 | 281 | handleMoveRotate(x: number, y: number) { 282 | const movement = new Vec2(x, y) 283 | const moveRot = movement 284 | .subtractNew(this.rotateStart) 285 | .scale(this.rotateSpeed) 286 | const el = this.element 287 | 288 | this.sphericalDelta.theta -= (2 * Math.PI * moveRot.x) / el.clientHeight 289 | this.sphericalDelta.phi -= (2 * Math.PI * moveRot.y) / el.clientHeight 290 | this.rotateStart.resetToVector(movement) 291 | } 292 | 293 | handleMouseMoveDolly(e: MouseEvent) { 294 | const movement = new Vec2(e.clientX, e.clientY) 295 | const dolly = movement.subtractNew(this.dollyStart) 296 | if (dolly.y > 0) { 297 | this.dolly(this.getZoomScale()) 298 | } else if (dolly.y < 0) { 299 | this.dolly(1 / this.getZoomScale()) 300 | } 301 | this.dollyStart.resetToVector(movement) 302 | } 303 | 304 | handleMovePan(x: number, y: number) { 305 | const movement = new Vec2(x, y) 306 | const panm = movement.subtractNew(this.panStart).scale(this.panSpeed) 307 | this.pan(panm.x, panm.y) 308 | this.panStart.resetToVector(movement) 309 | } 310 | 311 | handleTouchStartDollyPan(e: TouchEvent) { 312 | if (this.enableZoom) { 313 | const distance = new Vec2( 314 | e.touches[0].pageX - e.touches[1].pageX, 315 | e.touches[0].pageY - e.touches[1].pageY 316 | ).length 317 | this.dollyStart.reset(0, distance) 318 | } 319 | 320 | if (this.enablePan) { 321 | this.panStart.reset( 322 | 0.5 * (e.touches[0].pageX + e.touches[1].pageX), 323 | 0.5 * (e.touches[0].pageY + e.touches[1].pageY) 324 | ) 325 | } 326 | } 327 | 328 | handleTouchMoveDollyPan(e: TouchEvent) { 329 | if (this.enableZoom) { 330 | const touchzoom = new Vec2( 331 | e.touches[0].pageX - e.touches[1].pageX, 332 | e.touches[0].pageY - e.touches[1].pageY 333 | ) 334 | const zoom = new Vec2(0, touchzoom.length) 335 | const dollyZoom = new Vec2( 336 | 0, 337 | Math.pow(zoom.y / this.dollyStart.y, this.zoomSpeed) 338 | ) 339 | this.dolly(dollyZoom.y) 340 | this.dollyStart.resetToVector(zoom) 341 | } 342 | 343 | if (this.enablePan) { 344 | const x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX) 345 | const y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY) 346 | this.handleMovePan(x, y) 347 | } 348 | } 349 | 350 | onMouseDown(e: MouseEvent) { 351 | if (!this.enabled) return 352 | 353 | switch (e.button) { 354 | case this.mouseButtons.ORBIT: 355 | if (this.enableRotate === false) return 356 | this.rotateStart.reset(e.clientX, e.clientY) 357 | this.state = DollyCamera.STATE_ROTATE 358 | break 359 | case this.mouseButtons.ZOOM: 360 | if (this.enableZoom === false) return 361 | this.dollyStart.reset(e.clientX, e.clientY) 362 | this.state = DollyCamera.STATE_DOLLY 363 | break 364 | case this.mouseButtons.PAN: 365 | if (this.enablePan === false) return 366 | this.panStart.reset(e.clientX, e.clientY) 367 | this.state = DollyCamera.STATE_PAN 368 | break 369 | } 370 | 371 | if (this.state !== DollyCamera.STATE_NONE) { 372 | window.addEventListener('mousemove', this.onMouseMove, false) 373 | window.addEventListener('mouseup', this.onMouseUp, false) 374 | } 375 | } 376 | 377 | onMouseMove(e: MouseEvent) { 378 | if (!this.enabled) return 379 | 380 | switch (this.state) { 381 | case DollyCamera.STATE_ROTATE: 382 | if (this.enableRotate === false) return 383 | this.handleMoveRotate(e.clientX, e.clientY) 384 | break 385 | case DollyCamera.STATE_DOLLY: 386 | if (this.enableZoom === false) return 387 | this.handleMouseMoveDolly(e) 388 | break 389 | case DollyCamera.STATE_PAN: 390 | if (this.enablePan === false) return 391 | this.handleMovePan(e.clientX, e.clientY) 392 | break 393 | } 394 | } 395 | 396 | onMouseUp() { 397 | window.removeEventListener('mousemove', this.onMouseMove, false) 398 | window.removeEventListener('mouseup', this.onMouseUp, false) 399 | this.state = DollyCamera.STATE_NONE 400 | } 401 | 402 | onMouseWheel(e: WheelEvent) { 403 | if ( 404 | !this.enabled || 405 | !this.enableZoom || 406 | (this.state !== DollyCamera.STATE_NONE && 407 | this.state !== DollyCamera.STATE_ROTATE) 408 | ) 409 | return 410 | e.stopPropagation() 411 | e.preventDefault() 412 | 413 | if (e.deltaY < 0) { 414 | this.dolly(1 / this.getZoomScale()) 415 | } else if (e.deltaY > 0) { 416 | this.dolly(this.getZoomScale()) 417 | } 418 | } 419 | 420 | onTouchStart(e: TouchEvent) { 421 | if (!this.enabled) return 422 | e.preventDefault() 423 | 424 | switch (e.touches.length) { 425 | case 1: 426 | if (this.enableRotate === false) return 427 | this.rotateStart.reset(e.touches[0].pageX, e.touches[0].pageY) 428 | this.state = DollyCamera.STATE_ROTATE 429 | break 430 | case 2: 431 | if (this.enableZoom === false && this.enablePan === false) return 432 | this.handleTouchStartDollyPan(e) 433 | this.state = DollyCamera.STATE_DOLLY_PAN 434 | break 435 | default: 436 | this.state = DollyCamera.STATE_NONE 437 | } 438 | } 439 | 440 | onTouchMove(e: TouchEvent) { 441 | if (!this.enabled) return 442 | e.preventDefault() 443 | e.stopPropagation() 444 | 445 | switch (e.touches.length) { 446 | case 1: 447 | if (this.enableRotate === false) return 448 | this.handleMoveRotate(e.touches[0].pageX, e.touches[0].pageY) 449 | break 450 | case 2: 451 | if (this.enableZoom === false && this.enablePan === false) return 452 | this.handleTouchMoveDollyPan(e) 453 | break 454 | default: 455 | this.state = DollyCamera.STATE_NONE 456 | } 457 | } 458 | 459 | onTouchEnd() { 460 | if (!this.enabled) return 461 | this.state = DollyCamera.STATE_NONE 462 | } 463 | 464 | onContextMenu(e: Event) { 465 | if (!this.enablePan) return 466 | if (!this.enabled) return 467 | e.preventDefault() 468 | } 469 | 470 | addHandlers() { 471 | this.element.addEventListener('contextmenu', this.onContextMenu, false) 472 | this.element.addEventListener('mousedown', this.onMouseDown, false) 473 | this.element.addEventListener('wheel', this.onMouseWheel, { 474 | passive: false 475 | }) 476 | this.element.addEventListener('touchstart', this.onTouchStart, { 477 | passive: false 478 | }) 479 | this.element.addEventListener('touchend', this.onTouchEnd, false) 480 | this.element.addEventListener('touchmove', this.onTouchMove, { 481 | passive: false 482 | }) 483 | } 484 | 485 | removeHandlers() { 486 | this.element.removeEventListener('contextmenu', this.onContextMenu) 487 | this.element.removeEventListener('mousedown', this.onMouseDown) 488 | this.element.removeEventListener('wheel', this.onMouseWheel) 489 | this.element.removeEventListener('touchstart', this.onTouchStart) 490 | this.element.removeEventListener('touchend', this.onTouchEnd) 491 | this.element.removeEventListener('touchmove', this.onTouchMove) 492 | window.removeEventListener('mousemove', this.onMouseMove) 493 | window.removeEventListener('mouseup', this.onMouseUp) 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /src/lib/core/Renderer.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from 'wtc-math' 2 | 3 | import type { 4 | WTCGLRendererState, 5 | WTCGLRenderingContext, 6 | WTCGLExtensions, 7 | WTCGLRendererParams 8 | } from '../types' 9 | 10 | import { Camera } from './Camera' 11 | import { RenderTarget } from './RenderTarget' 12 | import { Obj } from './Object' 13 | import { Drawable } from './Drawable' 14 | 15 | export interface RendererOptions { 16 | canvas: HTMLCanvasElement 17 | width: number 18 | height: number 19 | dpr: number 20 | alpha: boolean 21 | depth: boolean 22 | stencil: boolean 23 | antialias: boolean 24 | premultipliedAlpha: boolean 25 | preserveDrawingBuffer: boolean 26 | powerPreference: string 27 | autoClear: boolean 28 | webgl: number 29 | } 30 | 31 | export interface RenderOptions { 32 | scene: Obj 33 | camera?: Camera 34 | target?: RenderTarget | null 35 | update?: boolean 36 | sort?: boolean 37 | frustumCull?: boolean 38 | clear?: boolean 39 | viewport?: [Vec2, Vec2] 40 | } 41 | 42 | /** 43 | * Create a renderer. This is responsible for bringing together the whole state and, eventually, rendering to screen. 44 | */ 45 | export class Renderer { 46 | /** 47 | * The pixel aspect ratio of the renderer. 48 | * @default window.devicePixelRatio or 2, whichever is smaller. 49 | */ 50 | dpr: number 51 | /** 52 | * The dimensions of the renderer 53 | */ 54 | #dimensions: Vec2 55 | 56 | /** 57 | * Whether to render an alpha channel. This property is passed to the rendering context. 58 | * @default false 59 | */ 60 | alpha: boolean 61 | /** 62 | * Whether to clear the color bit 63 | * @default true 64 | */ 65 | colour: boolean 66 | /** 67 | * Whether to render a depth buffer. This property is passed to the rendering context. 68 | * @default true 69 | */ 70 | depth: boolean 71 | /** 72 | * Whether to render a stencil buffer. This property is passed to the rendering context. 73 | * @default false 74 | */ 75 | stencil: boolean 76 | /** 77 | * Whether to use premultiplied alphs. This property is passed to the rendering context. 78 | * @default false 79 | */ 80 | premultipliedAlpha: boolean 81 | /** 82 | * Whether to automatically clear the buffers before render. 83 | * @default true 84 | */ 85 | autoClear: boolean 86 | /** 87 | * Whether the context we've retrieved is webGL2 or not. 88 | */ 89 | isWebgl2: boolean 90 | 91 | /** 92 | * The WTCGL rendering context. 93 | */ 94 | gl: WTCGLRenderingContext 95 | /** 96 | * The rendering state. Allows us to avoid redundant calls on methods used internally 97 | */ 98 | state: WTCGLRendererState 99 | /** 100 | * Stores the enabled extensions 101 | */ 102 | extensions: WTCGLExtensions 103 | /** 104 | * Stored device parameters such as max allowable units etc. 105 | */ 106 | parameters: WTCGLRendererParams 107 | 108 | /** 109 | * Stores the current geometry being rendered. Used to otimise geo rendering. 110 | */ 111 | currentGeometry: string 112 | 113 | /** 114 | * The WebGL2RenderingContext.vertexAttribDivisor() method of the WebGL 2 API modifies the rate at which generic vertex attributes advance when rendering multiple instances of primitives with 115 | */ 116 | vertexAttribDivisor: (index: number, divisor: number) => void 117 | /** 118 | * The WebGL2RenderingContext.drawArraysInstanced() method of the WebGL 2 API renders primitives from array data like the gl.drawArrays() method. In addition, it can execute multiple instances of the range of elements. 119 | */ 120 | drawArraysInstanced: ( 121 | mode: GLenum, 122 | first: number, 123 | count: number, 124 | instanceCound: number 125 | ) => void 126 | /** 127 | * The WebGL2RenderingContext.drawElementsInstanced() method of the WebGL 2 API renders primitives from array data like the gl.drawElements() method. In addition, it can execute multiple instances of a set of elements. 128 | */ 129 | drawElementsInstanced: ( 130 | mode: GLenum, 131 | count: number, 132 | type: GLenum, 133 | offset: GLintptr, 134 | instanceCount: number 135 | ) => void 136 | 137 | /** 138 | * The WebGL2RenderingContext.createVertexArray() method of the WebGL 2 API creates and initializes a WebGLVertexArrayObject object that represents a vertex array object (VAO) pointing to vertex array data and which provides names for different sets of vertex data. 139 | */ 140 | createVertexArray: () => WebGLVertexArrayObject 141 | /** 142 | * The WebGL2RenderingContext.bindVertexArray() method of the WebGL 2 API binds a passed WebGLVertexArrayObject object to the buffer. 143 | */ 144 | bindVertexArray: (vertexArray: WebGLVertexArrayObject | null) => void 145 | /** 146 | * The WebGL2RenderingContext.deleteVertexArray() method of the WebGL 2 API deletes a given WebGLVertexArrayObject object. 147 | */ 148 | deleteVertexArray: (vertexArray: WebGLVertexArrayObject) => void 149 | /** 150 | * The WebGL2RenderingContext.drawBuffers() method of the WebGL 2 API defines draw buffers to which fragment colors are written into. The draw buffer settings are part of the state of the currently bound framebuffer or the drawingbuffer if no framebuffer is bound. 151 | */ 152 | drawBuffers: (buffers: GLenum[]) => void 153 | 154 | // Allows programs to optimise by determine if they're the currently rendering program and skipping some steps 155 | currentProgram: number 156 | 157 | /** 158 | * create a renderer 159 | * @param __namedParameters - The parameters to initialise the renderer. 160 | * @param canvas - The canvas HTML element to render to. 161 | * @param width - The width of the canvas. 162 | * @param height - The height of the canvas. 163 | * @param dpr - The pixel aspect ratio of the canvas. 164 | * @param alpha - Whether to render an alpha channel. 165 | * @param depth - Whether to render a depth buffer. 166 | * @param stencil - Whether to render a stencil buffer. 167 | * @param premultipliedAlpha - Whether to use premultiplied alpha. 168 | * @param preserveDrawingBuffer - Preserve the drawing buffer between calls. Useful for when downloading the canvas to an image. 169 | * @param powerPreference - WebGL power preference. 170 | * @param autoClear - Whether to clear the canvas between draw. 171 | * @param webgl - The webGL version to try to use - 1, or 2 172 | */ 173 | constructor({ 174 | canvas = document.createElement('canvas'), 175 | width = 300, 176 | height = 150, 177 | dpr = Math.min(window.devicePixelRatio, 2), 178 | alpha = false, 179 | depth = true, 180 | stencil = false, 181 | antialias = false, 182 | premultipliedAlpha = false, 183 | preserveDrawingBuffer = false, 184 | powerPreference = 'default', 185 | autoClear = true, 186 | webgl = 2 187 | }: Partial = {}) { 188 | const attributes = { 189 | alpha, 190 | depth, 191 | stencil, 192 | antialias, 193 | premultipliedAlpha, 194 | preserveDrawingBuffer, 195 | powerPreference 196 | } 197 | this.dpr = dpr 198 | this.alpha = alpha 199 | this.colour = true 200 | this.depth = depth 201 | this.stencil = stencil 202 | this.premultipliedAlpha = premultipliedAlpha 203 | this.autoClear = autoClear 204 | 205 | if (webgl === 2) 206 | this.gl = canvas.getContext('webgl2', attributes) as WTCGLRenderingContext 207 | this.isWebgl2 = !!this.gl 208 | if (!this.gl) { 209 | this.gl = canvas.getContext('webgl', attributes) as WTCGLRenderingContext 210 | } 211 | if (!this.gl) { 212 | console.error('unable to create webgl context') 213 | return this 214 | } 215 | 216 | this.gl.renderer = this 217 | 218 | // initialise size values 219 | this.dimensions = new Vec2(width, height) 220 | 221 | this.state = { 222 | blendFunc: { src: this.gl.ONE, dst: this.gl.ZERO }, 223 | blendEquation: { 224 | modeRGB: this.gl.FUNC_ADD, 225 | modeAlpha: this.gl.ONE_MINUS_SRC_ALPHA 226 | }, 227 | cullFace: null, 228 | frontFace: this.gl.CCW, 229 | depthMask: true, 230 | depthFunc: this.gl.LESS, 231 | premultiplyAlpha: false, 232 | flipY: false, 233 | unpackAlignment: 4, 234 | framebuffer: null, 235 | viewport: { width: null, height: null, x: 0, y: 0 }, 236 | textureUnits: [], 237 | activeTextureUnit: 0, 238 | boundBuffer: null, 239 | uniformLocations: new Map() 240 | } 241 | 242 | // store requested extensions 243 | this.extensions = {} 244 | 245 | // Initialise extra format types 246 | if (this.isWebgl2) { 247 | this.getExtension('EXT_color_buffer_float') 248 | this.getExtension('OES_texture_float_linear') 249 | this.getExtension('OES_standard_derivatives') 250 | } else { 251 | this.getExtension('OES_texture_float') 252 | this.getExtension('OES_texture_float_linear') 253 | this.getExtension('OES_texture_half_float') 254 | this.getExtension('OES_texture_half_float_linear') 255 | this.getExtension('OES_element_index_uint') 256 | this.getExtension('OES_standard_derivatives') 257 | this.getExtension('EXT_sRGB') 258 | this.getExtension('WEBGL_depth_texture') 259 | this.getExtension('WEBGL_draw_buffers') 260 | } 261 | 262 | // Create method aliases using extension (WebGL1) or native if available (WebGL2) 263 | this.vertexAttribDivisor = this.getExtension( 264 | 'ANGLE_instanced_arrays', 265 | 'vertexAttribDivisor', 266 | 'vertexAttribDivisorANGLE' 267 | ) 268 | this.drawArraysInstanced = this.getExtension( 269 | 'ANGLE_instanced_arrays', 270 | 'drawArraysInstanced', 271 | 'drawArraysInstancedANGLE' 272 | ) 273 | this.drawElementsInstanced = this.getExtension( 274 | 'ANGLE_instanced_arrays', 275 | 'drawElementsInstanced', 276 | 'drawElementsInstancedANGLE' 277 | ) 278 | this.createVertexArray = this.getExtension( 279 | 'OES_vertex_array_object', 280 | 'createVertexArray', 281 | 'createVertexArrayOES' 282 | ) 283 | this.bindVertexArray = this.getExtension( 284 | 'OES_vertex_array_object', 285 | 'bindVertexArray', 286 | 'bindVertexArrayOES' 287 | ) 288 | this.deleteVertexArray = this.getExtension( 289 | 'OES_vertex_array_object', 290 | 'deleteVertexArray', 291 | 'deleteVertexArrayOES' 292 | ) 293 | this.drawBuffers = this.getExtension( 294 | 'WEBGL_draw_buffers', 295 | 'drawBuffers', 296 | 'drawBuffersWEBGL' 297 | ) 298 | 299 | this.parameters = { 300 | maxTextureUnits: this.gl.getParameter( 301 | this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS 302 | ), 303 | maxAnisotropy: this.getExtension('EXT_texture_filter_anisotropic') 304 | ? this.gl.getParameter( 305 | this.getExtension('EXT_texture_filter_anisotropic') 306 | .MAX_TEXTURE_MAX_ANISOTROPY_EXT 307 | ) 308 | : 0 309 | } 310 | } 311 | 312 | set dimensions(v: Vec2) { 313 | this.#dimensions = v 314 | 315 | this.gl.canvas.width = v.width * this.dpr 316 | this.gl.canvas.height = v.height * this.dpr 317 | } 318 | get dimensions() { 319 | return this.#dimensions 320 | } 321 | 322 | /** 323 | * Set up the viewport for rendering. 324 | * @param dimensions - The dimensions of the viewport 325 | * @param position - The position of the viewport 326 | */ 327 | setViewport(dimensions: Vec2, position: Vec2): void { 328 | if ( 329 | this.state.viewport.width === dimensions.width && 330 | this.state.viewport.height === dimensions.height && 331 | this.state.viewport.x === position.x && 332 | this.state.viewport.y === position.y 333 | ) 334 | return 335 | this.state.viewport.width = dimensions.width 336 | this.state.viewport.height = dimensions.height 337 | this.state.viewport.x = position.x 338 | this.state.viewport.y = position.y 339 | this.gl.viewport( 340 | position.x, 341 | position.y, 342 | dimensions.width, 343 | dimensions.height 344 | ) 345 | } 346 | 347 | /** 348 | * Enables specific WebGL capabilities for this context. 349 | * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/enable) for more details 350 | * @param id - The ID of the capability to enable 351 | */ 352 | enable(id: GLenum): void { 353 | // these typings are ugly I just couldn't find a way to map the rendering context to the state without major refactor of the interface and its uses 354 | if (this.state[id as unknown as keyof WTCGLRendererState] === true) return 355 | 356 | this.gl.enable(id) 357 | this.state[id as unknown as keyof WTCGLRendererState] = true as never 358 | } 359 | 360 | /** 361 | * Disables specific WebGL capabilities for this context. 362 | * @param id - The ID of the capability to enable 363 | */ 364 | disable(id: GLenum): void { 365 | // these typings are ugly I just couldn't find a way to map the rendering context to the state without major refactor of the interface and its uses 366 | if (this.state[id as unknown as keyof WTCGLRendererState] === false) return 367 | 368 | this.gl.disable(id) 369 | this.state[id as unknown as keyof WTCGLRendererState] = false as never 370 | } 371 | 372 | /** 373 | * Set's various blend functions are used for blending pixel calculations 374 | * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendFunc) for more details 375 | * If alpha functions are provided, this will call gl.blendFuncSeparate, otherwise blendFunc 376 | * @param src - A WebGL_API.Types specifying a multiplier for the RGB source blending factors. 377 | * @param dst - A WebGL_API.Types specifying a multiplier for the RGB destination blending factors. 378 | * @param srcAlpha - A WebGL_API.Types specifying a multiplier for the alpha source blending factor. 379 | * @param dstAlpha - A WebGL_API.Types specifying a multiplier for the alpha destination blending factor. 380 | */ 381 | setBlendFunc( 382 | src: GLenum, 383 | dst: GLenum, 384 | srcAlpha?: GLenum, 385 | dstAlpha?: GLenum 386 | ): void { 387 | if ( 388 | this.state.blendFunc.src === src && 389 | this.state.blendFunc.dst === dst && 390 | this.state.blendFunc.srcAlpha === srcAlpha && 391 | this.state.blendFunc.dstAlpha === dstAlpha 392 | ) 393 | return 394 | 395 | this.state.blendFunc.src = src 396 | this.state.blendFunc.dst = dst 397 | this.state.blendFunc.srcAlpha = srcAlpha 398 | this.state.blendFunc.dstAlpha = dstAlpha 399 | 400 | if (srcAlpha !== undefined && dstAlpha !== undefined) 401 | this.gl.blendFuncSeparate(src, dst, srcAlpha, dstAlpha) 402 | else this.gl.blendFunc(src, dst) 403 | } 404 | 405 | /** 406 | * Sets a blending function for use in the application. 407 | * See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendEquation) for more information 408 | * @param modeRGB - The function to be used in RGB models 409 | * @param modeAlpha - The functions to be used for both RGB and alpha models 410 | */ 411 | setBlendEquation(modeRGB: GLenum, modeAlpha: GLenum): void { 412 | modeRGB = modeRGB || this.gl.FUNC_ADD 413 | if ( 414 | this.state.blendEquation.modeRGB === modeRGB && 415 | this.state.blendEquation.modeAlpha === modeAlpha 416 | ) 417 | return 418 | this.state.blendEquation.modeRGB = modeRGB 419 | this.state.blendEquation.modeAlpha = modeAlpha 420 | if (modeAlpha !== undefined) 421 | this.gl.blendEquationSeparate(modeRGB, modeAlpha) 422 | else this.gl.blendEquation(modeRGB) 423 | } 424 | 425 | /** 426 | * Sets the cull face bit 427 | */ 428 | set cullFace(value: GLenum) { 429 | if (this.state.cullFace === value) return 430 | this.state.cullFace = value 431 | this.gl.cullFace(value) 432 | } 433 | get cullFace(): GLenum | null { 434 | return this.state.cullFace 435 | } 436 | 437 | /** 438 | * Sets the front face bit 439 | */ 440 | set frontFace(value: GLenum) { 441 | if (this.state.frontFace === value) return 442 | this.state.frontFace = value 443 | this.gl.frontFace(value) 444 | } 445 | get frontFace(): GLenum | null { 446 | return this.state.frontFace 447 | } 448 | 449 | /** 450 | * Sets the depth mask bit 451 | */ 452 | set depthMask(value: boolean) { 453 | if (this.state.depthMask === value) return 454 | this.state.depthMask = value 455 | this.gl.depthMask(value) 456 | } 457 | get depthMask() { 458 | return this.state.depthMask 459 | } 460 | 461 | /** 462 | * Sets the depth function bit 463 | */ 464 | set depthFunc(value: GLenum) { 465 | if (this.state.depthFunc === value) return 466 | this.state.depthFunc = value 467 | this.gl.depthFunc(value) 468 | } 469 | get depthFunc(): GLenum | null { 470 | return this.state.depthFunc 471 | } 472 | 473 | /** 474 | * Sets the active texture value 475 | */ 476 | set activeTexture(value: number) { 477 | if (this.state.activeTextureUnit === value) return 478 | this.state.activeTextureUnit = value 479 | this.gl.activeTexture(this.gl.TEXTURE0 + value) 480 | } 481 | get activeTexture() { 482 | return this.state.activeTextureUnit 483 | } 484 | 485 | /** 486 | * Binds a given WebGLFramebuffer to a target . 487 | * @param __namedParameters 488 | * @param target - A GLenum specifying the binding point (target). Typically only `gl.FRAMEBUFFER` see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindFramebuffer) for more information 489 | * @param buffer - A WebGLFramebuffer object to bind. If framebuffer is null, then the canvas (which has no WebGLFramebuffer object) is bound. 490 | * @returns 491 | */ 492 | bindFramebuffer({ 493 | target = this.gl.FRAMEBUFFER, 494 | buffer = null 495 | }: { target?: GLenum; buffer?: WebGLFramebuffer | null } = {}) { 496 | if (this.state.framebuffer === buffer) return 497 | this.state.framebuffer = buffer 498 | this.gl.bindFramebuffer(target, buffer) 499 | } 500 | 501 | /** 502 | * Finds and enables a webGL extension and, if it has a corresponding function, returns that. 503 | * @param extension - The extension identifier. 504 | * @param webgl2Func -The name of the webGL2 function to return. 505 | * @param extFunc - The name of the webGL1 functiont to return. 506 | * @returns - A WebGL function, bound to this renderer or null (if no function exists) 507 | */ 508 | getExtension(extension: string, webgl2Func?: string, extFunc?: string) { 509 | // if webgl2 function supported, return func bound to gl context 510 | const castedWebgl2Func = webgl2Func as keyof WTCGLRenderingContext 511 | if (webgl2Func && this.gl[castedWebgl2Func]) { 512 | const func = this.gl[castedWebgl2Func] 513 | 514 | if (typeof func === 'function') return func.bind(this.gl) 515 | else return func 516 | } 517 | 518 | const castedExtension = extension as keyof WTCGLExtensions 519 | 520 | // fetch extension once only 521 | if (!this.extensions[castedExtension]) { 522 | this.extensions[castedExtension] = this.gl.getExtension(extension) 523 | } 524 | 525 | // return extension if no function requested 526 | if (!webgl2Func) return this.extensions[castedExtension] 527 | 528 | // Return null if extension not supported 529 | if (!this.extensions[castedExtension]) return null 530 | 531 | // return extension function, bound to extension 532 | if (extFunc) { 533 | const ext = this.extensions[castedExtension] 534 | // bit of type hacking here too unfortunately as I can't mapt the webgl1 function type names to the actual calls 535 | const gl1Func = ext?.[ 536 | extFunc as unknown as number 537 | // eslint-disable-next-line @typescript-eslint/ban-types 538 | ] as unknown as Function 539 | if (gl1Func) return gl1Func.bind(ext) 540 | } 541 | } 542 | 543 | /** 544 | * An array sort for opaque elements 545 | * @param a - A renderable object for sorting 546 | * @param b - A renderable object for sorting 547 | * @returns The number to determine relative position 548 | */ 549 | sortOpaque(a: Drawable, b: Drawable) { 550 | if (a.renderOrder !== b.renderOrder) { 551 | return a.renderOrder - b.renderOrder 552 | } else if (a.program.id !== b.program.id) { 553 | return a.program.id - b.program.id 554 | } else if (a.zDepth !== b.zDepth) { 555 | return a.zDepth - b.zDepth 556 | } else { 557 | return b.id - a.id 558 | } 559 | } 560 | 561 | /** 562 | * An array sort for transparent elements 563 | * @param a - A renderable object for sorting 564 | * @param b - A renderable object for sorting 565 | * @returns The number to determine relative position 566 | */ 567 | sortTransparent(a: Drawable, b: Drawable) { 568 | if (a.renderOrder !== b.renderOrder) { 569 | return a.renderOrder - b.renderOrder 570 | } 571 | if (a.zDepth !== b.zDepth) { 572 | return b.zDepth - a.zDepth 573 | } else { 574 | return b.id - a.id 575 | } 576 | } 577 | 578 | /** 579 | * An array sort for UI (no depth) elements 580 | * @param a - A renderable object for sorting 581 | * @param b - A renderable object for sorting 582 | * @returns The number to determine relative position 583 | */ 584 | sortUI(a: Drawable, b: Drawable) { 585 | if (a.renderOrder !== b.renderOrder) { 586 | return a.renderOrder - b.renderOrder 587 | } else if (a.program.id !== b.program.id) { 588 | return a.program.id - b.program.id 589 | } else { 590 | return b.id - a.id 591 | } 592 | } 593 | 594 | /** 595 | * Retrieves the list of renderable objects sorted by position, explicit render order and applied depth 596 | * @param __namedParameters 597 | * @param scene - The root scene to render. 598 | * @param camera - The camera to use to determine the render list, applies frustum culling etc. 599 | * @param frustumCull - Whether to apply frustum culling 600 | * @param sort - Whether to sort the list at all. 601 | * @returns - An array of renderable objects 602 | */ 603 | getRenderList({ 604 | scene, 605 | camera, 606 | frustumCull, 607 | sort 608 | }: { 609 | scene: Obj 610 | camera?: Camera 611 | frustumCull: boolean 612 | sort: boolean 613 | }): Drawable[] { 614 | let renderList: Drawable[] = [] 615 | 616 | if (camera && frustumCull) camera.updateFrustum() 617 | 618 | // Get visible 619 | scene.traverse((node: Obj): boolean | null => { 620 | if (!node.visible) return true 621 | if (!(node instanceof Drawable)) return null 622 | 623 | if (frustumCull && node.frustumCulled && camera) { 624 | if (!camera.frustumIntersects(node)) return null 625 | } 626 | 627 | renderList.push(node) 628 | 629 | return null 630 | }) 631 | 632 | if (sort) { 633 | const opaque: Drawable[] = [] 634 | const transparent: Drawable[] = [] // depthTest true 635 | const ui: Drawable[] = [] // depthTest false 636 | 637 | renderList.forEach((node) => { 638 | // Split into the 3 render groups 639 | const { program } = node 640 | 641 | if (program) { 642 | if (!program.transparent) { 643 | opaque.push(node) 644 | } else if (program.depthTest) { 645 | transparent.push(node) 646 | } 647 | } else { 648 | ui.push(node) 649 | } 650 | 651 | // Only calculate z-depth if renderOrder unset and depthTest is true 652 | if (node.renderOrder !== 0 || !program?.depthTest || !camera) return 653 | 654 | // update z-depth 655 | const translation = node.worldMatrix.translation 656 | translation.transformByMat4(camera.projectionViewMatrix) 657 | 658 | node.zDepth = translation.z 659 | }) 660 | 661 | opaque.sort(this.sortOpaque) 662 | transparent.sort(this.sortTransparent) 663 | ui.sort(this.sortUI) 664 | 665 | renderList = opaque.concat(transparent, ui) 666 | } 667 | 668 | return renderList 669 | } 670 | 671 | /** 672 | * Renders a scene 673 | * @param __namedParameters 674 | * @param scene - The renderable object to render. 675 | * @param camera - The camera to render with. If not supplied will just render as is. 676 | * @param target - The render target to render to. 677 | * @param update - Whether to update all of the object worl matrices prior to rendering. 678 | * @param sort - Whether to sort the objects prior to rendering. 679 | * @param frustumCull - Whether to apply frustum culling prior to rendering. 680 | * @param clear - Whether to clear the scene prior to rendering. Only matters if renderer.autoClear is false. 681 | */ 682 | render({ 683 | scene, 684 | camera, 685 | target = null, 686 | update = true, 687 | sort = true, 688 | frustumCull = true, 689 | clear, 690 | viewport 691 | }: RenderOptions) { 692 | if (target === null) { 693 | // make sure no render target bound so draws to canvas 694 | this.bindFramebuffer() 695 | if (!viewport) viewport = [this.dimensions.scaleNew(this.dpr), new Vec2()] 696 | this.setViewport(...viewport) 697 | } else { 698 | // bind supplied render target and update viewport 699 | 700 | this.bindFramebuffer(target) 701 | if (!viewport) 702 | viewport = [new Vec2(target.width, target.height), new Vec2()] 703 | this.setViewport(...viewport) 704 | } 705 | 706 | if (clear || (this.autoClear && clear !== false)) { 707 | // Ensure depth buffer writing is enabled so it can be cleared 708 | if (this.depth && (!target || target.depth)) { 709 | this.enable(this.gl.DEPTH_TEST) 710 | this.depthMask = true 711 | } 712 | this.gl.clear( 713 | (this.colour ? this.gl.COLOR_BUFFER_BIT : 0) | 714 | (this.depth ? this.gl.DEPTH_BUFFER_BIT : 0) | 715 | (this.stencil ? this.gl.STENCIL_BUFFER_BIT : 0) 716 | ) 717 | } 718 | 719 | // updates all scene graph matrices 720 | if (update) scene.updateMatrixWorld() 721 | 722 | // Update camera separately, in case not in scene graph 723 | if (camera) camera.updateMatrixWorld() 724 | 725 | // Get render list - entails culling and sorting 726 | const renderList = this.getRenderList({ scene, camera, frustumCull, sort }) 727 | 728 | renderList.forEach((node) => { 729 | node.draw({ camera }) 730 | }) 731 | } 732 | } 733 | --------------------------------------------------------------------------------