├── .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 |
--------------------------------------------------------------------------------