├── scripts ├── launch.js ├── build.js ├── tsc.js └── glslLoader.js ├── examples ├── basic │ ├── material │ │ └── index.ts │ ├── dark.css │ ├── shadow │ │ └── index.html │ ├── renderToTexture │ │ ├── color.ts │ │ └── depth.ts │ ├── bones │ │ └── index.ts │ ├── Loader │ │ └── obj_mtl.ts │ ├── pbs │ │ └── index.ts │ └── lightesAndGeometries │ │ └── index.ts ├── resources │ ├── images │ │ ├── wood.jpg │ │ ├── chrome.png │ │ ├── uniquestudio.png │ │ └── skybox │ │ │ ├── arid2_bk.jpg │ │ │ ├── arid2_dn.jpg │ │ │ ├── arid2_ft.jpg │ │ │ ├── arid2_lf.jpg │ │ │ ├── arid2_rt.jpg │ │ │ └── arid2_up.jpg │ └── models │ │ ├── teapot │ │ └── teapot.mtl │ │ └── cube │ │ └── cube.obj ├── index.ts ├── tsconfig.json ├── index.html ├── global.ts └── deferredRendering │ └── index.ts ├── src ├── shader │ ├── sources │ │ ├── calculators │ │ │ ├── blur │ │ │ │ ├── gaussian_log.glsl │ │ │ │ └── gaussian.glsl │ │ │ ├── unpackFloat1x32.glsl │ │ │ ├── linearlize_depth.glsl │ │ │ ├── packFloat1x32.glsl │ │ │ ├── phong.glsl │ │ │ ├── types.glsl │ │ │ └── shadow_factor.glsl │ │ ├── definitions │ │ │ ├── material_pbs.glsl │ │ │ ├── material_blinnphong.glsl │ │ │ └── light.glsl │ │ ├── interploters │ │ │ ├── forward │ │ │ │ ├── skybox.frag │ │ │ │ ├── esm │ │ │ │ │ ├── depth.frag │ │ │ │ │ ├── depth.vert │ │ │ │ │ ├── prefiltering.vert │ │ │ │ │ └── prefiltering.frag │ │ │ │ ├── skybox.vert │ │ │ │ ├── gouraud.frag │ │ │ │ ├── gouraud.vert │ │ │ │ ├── phong.vert │ │ │ │ └── phong.frag │ │ │ └── deferred │ │ │ │ ├── tiledLight.vert │ │ │ │ ├── geometry.vert │ │ │ │ ├── geometry.frag │ │ │ │ └── tiledLightPoint.frag │ │ ├── debug │ │ │ └── checkBox.glsl │ │ └── light_model │ │ │ ├── blinn_phong.glsl │ │ │ └── pbs_ggx.glsl │ ├── Attibute.ts │ └── ShaderBuilder.ts ├── Dirtyable.ts ├── lights │ ├── ShadowLevel.ts │ ├── DampingLight.ts │ ├── DirectionalLight.ts │ ├── Light.ts │ └── SpotLight.ts ├── Intersections │ └── BoundingBox.ts ├── DataTypeEnum.ts ├── IAsyncResource.ts ├── loader │ ├── obj_mtl │ │ ├── CommonPatterns.ts │ │ ├── OBJLoader.ts │ │ └── MTLLoader.ts │ └── ResourceFetcher.ts ├── renderer │ ├── IProcessor.ts │ ├── IExtension.ts │ ├── SwapFramebuffer.ts │ ├── forward │ │ └── ForwardProcessor.ts │ ├── ShadowPreProcessor.ts │ ├── FrameBuffer.ts │ └── GraphicsUtils.ts ├── extensions │ └── Water.ts ├── textures │ ├── Texture2D.ts │ ├── DataTexture.ts │ ├── CubeTexture.ts │ └── Texture.ts ├── materials │ ├── Material.ts │ ├── ESM │ │ ├── DepthPackMaterial.ts │ │ └── LogBlurMaterial.ts │ ├── SkyMaterial.ts │ └── surface │ │ ├── BlinnPhongMaterial.ts │ │ ├── StandardMaterial.ts │ │ └── ISurfaceMaterial.ts ├── geometries │ ├── RectGeometry.ts │ ├── TileGeometry.ts │ ├── CubeGeometry.ts │ ├── SphereGeometry.ts │ └── Geometry.ts ├── cameras │ ├── CubeCamera.ts │ ├── PerspectiveCamera.ts │ ├── OrthoCamera.ts │ └── Camera.ts ├── Mesh.ts ├── CanvasToy.ts ├── Util.ts ├── Scene.ts └── Decorators.ts ├── .npmignore ├── canvas-toy-deploy.enc ├── tests ├── testingci.sh ├── unit │ ├── Camera │ │ ├── PerspectiveCamera-Test.ts │ │ └── OrhtoCamera-Test.ts │ └── Loader │ │ └── OBJLoader-Test.ts ├── tsconfig.json ├── test-main.js ├── karma.conf.js └── Util.ts ├── install-dependency.sh ├── require.config.js ├── .travis.yml ├── tsconfig.release.json ├── tsconfig.json ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── tslint.json ├── package.json ├── deploy.sh └── README.md /scripts/launch.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/basic/material/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/blur/gaussian_log.glsl: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | examples/ 3 | bower_components/ 4 | *.sh 5 | -------------------------------------------------------------------------------- /examples/basic/dark.css: -------------------------------------------------------------------------------- 1 | .black { 2 | background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /src/Dirtyable.ts: -------------------------------------------------------------------------------- 1 | export interface IDirtyable { 2 | resetLightShadows(...args); 3 | } 4 | -------------------------------------------------------------------------------- /canvas-toy-deploy.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/canvas-toy-deploy.enc -------------------------------------------------------------------------------- /src/lights/ShadowLevel.ts: -------------------------------------------------------------------------------- 1 | export enum ShadowLevel { 2 | None, 3 | Hard, 4 | Soft, 5 | PCSS, 6 | } 7 | -------------------------------------------------------------------------------- /examples/resources/images/wood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/wood.jpg -------------------------------------------------------------------------------- /tests/testingci.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | tsc -v 4 | rm -rf build/ 5 | tsc 6 | karma start karma.conf.js --browsers Firefox 7 | -------------------------------------------------------------------------------- /examples/resources/images/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/chrome.png -------------------------------------------------------------------------------- /install-dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm install -g typescript typescript-formatter uglifyjs 3 | npm install 4 | npm link typescript 5 | -------------------------------------------------------------------------------- /examples/resources/images/uniquestudio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/uniquestudio.png -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_bk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_bk.jpg -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_dn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_dn.jpg -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_ft.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_ft.jpg -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_lf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_lf.jpg -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_rt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_rt.jpg -------------------------------------------------------------------------------- /examples/resources/images/skybox/arid2_up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danielmelody/CanvasToy/HEAD/examples/resources/images/skybox/arid2_up.jpg -------------------------------------------------------------------------------- /src/shader/sources/definitions/material_pbs.glsl: -------------------------------------------------------------------------------- 1 | struct Material { 2 | vec3 ambient; 3 | vec3 albedo; 4 | float metallic; 5 | float roughness; 6 | }; -------------------------------------------------------------------------------- /src/Intersections/BoundingBox.ts: -------------------------------------------------------------------------------- 1 | export interface BoundingBox2D { 2 | top: number; 3 | bottom: number; 4 | left: number; 5 | right: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/unpackFloat1x32.glsl: -------------------------------------------------------------------------------- 1 | float unpackFloat1x32( vec4 rgba ) { 2 | return dot( rgba, vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 160581375.0) ); 3 | } 4 | -------------------------------------------------------------------------------- /src/DataTypeEnum.ts: -------------------------------------------------------------------------------- 1 | export enum DataType { 2 | float, 3 | int, 4 | vec2, 5 | vec3, 6 | vec4, 7 | mat2, 8 | mat3, 9 | mat4, 10 | } 11 | -------------------------------------------------------------------------------- /src/IAsyncResource.ts: -------------------------------------------------------------------------------- 1 | export interface IAsyncResource { 2 | asyncFinished: () => Promise; 3 | setAsyncFinished(promise: Promise): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/loader/obj_mtl/CommonPatterns.ts: -------------------------------------------------------------------------------- 1 | export namespace patterns { 2 | export const num = /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/mg; 3 | export const commentPattern = /#.*/mg; 4 | } 5 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/skybox.frag: -------------------------------------------------------------------------------- 1 | varying vec3 cubeUV; 2 | uniform samplerCube uCubeTexture; 3 | void main() 4 | { 5 | gl_FragColor = textureCube(uCubeTexture, cubeUV); 6 | } 7 | -------------------------------------------------------------------------------- /src/shader/sources/definitions/material_blinnphong.glsl: -------------------------------------------------------------------------------- 1 | struct Material { 2 | vec3 ambient; 3 | vec3 diffuse; 4 | vec3 specular; 5 | float specularExponent; 6 | float reflectivity; 7 | }; -------------------------------------------------------------------------------- /src/shader/sources/interploters/deferred/tiledLight.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | varying vec3 vPosition; 3 | 4 | void main() 5 | { 6 | gl_Position = vec4(position, 1.0); 7 | vPosition = position; 8 | } 9 | -------------------------------------------------------------------------------- /require.config.js: -------------------------------------------------------------------------------- 1 | { 2 | baseUrl: '.', name: 'browser/almond', include: [ 'build/canvas-toy-amd' ], 3 | out: 'build/canvas-toy-umd.js', 4 | wrap: {startFile : 'browser/start.js', endFile : 'browser/end.js'} 5 | } 6 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/linearlize_depth.glsl: -------------------------------------------------------------------------------- 1 | float linearlizeDepth(float far, float near, float depth) { 2 | float NDRDepth = depth * 2.0 - 1.0;; 3 | return 2.0 * near / (near + far - NDRDepth * (far - near)); 4 | } 5 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/esm/depth.frag: -------------------------------------------------------------------------------- 1 | uniform float softness; 2 | varying vec3 viewPos; 3 | 4 | void main () { 5 | float d = length(viewPos); 6 | gl_FragColor.r = d * softness; 7 | gl_FragColor.g = exp(d) * d; 8 | } 9 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/packFloat1x32.glsl: -------------------------------------------------------------------------------- 1 | vec4 packFloat1x32(float val) 2 | { 3 | vec4 pack = vec4(1.0, 255.0, 65025.0, 16581375.0) * val; 4 | pack = fract(pack); 5 | pack -= vec4(pack.yzw / 255.0, 0.0); 6 | return pack; 7 | } 8 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import "./basic/bones/index"; 2 | import "./basic/lightesAndGeometries/index"; 3 | import "./basic/Loader/obj_mtl"; 4 | import "./basic/pbs/index"; 5 | import "./deferredRendering/index"; 6 | 7 | import { examples } from "./global"; 8 | -------------------------------------------------------------------------------- /src/renderer/IProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "../cameras/Camera"; 2 | import { IMaterial } from "../materials/Material"; 3 | import { Scene } from "../Scene"; 4 | 5 | export interface IProcessor { 6 | process(scene: Scene, camera: Camera, matriels: IMaterial[]); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/IExtension.ts: -------------------------------------------------------------------------------- 1 | export interface WebGLExtension { 2 | depth_texture: WebGLDepthTexture; 3 | draw_buffer: WebGLDrawBuffers; 4 | texture_float: OESTextureFloat; 5 | texture_float_linear: OESTextureFloatLinear; 6 | texture_half_float: OESTextureHalfFloat; 7 | } 8 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/skybox.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 viewProjectionMatrix; 3 | varying vec3 cubeUV; 4 | 5 | void main (){ 6 | vec4 mvp = viewProjectionMatrix * vec4(position, 1.0); 7 | cubeUV = position; 8 | gl_Position = mvp.xyww; 9 | } 10 | -------------------------------------------------------------------------------- /src/shader/sources/debug/checkBox.glsl: -------------------------------------------------------------------------------- 1 | float checkerBoard(in vec2 uv, in float subSize) { 2 | vec2 bigBox = mod(uv, vec2(subSize * 2.0)); 3 | return ( 4 | step(subSize, bigBox.x) * step(subSize, bigBox.y) 5 | + step(subSize, subSize * 2.0 -bigBox.x) * step(subSize, subSize * 2.0 -bigBox.y) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/resources/models/teapot/teapot.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'Utah Teapot.blend' 2 | # Material Count: 1 3 | 4 | newmtl Glossy 5 | Ns 98 6 | Ka 0.000000 0.000000 0.000000 7 | Kd 1.00000 1.000000 1.000000 8 | Ks 0.500000 0.500000 0.500000 9 | Ni 1.000000 10 | d 1.000000 11 | Pr 0.000000 12 | Pm 0.900000 13 | 14 | illum 2 15 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/esm/depth.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 modelViewProjectionMatrix; 3 | uniform mat4 modelViewMatrix; 4 | varying vec3 viewPos; 5 | 6 | void main () { 7 | gl_Position = modelViewProjectionMatrix * vec4(position, 1.0); 8 | viewPos = (modelViewMatrix * vec4(position, 1.0)).xyz; 9 | } 10 | -------------------------------------------------------------------------------- /src/extensions/Water.ts: -------------------------------------------------------------------------------- 1 | import { Geometry } from "../geometries/Geometry"; 2 | import { StandardMaterial } from "../materials/surface/StandardMaterial"; 3 | import { Mesh } from "../Mesh"; 4 | 5 | export class Water extends Mesh { 6 | constructor(gl: WebGLRenderingContext) { 7 | super(new Geometry(gl), [new StandardMaterial(gl)]); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/esm/prefiltering.vert: -------------------------------------------------------------------------------- 1 | uniform mat4 normalMatrix; 2 | attribute vec3 position; 3 | attribute vec3 normal; 4 | varying vec2 uv; 5 | varying vec3 vNormal; 6 | 7 | void main () { 8 | gl_Position = vec4(position, 1.0); 9 | uv = gl_Position.xy * 0.5 + 0.5; 10 | vNormal = normalize((normalMatrix * vec4(normal, 1.0)).xyz); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const loader = require('./glslLoader') 2 | const tsc = require('./tsc') 3 | const fs = require('fs'); 4 | 5 | console.log("building: convert shaders..."); 6 | loader.convertGLSL(); 7 | console.log("building: compile engine..."); 8 | tsc.compile("tsconfig.json") 9 | console.log("building: compile examples..."); 10 | tsc.compile("examples/tsconfig.json"); 11 | console.log("building: done."); -------------------------------------------------------------------------------- /src/loader/ResourceFetcher.ts: -------------------------------------------------------------------------------- 1 | export function fetchRes(url: string) { 2 | return new Promise((resolve, reject) => { 3 | const request = new XMLHttpRequest(); 4 | request.onreadystatechange = () => { 5 | if (request.readyState === 4 && request.status === 200) { 6 | resolve(request.responseText); 7 | } 8 | }; 9 | request.open("GET", url); 10 | request.send(); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: linux 3 | sudo: required 4 | dist: trusty 5 | addons: 6 | apt: 7 | packages: 8 | - mesa-utils 9 | - xvfb 10 | - libgl1-mesa-dri 11 | - libglapi-mesa 12 | - libosmesa6 13 | node_js: 14 | - '6' 15 | 16 | before_script: 17 | - "export DISPLAY=:99.0" 18 | - "sh -e /etc/init.d/xvfb start" 19 | - sleep 3 20 | 21 | install: 22 | - bash ./install-dependency.sh 23 | 24 | script: 25 | - npm run testci && bash ./deploy.sh 26 | -------------------------------------------------------------------------------- /tests/unit/Camera/PerspectiveCamera-Test.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "../../../src/CanvasToy"; 2 | 3 | describe("PerspectiveCamera testing", () => { 4 | it("function adapTargetRadio", () => { 5 | const camera = new CanvasToy.PerspectiveCamera(); 6 | const x = Math.random() % 100; 7 | const y = Math.random() % 100; 8 | camera.setAspectRadio(x / y); 9 | (expect(camera.aspect) as jasmine.ToyMatchers).toBeEqualish(x / y); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/textures/Texture2D.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from "./Texture"; 2 | 3 | export class Texture2D extends Texture { 4 | constructor( 5 | gl: WebGLRenderingContext, 6 | url?: string, 7 | ) { 8 | super( 9 | gl, 10 | url, 11 | ); 12 | } 13 | 14 | public apply(gl: WebGLRenderingContext) { 15 | super.apply(gl); 16 | gl.texImage2D(this.target, 0, this.format, this.format, this.type, this.image); 17 | return this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/Camera/OrhtoCamera-Test.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "../../../src/CanvasToy"; 2 | 3 | describe("OrhtoCamera testing", () => { 4 | it("function adapTargetRadio", () => { 5 | const camera = new CanvasToy.OrthoCamera(); 6 | const x = Math.random() % 100; 7 | const y = Math.random() % 100; 8 | camera.setAspectRadio(x / y); 9 | (expect((camera.right - camera.left) / (camera.top - camera.bottom)) as jasmine.ToyMatchers) 10 | .toBeEqualish(x / y); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/deferred/geometry.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 modelViewProjectionMatrix; 3 | 4 | #ifdef _MAIN_TEXTURE 5 | attribute vec2 aMainUV; 6 | varying vec2 vMainUV; 7 | #endif 8 | 9 | uniform mat4 normalViewMatrix; 10 | attribute vec3 aNormal; 11 | varying vec3 vNormal; 12 | 13 | void main (){ 14 | gl_Position = modelViewProjectionMatrix * vec4(position, 1.0); 15 | vNormal = (normalViewMatrix * vec4(aNormal, 1.0)).xyz; 16 | 17 | #ifdef _MAIN_TEXTURE 18 | vMainUV = aMainUV; 19 | #endif 20 | } 21 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "baseUrl": ".", 4 | "removeComments": true, 5 | "outFile": "index.js", 6 | "target": "es5", 7 | "allowJs": true, 8 | "module": "amd", 9 | "lib":["es5", "es2015.promise", "dom"], 10 | "sourceMap": true, 11 | "experimentalDecorators": true 12 | }, 13 | "compileOnSave": true, 14 | "include" : [ 15 | "../build/canvas-toy-amd.d.ts", 16 | "../build/canvas-toy-amd.js", 17 | "**/*.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/gouraud.frag: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 modelViewProjectionMatrix; 3 | 4 | void main() { 5 | textureColor = colorOrMainTexture(vMainUV); 6 | #ifdef OPEN_LIGHT 7 | totalLighting = ambient; 8 | vec3 normal = normalize(vNormal); 9 | gl_FragColor = vec4(totalLighting, 1.0); 10 | #else 11 | #ifdef USE_COLOR 12 | gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); 13 | #endif 14 | #endif 15 | #ifdef _MAIN_TEXTURE 16 | gl_FragColor = gl_FragColor * textureColor; 17 | #endif 18 | #ifdef USE_COLOR 19 | gl_FragColor = gl_FragColor * color; 20 | #endif 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "module": "amd", 4 | "removeComments": true, 5 | "outFile":"build/release/canvas-toy.js", 6 | "noLib": false, 7 | "target": "es5", 8 | "sourceMap": false, 9 | "allowJs": true 10 | }, 11 | "include":[ 12 | "bower_components/gl-matrix/dist/gl-matrix.js", 13 | "typings/**/*.ts", 14 | "src/**/*.ts" 15 | ], 16 | "exclude":[ 17 | "examples/**/*.ts", 18 | "tests/**/*.ts", 19 | "build/**/*.ts", 20 | "node_modules/**/*.ts" 21 | ], 22 | "compileOnSave": false 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "removeComments": true, 5 | "lib": ["es5", "es2015.promise", "dom"], 6 | "outFile": "build/canvas-toy-amd.js", 7 | "noLib": false, 8 | "target": "es5", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "noImplicitUseStrict": true, 12 | "experimentalDecorators": true 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules/", 19 | "examples/", 20 | "tests/", 21 | "utils/", 22 | "build/" 23 | ], 24 | "compileOnSave": false 25 | } -------------------------------------------------------------------------------- /src/shader/sources/calculators/phong.glsl: -------------------------------------------------------------------------------- 1 | vec3 calculateLight( 2 | vec3 position, 3 | vec3 normal, 4 | vec3 lightDir, 5 | vec3 eyePos, 6 | vec3 specularLight, 7 | vec3 diffuseLight, 8 | float shiness, 9 | float idensity 10 | ) { 11 | float lambortian = max(dot(lightDir, normal), 0.0); 12 | vec3 reflectDir = normalize(reflect(lightDir, normal)); 13 | vec3 viewDir = normalize(eyePos - position); 14 | float specularAngle = max(dot(reflectDir, viewDir), 0.0); 15 | vec3 specularColor = specularLight * pow(specularAngle, shiness); 16 | vec3 diffuseColor = diffuse * lambortian; 17 | return (diffuseColor + specularColor) * idensity; 18 | } 19 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "removeComments": true, 5 | "outDir":"build/", 6 | "lib": ["es5", "es2015.promise", "dom"], 7 | "target": "es5", 8 | "allowJs": true, 9 | "noImplicitUseStrict": true, 10 | "experimentalDecorators":true, 11 | "sourceMap": true 12 | }, 13 | "include": [ 14 | "../node_modules/gl-matrix/dist/gl-matrix.js", 15 | "../src/**/*.ts", 16 | "Util.ts", 17 | "unit/**/*.ts" 18 | ], 19 | "exclude":[ 20 | "../examples/**/*.ts", 21 | "../build/**/*.ts", 22 | "../node_modules/**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore node.js dependency 2 | node_modules/ 3 | 4 | # ignore bower dependency 5 | bower_components/ 6 | 7 | # User-specific stuff: 8 | .idea/ 9 | .vscode/ 10 | 11 | 12 | ## Plugin-specific files: 13 | 14 | # IntelliJ 15 | /out/ 16 | 17 | # mpeltonen/sbt-idea plugin 18 | .idea_modules/ 19 | 20 | # JIRA plugin 21 | atlassian-ide-plugin.xml 22 | 23 | # Crashlytics plugin (for Android Studio and IntelliJ) 24 | com_crashlytics_export_strings.xml 25 | crashlytics.properties 26 | crashlytics-build.properties 27 | fabric.properties 28 | 29 | /tests/npm-debug.log 30 | npm-debug.log 31 | /tests/npm-debug.log.* 32 | npm-debug.log.* 33 | /tests/build/ 34 | 35 | .ruby-version 36 | 37 | /typings/ 38 | /tests/typings/ 39 | -------------------------------------------------------------------------------- /src/materials/Material.ts: -------------------------------------------------------------------------------- 1 | import { vec4 } from "gl-matrix"; 2 | import { Program } from "../shader/Program"; 3 | 4 | export let colors = { 5 | black: vec4.fromValues(0, 0, 0, 1), 6 | gray: vec4.fromValues(0.5, 0.5, 0.5, 1), 7 | red: vec4.fromValues(1, 0, 0, 1), 8 | white: vec4.fromValues(1, 1, 1, 1), 9 | }; 10 | 11 | export abstract class IMaterial { 12 | public name: string; 13 | public defines: string[] = []; 14 | public shader: Program; 15 | 16 | protected gl: WebGLRenderingContext; 17 | 18 | public constructor(gl: WebGLRenderingContext) { 19 | this.gl = gl; 20 | this.shader = this.initShader(gl); 21 | } 22 | 23 | protected abstract initShader(gl: WebGLRenderingContext): Program; 24 | } 25 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/gouraud.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 modelViewProjectionMatrix; 3 | 4 | attribute vec2 aMainUV; 5 | varying vec2 vMainUV; 6 | 7 | void main (){ 8 | gl_Position = modelViewProjectionMatrix * vec4(position, 1.0); 9 | #ifdef OPEN_LIGHT 10 | vec3 normal = (normalMatrix * vec4(aNormal, 0.0)).xyz; 11 | totalLighting = ambient; 12 | normal = normalize(normal); 13 | for (int index = 0; index < LIGHT_NUM; index++) { 14 | totalLighting += calculate_light(gl_Position, normal, lights[index].position, eyePos, lights[index].specular, lights[index].diffuse, 4, lights[index].idensity); 15 | } 16 | vLightColor = totalLighting; 17 | #endif 18 | #ifdef _MAIN_TEXTURE 19 | vTextureCoord = aTextureCoord; 20 | #endif 21 | } 22 | -------------------------------------------------------------------------------- /src/geometries/RectGeometry.ts: -------------------------------------------------------------------------------- 1 | import { Geometry } from "./Geometry"; 2 | 3 | export class RectGeometry extends Geometry { 4 | constructor(gl: WebGLRenderingContext) { 5 | super(gl); 6 | this.attributes.position.data = [ 7 | -1.0, -1.0, 0.0, 8 | 1.0, -1.0, 0.0, 9 | -1.0, 1.0, 0.0, 10 | 1.0, 1.0, 0.0, 11 | ]; 12 | this.attributes.aMainUV.data = [ 13 | 0.0, 0.0, 14 | 1.0, 0.0, 15 | 0.0, 1.0, 16 | 1.0, 1.0, 17 | ]; 18 | this.attributes.aNormal.data = [ 19 | 0, 0, 1, 20 | 0, 0, 1, 21 | 0, 0, 1, 22 | 0, 0, 1, 23 | ]; 24 | this.faces.data = [ 25 | 0, 1, 2, 26 | 2, 1, 3, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/blur/gaussian.glsl: -------------------------------------------------------------------------------- 1 | vec4 gaussian_blur(sampler2D origin, vec2 uv, float blurStep, vec2 blurDir) { 2 | vec4 average = vec4(0.0, 0.0, 0.0, 0.0); 3 | average += texture2D(origin, uv - 4.0 * blurStep * blurDir) * 0.0162162162; 4 | average += texture2D(origin, uv - 3.0 * blurStep * blurDir) * 0.0540540541; 5 | average += texture2D(origin, uv - 2.0 * blurStep * blurDir) * 0.1216216216; 6 | average += texture2D(origin, uv - 1.0 * blurStep * blurDir) * 0.1945945946; 7 | average += texture2D(origin, uv) * 0.2270270270; 8 | average += texture2D(origin, uv + 1.0 * blurStep * blurDir) * 0.1945945946; 9 | average += texture2D(origin, uv + 2.0 * blurStep * blurDir) * 0.1216216216; 10 | average += texture2D(origin, uv + 3.0 * blurStep * blurDir) * 0.0540540541; 11 | average += texture2D(origin, uv + 4.0 * blurStep * blurDir) * 0.0162162162; 12 | return average; 13 | } 14 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/deferred/geometry.frag: -------------------------------------------------------------------------------- 1 | uniform Material uMaterial; 2 | 3 | uniform vec3 eyePos; 4 | varying vec3 vNormal; 5 | 6 | #ifdef _MAIN_TEXTURE 7 | uniform sampler2D uMainTexture; 8 | varying vec2 vMainUV; 9 | #endif 10 | 11 | #ifdef _NORMAL_TEXTURE 12 | uniform sampler2D uNormalTexture; 13 | varying vec2 vNormalUV; 14 | #endif 15 | 16 | void main () { 17 | vec3 normal = normalize(vNormal); 18 | #ifdef _NORMAL_TEXTURE 19 | gl_FragData[0] = vec4(normal, uMaterial.roughness); 20 | #else 21 | gl_FragData[0] = vec4(normal, uMaterial.roughness); 22 | #endif 23 | #ifdef _MAIN_TEXTURE 24 | gl_FragData[1] = vec4(uMaterial.albedo * texture2D(uMainTexture, vMainUV).xyz, uMaterial.metallic); 25 | #else 26 | gl_FragData[1] = vec4(uMaterial.albedo, uMaterial.metallic); 27 | #endif 28 | // save 32 bit depth to render target 3 29 | gl_FragData[2] = packFloat1x32(gl_FragCoord.z); 30 | } 31 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CanvasToy Examples 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/cameras/CubeCamera.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { PerspectiveCamera } from "./PerspectiveCamera"; 3 | 4 | export class CubeCamera extends PerspectiveCamera { 5 | 6 | private _projectionMatrices: mat4[]; 7 | 8 | public constructor() { 9 | super(); 10 | this._projectionMatrices = [0, 0, 0, 0, 0, 0].map(() => mat4.create()); 11 | } 12 | 13 | public compuseProjectionMatrix() { 14 | for (const mat of this._projectionMatrices) { 15 | mat4.perspective( 16 | mat, 17 | this._fovy, 18 | this._aspect, 19 | this._near, 20 | this._far, 21 | ); 22 | } 23 | } 24 | 25 | // public changeZoom(offset: number): CubeCamera { 26 | // throw new Error("Method not implemented."); 27 | // } 28 | 29 | public deCompuseProjectionMatrix() { 30 | // TODO: decompuse cube camera 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test-main.js: -------------------------------------------------------------------------------- 1 | // console.error(window.__karma__.files); 2 | // Get a list of all the test files to include 3 | // 4 | // Get a list of all the test files to include 5 | // 6 | var allTestFiles = []; 7 | 8 | // console.error(Object.keys(window.__karma__.files)); 9 | Object.keys(window.__karma__.files).forEach(function(file) { 10 | if (/.+\-Test\.js/.test(file)) { 11 | // Normalize paths to RequireJS module names. 12 | // If you require sub-dependencies of test files to be loaded as-is 13 | // (requiring file extension) 14 | // then do not normalize the paths 15 | allTestFiles.push(file); 16 | } 17 | }); 18 | 19 | require.config({ 20 | // Karma serves files under /base, which is the basePath from your config file 21 | 22 | paths : {"gl-matrix" : "base/build/node_modules/gl-matrix/dist/gl-matrix"}, 23 | // dynamically load all test files 24 | deps : allTestFiles, 25 | 26 | // we have to kickoff jasmine, as it is asynchronous 27 | callback : window.__karma__.start 28 | }) 29 | -------------------------------------------------------------------------------- /src/shader/sources/light_model/blinn_phong.glsl: -------------------------------------------------------------------------------- 1 | vec3 calculateLight( 2 | Material material, 3 | vec3 viewDir, 4 | vec3 normal, 5 | vec3 lightDir, 6 | vec3 lightColor, 7 | float idensity 8 | ) { 9 | float lambortian = max(dot(lightDir, normal), 0.0); 10 | 11 | // replace R * V with N * H 12 | vec3 H = (lightDir + viewDir) / length(lightDir + viewDir); 13 | float specularAngle = max(dot(H, normal), 0.0); 14 | 15 | vec3 specularColor = material.specular * pow(specularAngle, material.specularExponent); 16 | vec3 diffuseColor = material.diffuse * lambortian; 17 | vec3 color = (diffuseColor + specularColor) * idensity * lightColor; 18 | return color; 19 | } 20 | 21 | vec3 calculateImageBasedLight( 22 | Material material, 23 | vec3 lightDir, 24 | vec3 normal, 25 | vec3 viewDir, 26 | vec3 specularColor, 27 | vec3 diffuseColor 28 | ) { 29 | 30 | vec3 color = mix(specularColor, diffuseColor, material.reflectivity); 31 | return color; 32 | } 33 | -------------------------------------------------------------------------------- /src/shader/Attibute.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "../DataTypeEnum"; 2 | 3 | export class Attribute { 4 | public name?: string; 5 | public size: number = 3; 6 | public data: number[] = []; 7 | public type: number; 8 | public index: number = 0; 9 | public stride: number = 0; 10 | public buffer: WebGLBuffer = null; 11 | public gl: WebGLRenderingContext = null; 12 | constructor( 13 | gl: WebGLRenderingContext, 14 | paramter: { type: number, size?: number, data?: number[], stride?: number }, 15 | ) { 16 | this.buffer = gl.createBuffer(); 17 | this.gl = gl; 18 | for (const attributeInfo in paramter) { 19 | this[attributeInfo] = paramter[attributeInfo] ? paramter[attributeInfo] : this[attributeInfo]; 20 | } 21 | switch (paramter.type) { 22 | case DataType.float: this.type = gl.FLOAT; break; 23 | case DataType.int: this.type = gl.INT; break; 24 | default: break; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mesh.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { DataType } from "./DataTypeEnum"; 3 | import { uniform } from "./Decorators"; 4 | import { Geometry } from "./geometries/Geometry"; 5 | import { IMaterial } from "./materials/Material"; 6 | import { Object3d } from "./Object3d"; 7 | 8 | export class Mesh extends Object3d { 9 | public readonly geometry: Geometry; 10 | 11 | public materials: IMaterial[] = []; 12 | 13 | @uniform(DataType.mat4, "modelMatrix") 14 | public get matrix(): mat4 { 15 | return this._matrix; 16 | } 17 | 18 | @uniform(DataType.mat4) 19 | public get normalMatrix(): mat4 { 20 | return mat4.transpose( 21 | mat4.create(), 22 | mat4.invert(mat4.create(), this._matrix), 23 | ); 24 | } 25 | 26 | constructor(geometry: Geometry, materials: IMaterial[]) { 27 | super(); 28 | this.materials = materials; 29 | this.geometry = geometry; 30 | } 31 | 32 | public drawMode(gl: WebGLRenderingContext): number { 33 | return gl.STATIC_DRAW; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute: 2 | 3 | ### Install dependency: 4 | 5 | ```bash 6 | npm install -g typescript@2 typescript-formatter http-server 7 | npm install 8 | npm link typescript 9 | ``` 10 | 11 | we use typescript **2.x** rather than current **1.x** to match [tsconfig.json schema on the official site](http://json.schemastore.org/tsconfig) 12 | 13 | 14 | ### Building: 15 | 16 | ```bash 17 | npm run build 18 | ``` 19 | 20 | ### Testing: 21 | 22 | CanvasToy now use Karma and Jasmine for unit test 23 | 24 | ```bash 25 | npm test 26 | ``` 27 | You can also run visible test from example 28 | 29 | first, start a local http-server, for example, 30 | 31 | ```bash 32 | http-server 33 | ``` 34 | 35 | Than open the local url with you browser and goto the example folder, you will find all examples there 36 | 37 | ### Format: 38 | 39 | Please, be sure to have formatted you code with typescript-formatter before any pull request 40 | 41 | ```bash 42 | tsfmt src/**/*.ts -r 43 | tsfmt tests/**/*.ts -r 44 | ``` 45 | 46 | 47 | 48 | Feel free to contribute 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Danielhu229 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "align": [ 5 | true, 6 | "arguments", 7 | "elements", 8 | "members", 9 | "parameters", 10 | "statements" 11 | ], 12 | "no-namespace": false, 13 | "no-default-export": true, 14 | "no-shadowed-variable": [true], 15 | "variable-name": [true, "allow-leading-underscore"], 16 | "no-bitwise": false, 17 | "object-literal-sort-keys": false, 18 | "no-console": false, 19 | "interface-name": false, 20 | "forin": false, 21 | "max-classes-per-file": false, 22 | "whitespace": [ 23 | true, 24 | "check-branch", 25 | "check-decl", 26 | "check-operator", 27 | "check-module", 28 | "check-separator", 29 | "check-rest-spread", 30 | "check-type", 31 | "check-typecast", 32 | "check-type-operator", 33 | "check-preblock" 34 | ], 35 | "no-unused-expression": true, 36 | "no-unbound-method": true, 37 | "indent": [true, "spaces", 4] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/materials/ESM/DepthPackMaterial.ts: -------------------------------------------------------------------------------- 1 | import { Program, shaderPassLib } from "../../shader/Program"; 2 | import { ShaderBuilder } from "../../shader/ShaderBuilder"; 3 | import { ShaderSource } from "../../shader/shaders"; 4 | import { IMaterial } from "../Material"; 5 | 6 | export class LinearDepthPackMaterial extends IMaterial { 7 | protected initShader(gl: WebGLRenderingContext): Program { 8 | return new ShaderBuilder() 9 | .resetShaderLib() 10 | .addShaderLib(ShaderSource.calculators__linearlize_depth_glsl) 11 | .addShaderLib(ShaderSource.calculators__packFloat1x32_glsl) 12 | .setShadingFrag(ShaderSource.interploters__forward__esm__depth_frag) 13 | .setShadingVert(ShaderSource.interploters__forward__esm__depth_vert) 14 | .setExtraRenderParamHolder("transform", { 15 | uniforms: { 16 | modelViewProjectionMatrix: 17 | shaderPassLib.uniforms.modelViewProjectionMatrix, 18 | modelViewMatrix: shaderPassLib.uniforms.modelViewMatrix, 19 | }, 20 | }) 21 | .setExtraRenderParamHolder("pcss", { 22 | defines: shaderPassLib.defines, 23 | }) 24 | .build(gl); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/resources/models/cube/cube.obj: -------------------------------------------------------------------------------- 1 | # Unit-volume cube with the same texture coordinates on each face. 2 | # 3 | # Created by Morgan McGuire and released into the Public Domain on 4 | # July 16, 2011. 5 | # 6 | # http://graphics.cs.williams.edu/data 7 | # 8 | # Added comments - Scott Kuhl 9 | # Corrected face normals - Scott Kuhl 10 | # Renamed texture and material file - Scott Kuhl 11 | 12 | g cube 13 | mtllib cube.mtl 14 | 15 | # Vertices 16 | v -0.5 0.5 -0.5 17 | v -0.5 0.5 0.5 18 | v 0.5 0.5 0.5 19 | v 0.5 0.5 -0.5 20 | v -0.5 -0.5 -0.5 21 | v -0.5 -0.5 0.5 22 | v 0.5 -0.5 0.5 23 | v 0.5 -0.5 -0.5 24 | 25 | # Texture coordinates 26 | vt 0 1 27 | vt 0 0 28 | vt 1 0 29 | vt 1 1 30 | 31 | # Normals 32 | vn -1 0 0 33 | vn 1 0 0 34 | vn 0 -1 0 35 | vn 0 1 0 36 | vn 0 0 -1 37 | vn 0 0 1 38 | 39 | usemtl default 40 | 41 | # Faces (vertex/texcoord/normal) 42 | # -X face 43 | f 6/4/1 2/3/1 1/2/1 44 | f 6/4/1 1/2/1 5/1/1 45 | # +X face 46 | f 8/4/2 4/3/2 3/2/2 47 | f 8/4/2 3/2/2 7/1/2 48 | # -Y face 49 | f 8/4/3 7/3/3 6/2/3 50 | f 8/4/3 6/2/3 5/1/3 51 | # +Y face 52 | f 3/4/4 4/3/4 1/2/4 53 | f 3/4/4 1/2/4 2/1/4 54 | # -Z face 55 | f 5/4/5 1/3/5 4/2/5 56 | f 5/4/5 4/2/5 8/1/5 57 | # +Z face 58 | f 7/4/6 3/3/6 2/2/6 59 | f 7/4/6 2/2/6 6/1/6 60 | -------------------------------------------------------------------------------- /src/textures/DataTexture.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from "./Texture"; 2 | 3 | export class DataTexture extends Texture { 4 | public width: number; 5 | public height: number; 6 | 7 | private data: TypeArray; 8 | 9 | constructor(gl: WebGLRenderingContext, data: TypeArray, width: number = 16, height: number = 16) { 10 | super(gl); 11 | this.data = data; 12 | this.width = width; 13 | this.height = height; 14 | } 15 | 16 | public resetData(gl: WebGLRenderingContext, data: TypeArray, width?: number, height?: number) { 17 | this.data = data; 18 | this.width = width ? width : this.width; 19 | this.height = height ? height : this.height; 20 | this.apply(gl); 21 | return this; 22 | } 23 | 24 | public apply(gl: WebGLRenderingContext) { 25 | super.apply(gl); 26 | gl.texImage2D( 27 | this.target, 28 | 0, 29 | this.format, 30 | this.width, 31 | this.height, 32 | 0, 33 | this.format, 34 | this.type, 35 | this.data, 36 | ); 37 | return this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/esm/prefiltering.frag: -------------------------------------------------------------------------------- 1 | uniform sampler2D uOrigin; 2 | uniform vec2 uBlurDir; 3 | uniform float uBlurStep; 4 | 5 | uniform float lightArea; 6 | 7 | varying vec2 uv; 8 | 9 | void main () { 10 | float base = texture2D(uOrigin, uv).r; 11 | float block = 0.0; 12 | 13 | for (int i = 0; i < BLOCK_SIZE; ++i) { 14 | for (int j = 0; j < BLOCK_SIZE; ++j) { 15 | float d = texture2D(uOrigin, uv + vec2(float(i - BLOCK_SIZE / 2) + 0.5, float(j - BLOCK_SIZE / 2) + 0.5) * uBlurStep).r; 16 | block += step(base, d) * d / float(BLOCK_SIZE * BLOCK_SIZE); 17 | } 18 | } 19 | 20 | float kenelSize = min(4.0, lightArea * (base - block) / base); 21 | float stepSize = kenelSize / float(FILTER_SIZE); 22 | 23 | float sum = 0.0; 24 | 25 | for (int i = 0; i < FILTER_SIZE; ++i) { 26 | for (int j = 0; j < FILTER_SIZE; ++j) { 27 | float d = texture2D(uOrigin, 28 | uv + stepSize * vec2(float(i - FILTER_SIZE / 2) + 0.5, float(j - FILTER_SIZE / 2) + 0.5) * uBlurStep).r; 29 | sum += exp(d - base) / float(FILTER_SIZE * FILTER_SIZE); 30 | } 31 | } 32 | 33 | float average = log(sum) + base; 34 | 35 | gl_FragColor.r = average; 36 | gl_FragColor.g = kenelSize; 37 | } 38 | -------------------------------------------------------------------------------- /src/materials/ESM/LogBlurMaterial.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "gl-matrix"; 2 | import { DataType } from "../../DataTypeEnum"; 3 | import { texture, uniform } from "../../Decorators"; 4 | import { Program, shaderPassLib } from "../../shader/Program"; 5 | import { ShaderBuilder } from "../../shader/ShaderBuilder"; 6 | import { ShaderSource } from "../../shader/shaders"; 7 | import { Texture } from "../../textures/Texture"; 8 | import { IMaterial } from "../Material"; 9 | export class PCSSFilteringMaterial extends IMaterial { 10 | @texture("uOrigin") 11 | public origin: Texture; 12 | 13 | @uniform(DataType.vec2, "uBlurDir") 14 | public blurDirection: vec2 = vec2.fromValues(1, 0); 15 | 16 | @uniform(DataType.float, "uBlurStep") 17 | public blurStep: number = 0.01; 18 | 19 | protected initShader(gl: WebGLRenderingContext): Program { 20 | return new ShaderBuilder() 21 | .resetShaderLib() 22 | .setShadingFrag( 23 | ShaderSource.interploters__forward__esm__prefiltering_frag, 24 | ) 25 | .setShadingVert( 26 | ShaderSource.interploters__forward__esm__prefiltering_vert, 27 | ) 28 | .setExtraRenderParamHolder("pcss", { 29 | defines: shaderPassLib.defines, 30 | }) 31 | .build(gl); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/basic/shadow/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | light test-CanvasToy 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 43 | 44 | -------------------------------------------------------------------------------- /src/shader/sources/definitions/light.glsl: -------------------------------------------------------------------------------- 1 | #define SHADOW_LEVEL_NONE 0 2 | #define SHADOW_LEVEL_HARD 1 3 | #define SHADOW_LEVEL_SOFT 2 4 | #define SHADOW_LEVEL_PCSS 3 5 | 6 | struct Light { 7 | vec3 color; 8 | float idensity; 9 | vec3 direction; 10 | #ifdef RECEIVE_SHADOW 11 | lowp int shadowLevel; 12 | float softness; 13 | float shadowMapSize; 14 | mat4 projectionMatrix; 15 | mat4 viewMatrix; 16 | #endif 17 | }; 18 | 19 | struct DirectLight { 20 | vec3 color; 21 | float idensity; 22 | vec3 direction; 23 | #ifdef RECEIVE_SHADOW 24 | lowp int shadowLevel; 25 | float softness; 26 | float shadowMapSize; 27 | mat4 projectionMatrix; 28 | mat4 viewMatrix; 29 | #endif 30 | }; 31 | 32 | struct PointLight { 33 | vec3 color; 34 | float idensity; 35 | float radius; 36 | vec3 position; 37 | float squareAtten; 38 | float linearAtten; 39 | float constantAtten; 40 | #ifdef RECEIVE_SHADOW 41 | lowp int shadowLevel; 42 | float softness; 43 | float shadowMapSize; 44 | mat4 projectionMatrix; 45 | mat4 viewMatrix; 46 | float pcssArea; 47 | #endif 48 | }; 49 | 50 | struct SpotLight { 51 | vec3 color; 52 | float idensity; 53 | float radius; 54 | vec3 position; 55 | float squareAtten; 56 | float linearAtten; 57 | float constantAtten; 58 | float coneAngleCos; 59 | vec3 spotDir; 60 | #ifdef RECEIVE_SHADOW 61 | lowp int shadowLevel; 62 | float softness; 63 | float shadowMapSize; 64 | mat4 projectionMatrix; 65 | mat4 viewMatrix; 66 | float pcssArea; 67 | #endif 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canvas-toy", 3 | "author": "danielhu", 4 | "keywords": [ 5 | "webgl", 6 | "glsl", 7 | "rendering", 8 | "deferred", 9 | "tbdr" 10 | ], 11 | "main": "build/debug/canvas-toy.js", 12 | "types": "build/debug/canvas-toy.d.ts", 13 | "description": "lightweight webgl rendering tool", 14 | "version": "0.4.0", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/UniqueStudio/CanvasToy" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/UniqueStudio/CanvasToy/issues" 22 | }, 23 | "devDependencies": { 24 | "@types/gl-matrix": "^2.2.34", 25 | "@types/jasmine": "^2.5.47", 26 | "@types/webgl-ext": "0.0.28", 27 | "awesome-typescript-loader": "^3.1.2", 28 | "cz-conventional-changelog": "latest", 29 | "gl": "latest", 30 | "jasmine-core": "latest", 31 | "karma": "latest", 32 | "karma-chrome-launcher": "latest", 33 | "karma-firefox-launcher": "latest", 34 | "karma-jasmine": "latest", 35 | "karma-requirejs": "^1.1.0", 36 | "requirejs": "^2.3.3", 37 | "webgl-debug": "^1.0.2" 38 | }, 39 | "scripts": { 40 | "test": "cd tests && tsc && karma start karma.conf.js --browsers Chrome", 41 | "testci": "cd tests && sh testingci.sh", 42 | "build": "node scripts/build.js", 43 | "build_example": "cd examples && tsc", 44 | "bundle": "webpack --config webpack.config.js" 45 | }, 46 | "config": { 47 | "commitizen": { 48 | "path": "./node_modules/cz-conventional-changelog" 49 | } 50 | }, 51 | "dependencies": { 52 | "gl-matrix": "^2.3.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lights/DampingLight.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { DataType } from "../DataTypeEnum"; 4 | import { uniform } from "../Decorators"; 5 | 6 | import { Light } from "./Light"; 7 | 8 | export abstract class DampingLight extends Light { 9 | 10 | @uniform(DataType.vec3) 11 | public get position() { 12 | return this._position; 13 | } 14 | 15 | protected _radius: number = 10; 16 | 17 | protected _squareAttenuation: number = 0.01; 18 | 19 | protected _linearAttenuation: number = 0.01; 20 | 21 | protected _constantAttenuation: number = 0.01; 22 | 23 | @uniform(DataType.float, "squareAtten") 24 | public get squareAttenuation() { 25 | return this._squareAttenuation; 26 | } 27 | @uniform(DataType.float, "linearAtten") 28 | public get linearAttenuation() { 29 | return this._squareAttenuation; 30 | } 31 | 32 | @uniform(DataType.float, "constantAtten") 33 | public get constantAttenuation() { 34 | return this._constantAttenuation; 35 | } 36 | 37 | @uniform(DataType.float) 38 | public get radius() { 39 | return this._radius; 40 | } 41 | 42 | public setSquareAtten(atten: number) { 43 | this._squareAttenuation = atten; 44 | return this; 45 | } 46 | 47 | public setLinearAtten(atten: number) { 48 | this._linearAttenuation = atten; 49 | return this; 50 | } 51 | 52 | public setConstAtten(atten: number) { 53 | this._constantAttenuation = atten; 54 | return this; 55 | } 56 | 57 | public abstract setRadius(radius: number); 58 | } 59 | -------------------------------------------------------------------------------- /src/geometries/TileGeometry.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Geometry } from "./Geometry"; 3 | 4 | export class TileGeometry extends Geometry { 5 | private _widthSegments: number = 8; 6 | private _heightSegments: number = 6; 7 | private _width: number = 2; 8 | private _height: number = 2; 9 | constructor(gl: WebGLRenderingContext) { 10 | super(gl); 11 | } 12 | 13 | public build() { 14 | let index = 0; 15 | const grid = []; 16 | for (let x = 0; x <= this._widthSegments; ++x) { 17 | const row = []; 18 | for (let y = 0; y <= this._heightSegments; ++y) { 19 | const position = [ 20 | this._width * (x - this._widthSegments / 2) / this._widthSegments, 21 | this._height * (y - this._heightSegments / 2) / this._heightSegments, 22 | 0, 23 | ]; 24 | const aMainUV = [x / this._widthSegments, y / this._heightSegments]; 25 | const aNormal = [0, 0, 1]; 26 | this.addVertex({ position, aNormal, aMainUV }); 27 | row.push(index++); 28 | } 29 | grid.push(row); 30 | } 31 | for (let x = 0; x < this._widthSegments; ++x) { 32 | for (let y = 0; y < this._heightSegments; ++y) { 33 | const a = grid[x][y]; 34 | const b = grid[x + 1][y]; 35 | const c = grid[x + 1][y + 1]; 36 | const d = grid[x][y + 1]; 37 | this.faces.data.push(a, b, c); 38 | this.faces.data.push(a, d, c); 39 | } 40 | } 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/phong.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 position; 2 | uniform mat4 modelViewProjectionMatrix; 3 | uniform mat4 modelMatrix; 4 | 5 | attribute vec2 aMainUV; 6 | varying vec2 vMainUV; 7 | 8 | uniform mat4 normalMatrix; 9 | attribute vec3 aNormal; 10 | varying vec3 vNormal; 11 | varying vec3 vPosition; 12 | varying vec4 clipPos; 13 | 14 | 15 | #if (directLightsNum > 0) 16 | uniform DirectLight directLights[directLightsNum]; 17 | #ifdef RECEIVE_SHADOW 18 | varying vec4 directShadowCoord[directLightsNum]; 19 | varying float directLightDepth[directLightsNum]; 20 | #endif 21 | #endif 22 | 23 | #if (spotLightsNum > 0) 24 | uniform SpotLight spotLights[spotLightsNum]; 25 | #ifdef RECEIVE_SHADOW 26 | varying vec4 spotShadowCoord[spotLightsNum]; 27 | varying float spotLightDepth[spotLightsNum]; 28 | #endif 29 | #endif 30 | 31 | 32 | void main (){ 33 | gl_Position = modelViewProjectionMatrix * vec4(position, 1.0); 34 | clipPos = gl_Position; 35 | vec4 worldPos = (modelMatrix * vec4(position, 1.0)); 36 | vPosition = worldPos.xyz; 37 | vNormal = (normalMatrix * vec4(aNormal, 1.0)).xyz; 38 | vMainUV = aMainUV; 39 | 40 | #ifdef RECEIVE_SHADOW 41 | #if (directLightsNum > 0) 42 | for (int i = 0; i < directLightsNum; ++i) { 43 | directShadowCoord[i] = directLights[i].projectionMatrix * directLights[i].viewMatrix * worldPos; 44 | directLightDepth[i] = length((directLights[i].viewMatrix * worldPos).xyz); 45 | } 46 | #endif 47 | 48 | #if (spotLightsNum > 0) 49 | for (int i = 0; i < spotLightsNum; ++i) { 50 | spotShadowCoord[i] = spotLights[i].projectionMatrix * spotLights[i].viewMatrix * worldPos; 51 | spotLightDepth[i] = length((spotLights[i].viewMatrix * worldPos).xyz); 52 | } 53 | #endif 54 | #endif 55 | } 56 | -------------------------------------------------------------------------------- /src/materials/SkyMaterial.ts: -------------------------------------------------------------------------------- 1 | import { mat4, quat } from "gl-matrix"; 2 | 3 | import { DataType } from "../DataTypeEnum"; 4 | import { texture } from "../Decorators"; 5 | 6 | import { Program } from "../shader/Program"; 7 | import { ShaderBuilder } from "../shader/ShaderBuilder"; 8 | import { ShaderSource } from "../shader/shaders"; 9 | import { CubeTexture } from "../textures/CubeTexture"; 10 | import { IMaterial } from "./Material"; 11 | 12 | export class SkyMaterial extends IMaterial { 13 | @texture("uCubeTexture") 14 | public cubeTexture: CubeTexture; 15 | 16 | constructor(gl: WebGLRenderingContext, cubeTexture: CubeTexture) { 17 | super(gl); 18 | this.cubeTexture = cubeTexture; 19 | } 20 | 21 | protected initShader(gl: WebGLRenderingContext): Program { 22 | return new ShaderBuilder() 23 | .resetShaderLib() 24 | .setShadingVert(ShaderSource.interploters__forward__skybox_vert) 25 | .setShadingFrag(ShaderSource.interploters__forward__skybox_frag) 26 | .setExtraRenderParamHolder("skyTransform", { 27 | uniforms: { 28 | viewProjectionMatrix: { 29 | type: DataType.mat4, 30 | updator: ({ mesh, camera }) => { 31 | let rotateOnlyViewMatrix = mat4.fromQuat( 32 | mat4.create(), 33 | mat4.getRotation(quat.create(), camera.matrix), 34 | ); 35 | rotateOnlyViewMatrix = mat4.invert( 36 | mat4.create(), 37 | rotateOnlyViewMatrix, 38 | ); 39 | return mat4.multiply( 40 | mat4.create(), 41 | camera.projectionMatrix, 42 | rotateOnlyViewMatrix, 43 | ); 44 | }, 45 | }, 46 | }, 47 | }) 48 | .build(gl); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/basic/renderToTexture/color.ts: -------------------------------------------------------------------------------- 1 | 2 | // examples.push((canvas: HTMLCanvasElement) => { 3 | // const renderer = new CanvasToy.Renderer(canvas); 4 | // const scenes = Array(2, 0).map(() => new CanvasToy.Scene()); 5 | // const cameras = Array(2, 0).map(() => new CanvasToy.PerspectiveCamera()); 6 | // const light = new CanvasToy.PointLight(renderer.gl); 7 | // const cubes = [ 8 | // new CanvasToy.Mesh(new CanvasToy.CubeGeometry(renderer.gl), 9 | // [new CanvasToy.StandardMaterial(renderer.gl)]), 10 | // ]; 11 | // 12 | // (cubes[0].materials[0] as CanvasToy.StandardMaterial) 13 | // .mainTexture = new CanvasToy.Texture2D(renderer.gl, "resources/images/chrome.png") 14 | // .setFormat(renderer.gl.RGBA); 15 | // cameras[0].setPosition(vec3.fromValues(0, 0, 5)); 16 | // scenes[0].ambientLight = vec3.fromValues(0.1, 0.1, 0.1); 17 | // scenes[1].ambientLight = vec3.fromValues(0.1, 0.1, 0.1); 18 | // light.setPosition(vec3.fromValues(100, 0, 100)); 19 | // scenes[0].addLight(light).addObject(cameras[0]).addObject(cubes[0]); 20 | // 21 | // const fbo = renderer.createFrameBuffer(); 22 | // const rttTexture = fbo.attachments.color.targetTexture; 23 | // 24 | // cubes.push( 25 | // new CanvasToy.Mesh( 26 | // new CanvasToy.CubeGeometry(renderer.gl), 27 | // [new CanvasToy.StandardMaterial(renderer.gl, { mainTexture: rttTexture })], 28 | // ), 29 | // ); 30 | // cubes[0].registUpdate(() => { 31 | // cubes.forEach((cube) => { 32 | // cube.rotateY(0.01); 33 | // }); 34 | // }); 35 | // cameras[1].setPosition(vec3.fromValues(0, 0, 5)); 36 | // scenes[1].addLight(light).addObject(cameras[1]).addObject(cubes[1]); 37 | // scenes[0].addLight(light); 38 | // renderer.renderFBO(scenes[0], cameras[0]); 39 | // renderer.render(scenes[1], cameras[1]); 40 | // return renderer; 41 | // }); 42 | -------------------------------------------------------------------------------- /examples/basic/bones/index.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "CanvasToy"; 2 | import { vec3 } from "gl-matrix"; 3 | import { createCanvas, onMouseEvent, onMouseOnStart } from "global"; 4 | 5 | const renderer = new CanvasToy.Renderer(createCanvas()); 6 | 7 | const scene = new CanvasToy.Scene(); 8 | const camera = new CanvasToy.PerspectiveCamera(); 9 | 10 | const mainTexture = new CanvasToy.Texture2D(renderer.gl, "resources/images/wood.jpg"); 11 | 12 | const material = new CanvasToy.StandardMaterial(renderer.gl) 13 | .setMetallic(0.1).setRoughness(0.8).setMainTexture(mainTexture).setCastShadow(true); 14 | const meshes: CanvasToy.Object3d[] = []; 15 | 16 | for (let i = 0; i < 4; ++i) { 17 | const mesh = new CanvasToy.Mesh( 18 | new CanvasToy.SphereGeometry(renderer.gl).setWidthSegments(50).setHeightSegments(50).build(), 19 | // new CanvasToy.CubeGeometry(renderer.gl), 20 | [material], 21 | ); 22 | if (i > 0) { 23 | mesh.setParent(meshes[i - 1]); 24 | if (i === 3) { 25 | mesh.setLocalPosition(vec3.fromValues(0, 2.5 - i / 4.0, 0)); 26 | } else { 27 | mesh.setLocalPosition(vec3.fromValues(2.5 - i / 4.0, 0, 0)); 28 | } 29 | } 30 | const scaleFactor = Math.pow(2, (1 - i)); 31 | mesh.setScaling(vec3.fromValues(scaleFactor, scaleFactor, scaleFactor)); 32 | meshes.push(mesh); 33 | } 34 | 35 | meshes[0].translate(vec3.fromValues(0, 0, -10)); 36 | 37 | const light = new CanvasToy.DirectionalLight(renderer) 38 | .rotateY(Math.PI / 3) 39 | .setPosition(vec3.fromValues(5, 0, -5)) 40 | .lookAt(meshes[0].position); 41 | let t = 0; 42 | 43 | scene.addOnUpdateListener((dt) => { 44 | meshes[0].rotateY(-0.005); 45 | meshes[1].rotateY(0.01); 46 | meshes[2].rotateX(0.05); 47 | t += dt; 48 | }); 49 | 50 | scene.addObject(meshes[0], camera); 51 | scene.addLight(light); 52 | renderer.render(scene, camera); 53 | renderer.stop(); 54 | onMouseOnStart(renderer); 55 | onMouseEvent(renderer, camera); 56 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # Exit with nonzero exit code if anything fails 3 | 4 | SOURCE_BRANCH="master" 5 | TARGET_BRANCH="gh-pages" 6 | 7 | # Pull requests and commits to other branches shouldn't try to deploy, just build to verify 8 | if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ]; then 9 | echo "Skipping deploy; just doing a build." 10 | npm run build 11 | exit 0 12 | fi 13 | 14 | # Save some useful information 15 | REPO=`git config remote.origin.url` 16 | SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} 17 | SHA=`git rev-parse --verify HEAD` 18 | 19 | # Clone the existing gh-pages for this repo into out/ 20 | # Create a new empty branch if gh-pages doesn't exist yet (should only happen on first deply) 21 | git clone $REPO site 22 | cd site 23 | git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH 24 | cd .. 25 | 26 | 27 | # Run our compile script 28 | npm run build 29 | 30 | rm -rf site/examples 31 | rm -rf site/build 32 | 33 | cp -r examples/ site/examples 34 | cp -r build/ site/build 35 | cp package.json site/package.json 36 | # Now let's go have some fun with the cloned repo 37 | cd site 38 | npm install --production 39 | git config user.name "Travis CI" 40 | git config user.email "yimingdz@gmail.com" 41 | 42 | # Commit the "changes", i.e. the new version. 43 | # The delta will show diffs between new and old versions. 44 | git add . 45 | if git commit -m "Deploy to GitHub Pages: ${SHA}"; then 46 | 47 | # Get the deploy key by using Travis's stored variables to decrypt deploy_key.enc 48 | ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" 49 | ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" 50 | ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} 51 | ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} 52 | openssl aes-256-cbc -K $encrypted_38efeecdea21_key -iv $encrypted_38efeecdea21_iv -in ../canvas-toy-deploy.enc -out deploy_key -d 53 | chmod 600 deploy_key 54 | eval `ssh-agent -s` 55 | ssh-add deploy_key 56 | 57 | # Now that we're all set up, we can push. 58 | git push $SSH_REPO $TARGET_BRANCH 59 | fi 60 | -------------------------------------------------------------------------------- /scripts/tsc.js: -------------------------------------------------------------------------------- 1 | const ts = require("typescript"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const process = require("process"); 5 | 6 | function reportDiagnostics(diagnostics) { 7 | diagnostics.forEach(diagnostic => { 8 | let message = "Error"; 9 | if (diagnostic.file) { 10 | const where = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 11 | message += ' ' + diagnostic.file.fileName + ' ' + where.line + ', ' + where.character + 1; 12 | } 13 | message += ": " + ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 14 | console.log(message); 15 | }); 16 | } 17 | 18 | function readConfigFile(configFileName) { 19 | // Read config file 20 | const configFileText = fs.readFileSync(configFileName).toString(); 21 | 22 | // Parse JSON, after removing comments. Just fancier JSON.parse 23 | const result = ts.parseConfigFileTextToJson(configFileName, configFileText); 24 | const configObject = result.config; 25 | if (!configObject) { 26 | reportDiagnostics([result.error]); 27 | process.exit(1); 28 | } 29 | 30 | // Extract config infromation 31 | const configParseResult = ts.parseJsonConfigFileContent(configObject, ts.sys, path.dirname(configFileName)); 32 | if (configParseResult.errors.length > 0) { 33 | reportDiagnostics(configParseResult.errors); 34 | process.exit(1); 35 | } 36 | return configParseResult; 37 | } 38 | 39 | 40 | function compile(configFileName) { 41 | // Extract configuration from config file 42 | const config = readConfigFile(configFileName); 43 | 44 | // Compile 45 | const program = ts.createProgram(config.fileNames, config.options); 46 | const emitResult = program.emit(); 47 | 48 | // Report errors 49 | reportDiagnostics(ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics)); 50 | 51 | // Return code 52 | const exitCode = emitResult.emitSkipped ? 1 : 0; 53 | return exitCode; 54 | } 55 | 56 | exports.compile = compile; 57 | -------------------------------------------------------------------------------- /examples/basic/Loader/obj_mtl.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "CanvasToy"; 2 | import { vec3 } from "gl-matrix"; 3 | import { createCanvas, createSkyBox, onMouseOnStart } from "global"; 4 | 5 | const renderer = new CanvasToy.Renderer(createCanvas()); 6 | const scene = new CanvasToy.Scene(); 7 | const camera = new CanvasToy.PerspectiveCamera() 8 | .translate(vec3.fromValues(0, 2, 5)) 9 | .lookAt(vec3.create()); 10 | const light = new CanvasToy.SpotLight(renderer) 11 | .translate(vec3.fromValues(-5, 5, 0)) 12 | .setConeAngle(Math.PI / 3) 13 | .setIdensity(10) 14 | .rotateX(-Math.PI / 4) 15 | .rotateY(-Math.PI / 4); 16 | scene.addLight(light); 17 | 18 | const skyTexture = new CanvasToy.CubeTexture( 19 | renderer.gl, 20 | { 21 | xpos: "resources/images/skybox/arid2_rt.jpg", 22 | xneg: "resources/images/skybox/arid2_lf.jpg", 23 | ypos: "resources/images/skybox/arid2_up.jpg", 24 | yneg: "resources/images/skybox/arid2_dn.jpg", 25 | zpos: "resources/images/skybox/arid2_bk.jpg", 26 | zneg: "resources/images/skybox/arid2_ft.jpg", 27 | }, 28 | ); 29 | 30 | scene.addObject(createSkyBox(renderer, skyTexture)); 31 | 32 | const teapot = CanvasToy.OBJLoader.load(renderer.gl, "resources/models/teapot/teapot.obj"); 33 | teapot.setAsyncFinished(teapot.asyncFinished().then(() => { 34 | const material = (teapot.children[0] as CanvasToy.Mesh).materials[0] as CanvasToy.StandardMaterial; 35 | material.setEnvironmentMap(skyTexture).setCastShadow(true).setMetallic(0.9).setRoughness(0.1); 36 | // (teapot.children[0] as CanvasToy.Mesh).materials[0] = new CanvasToy.StandardMaterial(renderer.gl); 37 | return Promise.resolve(teapot); 38 | })); 39 | teapot.setScaling(vec3.fromValues(0.1, 0.1, 0.1)); 40 | 41 | scene.addObject(teapot); 42 | camera.lookAt(teapot.position); 43 | let time = 0; 44 | scene.addOnUpdateListener(() => { 45 | time += 1 / 60; 46 | teapot.rotateY(0.01); 47 | }); 48 | renderer.render(scene, camera); 49 | renderer.stop(); 50 | onMouseOnStart(renderer); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CanvasToy.js 2 | 3 | ![](https://travis-ci.org/Danielhu229/CanvasToy.svg?branch=master) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 4 | 5 | ## *This is my undergraduate project, it is no longer suitable for modern typescript dev pipeline, please consider babylonjs for serious ts 3d engine* 6 | 7 | 8 | Lightweight WebGL/WenGL2 rendering engine, see some [examples](http://canvastoy.com/examples) here 9 | 10 | 11 | ### roadmap before 1.0: 12 | 13 | + PipeLine 14 | * [x] Forward Rendering 15 | * [x] Tile Based Deferred Rendering(TBDR) 16 | + Shading 17 | * [x] Physical-based(GGX) material model 18 | * [x] Image Based lighting (IBL) 19 | * [x] Traditional material model(blinn-phong) 20 | * [ ] Reflection and retracions 21 | + Load and Parsing 22 | * [x] .obj .mtl, 23 | * [ ] .fbx 24 | * [ ] .gltf 25 | * [x] Variours types of images (through HTMLImageElement) as Textures 26 | + Lights 27 | * [x] Point lights 28 | * [x] Directional lights 29 | * [x] Spot lights 30 | + Shadows 31 | * [x] Basic shadow mapping 32 | * [x] Expotional soft shadow mapping (forward approach) 33 | * [ ] Expotional soft shadow mapping (deferred approach) 34 | * [x] Percentage closer soft shadow(PCSS) 35 | * [ ] Screen-space shadow mapping (deferred approach) 36 | + Transforms 37 | * [x] Basic object transforms 38 | * [x] Scenes Graph 39 | * [ ] Bones and Animation Parsing 40 | + Engineering 41 | + [ ] More than 95% test cover 42 | * [x] Automatically handle async resources loading, including models, images and videos. You don't need to write some code like *image.onload = ...* 43 | * [x] Using github workflow 44 | * [x] Build && test using pure node.js scripts and typescript compiler, without gulp or grunt, webpack and bash , etc. 45 | * [x] Generate a typescript declaration file .d.ts to provide type hint. 46 | 47 | ### Contribute: 48 | 49 | See [CONTRIBUTING.md](CONTRIBUTING.md). 50 | 51 | ## License 52 | 53 | The MIT license. 54 | -------------------------------------------------------------------------------- /src/CanvasToy.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ifdefine, 3 | texture, 4 | textureArray, 5 | uniform, 6 | uniformArray, 7 | } from "./Decorators"; 8 | 9 | export { IAsyncResource } from "./IAsyncResource"; 10 | 11 | export { Renderer } from "./renderer/Renderer"; 12 | export { 13 | FrameBuffer, 14 | Attachment, 15 | AttachmentType, 16 | } from "./renderer/FrameBuffer"; 17 | 18 | export { Object3d } from "./Object3d"; 19 | 20 | export { Scene } from "./Scene"; 21 | 22 | export { DataType } from "./DataTypeEnum"; 23 | export * from "./Util"; 24 | 25 | export { Camera } from "./cameras/Camera"; 26 | export { PerspectiveCamera } from "./cameras/PerspectiveCamera"; 27 | export { OrthoCamera } from "./cameras/OrthoCamera"; 28 | 29 | export { Geometry } from "./geometries/Geometry"; 30 | export { CubeGeometry } from "./geometries/CubeGeometry"; 31 | export { RectGeometry } from "./geometries/RectGeometry"; 32 | export { SphereGeometry } from "./geometries/SphereGeometry"; 33 | export { TileGeometry } from "./geometries/TileGeometry"; 34 | 35 | export { Texture } from "./textures/Texture"; 36 | export { Texture2D } from "./textures/Texture2D"; 37 | export { CubeTexture } from "./textures/CubeTexture"; 38 | export { DataTexture } from "./textures/DataTexture"; 39 | 40 | export { IMaterial } from "./materials/Material"; 41 | export { StandardMaterial } from "./materials/surface/StandardMaterial"; 42 | export { BlinnPhongMaterial } from "./materials/surface/BlinnPhongMaterial"; 43 | export { SkyMaterial } from "./materials/SkyMaterial"; 44 | export { LinearDepthPackMaterial } from "./materials/ESM/DepthPackMaterial"; 45 | 46 | export { Light } from "./lights/Light"; 47 | export { PointLight } from "./lights/PointLight"; 48 | export { SpotLight } from "./lights/SpotLight"; 49 | export { DirectionalLight } from "./lights/DirectionalLight"; 50 | export { ShadowLevel } from "./lights/ShadowLevel"; 51 | 52 | export { OBJLoader } from "./loader/obj_mtl/OBJLoader"; 53 | 54 | export { Mesh } from "./Mesh"; 55 | export { Water } from "./extensions/Water"; 56 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Apr 26 2017 00:42:54 GMT+0800 (CST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath : '.', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks : [ 'jasmine', 'requirejs' ], 13 | 14 | // list of files / patterns to load in the browser 15 | files : [ 16 | {pattern : "build/tests/Util.js", included : true}, 17 | {pattern : "build/**/*.js", included : false}, 'test-main.js' 18 | ], 19 | 20 | // list of files to exclude 21 | exclude : [], 22 | 23 | // preprocess matching files before serving them to the browser 24 | // available preprocessors: 25 | // https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors : {}, 27 | 28 | // test results reporter to use 29 | // possible values: 'dots', 'progress' 30 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 31 | reporters : [ 'progress' ], 32 | 33 | // web server port 34 | port : 9876, 35 | 36 | // enable / disable colors in the output (reporters and logs) 37 | colors : true, 38 | 39 | // level of logging 40 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 41 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 42 | logLevel : config.LOG_INFO, 43 | 44 | // enable / disable watching file and executing tests whenever any file 45 | // changes 46 | autoWatch : false, 47 | 48 | // start these browsers 49 | // available browser launchers: 50 | // https://npmjs.org/browse/keyword/karma-launcher 51 | browsers : [ 'Chrome', 'Safari', 'Firefox', 'PhantomJS' ], 52 | 53 | // Continuous Integration mode 54 | // if true, Karma captures browsers, runs the tests and exits 55 | singleRun : true, 56 | 57 | // Concurrency level 58 | // how many browser should be started simultaneous 59 | concurrency : Infinity 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /examples/basic/pbs/index.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "CanvasToy"; 2 | import { vec3 } from "gl-matrix"; 3 | import { createCanvas, createSkyBox, onMouseOnStart } from "global"; 4 | 5 | const renderer = new CanvasToy.Renderer(createCanvas()); 6 | 7 | const scene = new CanvasToy.Scene(); 8 | const camera = new CanvasToy.PerspectiveCamera().setPosition(vec3.fromValues(0, 0, 20)); 9 | 10 | const skyTexture = new CanvasToy.CubeTexture( 11 | renderer.gl, 12 | { 13 | xpos: "resources/images/skybox/arid2_rt.jpg", 14 | xneg: "resources/images/skybox/arid2_lf.jpg", 15 | ypos: "resources/images/skybox/arid2_up.jpg", 16 | yneg: "resources/images/skybox/arid2_dn.jpg", 17 | zpos: "resources/images/skybox/arid2_bk.jpg", 18 | zneg: "resources/images/skybox/arid2_ft.jpg", 19 | }, 20 | ); 21 | 22 | scene.addObject(createSkyBox(renderer, skyTexture)); 23 | 24 | for (let i = 0; i < 5; ++i) { 25 | for (let j = 0; j < 5; ++j) { 26 | const mesh = new CanvasToy.Mesh( 27 | new CanvasToy.SphereGeometry(renderer.gl).setWidthSegments(50).setHeightSegments(50).build(), 28 | // new CanvasToy.CubeGeometry(renderer.gl), 29 | [new CanvasToy.StandardMaterial(renderer.gl) 30 | .setEnvironmentMap(skyTexture) 31 | .setRecieveShadow(false) 32 | .setCastShadow(false) 33 | .setMetallic((i + 0.5) / 5.0) 34 | .setRoughness((j + 0.5) / 5.0), 35 | // .setAlbedo(vec3.fromValues(1, 1, 0)), 36 | ], 37 | ); 38 | mesh.setPosition(vec3.fromValues((i - 2) * 3, (j - 2) * 3, 0)); 39 | scene.addObject(mesh); 40 | } 41 | } 42 | 43 | const light = new CanvasToy.DirectionalLight(renderer) 44 | .rotateY(Math.PI / 3) 45 | .setPosition(vec3.fromValues(5, 0, -5)) 46 | .setShadowLevel(CanvasToy.ShadowLevel.None) 47 | .lookAt(vec3.create()); 48 | 49 | scene.addOnUpdateListener(() => { 50 | light.rotateY(0.01); 51 | }); 52 | 53 | scene.addLight(light); 54 | renderer.render(scene, camera); 55 | renderer.stop(); 56 | onMouseOnStart(renderer); 57 | -------------------------------------------------------------------------------- /src/Util.ts: -------------------------------------------------------------------------------- 1 | import { vec2, vec3 } from "gl-matrix"; 2 | 3 | export function mixin(toObject: {}, fromObject: {}) { 4 | for (const property in fromObject) { 5 | if (toObject[property] instanceof Object) { 6 | mixin(toObject[property], fromObject[property]); 7 | } else { 8 | toObject[property] = fromObject[property]; 9 | } 10 | } 11 | } 12 | 13 | export function getDomScriptText(script: HTMLScriptElement): string { 14 | 15 | if (!script) { 16 | return null; 17 | } 18 | 19 | let theSource = ""; 20 | let currentChild = script.firstChild; 21 | 22 | while (currentChild) { 23 | if (currentChild.nodeType === 3) { 24 | theSource += currentChild.textContent; 25 | } 26 | 27 | currentChild = currentChild.nextSibling; 28 | } 29 | 30 | // Send the source to the shader object 31 | 32 | } 33 | 34 | function octWrap(inVec: vec2, outVec) { 35 | outVec[0] = (1.0 - Math.abs(inVec[1])) * (inVec[0] >= 0.0 ? 1.0 : -1.0); 36 | outVec[1] = (1.0 - Math.abs(inVec[0])) * (inVec[1] >= 0.0 ? 1.0 : -1.0); 37 | } 38 | 39 | export function encodeNormal(inVec: vec3, outVec: vec2 = vec2.create()) { 40 | inVec = vec3.scale(vec3.create(), inVec, Math.abs(outVec[0]) + Math.abs(outVec[1]) + Math.abs(outVec[2])); 41 | if (inVec[2] < 0.0) { 42 | const temp = vec2.fromValues(inVec[0], inVec[1]); 43 | octWrap(temp, outVec); 44 | outVec = temp; 45 | } 46 | vec2.scaleAndAdd(outVec, outVec, vec2.fromValues(0.5, 0.5), 0.5); 47 | return outVec; 48 | } 49 | 50 | export function decodeNormal(inVec: vec2, outVec: vec3 = vec3.create()) { 51 | inVec = vec2.scaleAndAdd(vec2.create(), inVec, vec2.fromValues(0.5, 0.5), 0.5); 52 | // https://twitter.com/Stubbesaurus/status/937994790553227264 53 | const n = vec3.fromValues(inVec[0], inVec[1], 1.0 - Math.abs(inVec[0]) - Math.abs(inVec[1])); 54 | const t = Math.max(0.0, Math.min(1.0, -n[2])); 55 | n[0] += n[0] >= 0.0 ? -t : t; 56 | n[1] += n[1] >= 0.0 ? -t : t; 57 | vec3.normalize(outVec, n); 58 | return outVec; 59 | } 60 | -------------------------------------------------------------------------------- /src/cameras/PerspectiveCamera.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { Camera } from "./Camera"; 3 | export class PerspectiveCamera extends Camera { 4 | protected _aspect: number = 1; 5 | protected _fovy: number = Math.PI / 4; 6 | constructor(parameter: { 7 | aspect?: number, 8 | fovy?: number, 9 | near?: number, 10 | far?: number, 11 | } = {}) { 12 | super(); 13 | this._aspect = parameter.aspect || this._aspect; 14 | this._fovy = parameter.fovy || this._fovy; 15 | this._near = parameter.near || this._near; 16 | this._far = parameter.far || this._far; 17 | } 18 | 19 | public compuseProjectionMatrix() { 20 | mat4.perspective( 21 | this._projectionMatrix, 22 | this._fovy, 23 | this._aspect, 24 | this._near, 25 | this._far, 26 | ); 27 | } 28 | 29 | public get aspect() { 30 | return this._aspect; 31 | } 32 | 33 | public get fovy() { 34 | return this._fovy; 35 | } 36 | 37 | public setAspect(aspect: number) { 38 | if (aspect !== this._aspect) { 39 | this._aspect = aspect; 40 | this.compuseProjectionMatrix(); 41 | } 42 | return this; 43 | } 44 | 45 | public setFovy(fovy: number) { 46 | if (fovy !== this._fovy) { 47 | this._fovy = fovy; 48 | this.compuseProjectionMatrix(); 49 | } 50 | return this; 51 | } 52 | 53 | public deCompuseProjectionMatrix() { 54 | // TODO: decompuse perspective camera 55 | } 56 | 57 | public setAspectRadio(ratio: number) { 58 | this._aspect = ratio; 59 | this.compuseProjectionMatrix(); 60 | return this; 61 | } 62 | 63 | public changeZoom(offset: number) { 64 | let fov = this._fovy / Math.PI * 180.0; 65 | fov -= offset; 66 | if (fov <= 1.0) { 67 | fov = 1.0; 68 | } 69 | if (fov >= 45.0) { 70 | fov = 45.0; 71 | } 72 | this.setFovy(fov * Math.PI / 180.0); 73 | return this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/SwapFramebuffer.ts: -------------------------------------------------------------------------------- 1 | import { FrameBuffer } from "./FrameBuffer"; 2 | export class ProcessingFrameBuffer { 3 | 4 | private _candidates: FrameBuffer[] = []; 5 | private _activeIndex: number = 0; 6 | private _gl; 7 | 8 | private _width; 9 | private _height; 10 | 11 | private _onInits: Array<(frameBuffer: FrameBuffer) => void> = []; 12 | 13 | constructor(gl: WebGLRenderingContext) { 14 | this._gl = gl; 15 | } 16 | 17 | public swap() { 18 | if (this._candidates.length === 1) { 19 | const fbo = new FrameBuffer(this._gl); 20 | fbo.setWidth(this._width).setHeight(this._height); 21 | this._onInits.forEach((inits) => { inits(fbo); }); 22 | this._candidates.push(fbo); 23 | } 24 | this._activeIndex = 1 - this._activeIndex; 25 | } 26 | 27 | public get active() { 28 | if (this._candidates.length === 0) { 29 | const fbo = new FrameBuffer(this._gl); 30 | fbo.setWidth(this._width).setHeight(this._height); 31 | this._onInits.forEach((inits) => { inits(fbo); }); 32 | fbo.attach(this._gl); 33 | this._candidates.push(fbo); 34 | } 35 | return this._candidates[this._activeIndex]; 36 | } 37 | 38 | public onInit(callback: (frameBuffer: FrameBuffer) => void) { 39 | this._onInits.push(callback); 40 | return this; 41 | } 42 | 43 | public setWidth(_width: number) { 44 | this._width = _width; 45 | for (const fbo of this._candidates) { 46 | fbo.setWidth(_width); 47 | } 48 | return this; 49 | } 50 | 51 | public setHeight(_height: number) { 52 | this._height = _height; 53 | for (const fbo of this._candidates) { 54 | fbo.setHeight(_height); 55 | } 56 | return this; 57 | } 58 | 59 | public attach(gl: WebGLRenderingContext, drawBuffer?: WebGLDrawBuffers) { 60 | for (const fbo of this._candidates) { 61 | fbo.attach(gl, drawBuffer); 62 | } 63 | } 64 | 65 | public get width() { 66 | return this._width; 67 | } 68 | 69 | public get height() { 70 | return this._height; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/forward/ForwardProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "../../cameras/Camera"; 2 | import { IMaterial } from "../../materials/Material"; 3 | import { Mesh } from "../../Mesh"; 4 | import { Object3d } from "../../Object3d"; 5 | import { Scene } from "../../Scene"; 6 | import { Program } from "../../shader/Program"; 7 | import { WebGLExtension } from "../IExtension"; 8 | import { IProcessor } from "../IProcessor"; 9 | 10 | export class ForwardProcessor implements IProcessor { 11 | public readonly gl: WebGLRenderingContext; 12 | 13 | public readonly ext: WebGLExtension; 14 | 15 | constructor( 16 | gl: WebGLRenderingContext, 17 | ext: WebGLExtension, 18 | scene: Scene, 19 | camera: Camera, 20 | ) { 21 | this.gl = gl; 22 | this.ext = ext; 23 | } 24 | 25 | public process(scene: Scene, camera: Camera, materials: IMaterial[]) { 26 | this.gl.clearColor( 27 | scene.clearColor[0], 28 | scene.clearColor[1], 29 | scene.clearColor[2], 30 | scene.clearColor[3], 31 | ); 32 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT | this.gl.COLOR_BUFFER_BIT); 33 | for (const object of scene.objects) { 34 | this.renderObject(scene, camera, object); 35 | } 36 | } 37 | 38 | private renderObject(scene: Scene, camera: Camera, object: Object3d) { 39 | if (object instanceof Mesh) { 40 | const mesh = object as Mesh; 41 | mesh.geometry.resetLightShadows(this.gl); 42 | for (const material of mesh.materials) { 43 | const shader = material.shader; 44 | if (shader.enableDepthTest) { 45 | this.gl.enable(this.gl.DEPTH_TEST); 46 | } else { 47 | this.gl.disable(this.gl.DEPTH_TEST); 48 | } 49 | if (shader.enableStencilTest) { 50 | this.gl.enable(this.gl.STENCIL_TEST); 51 | } else { 52 | this.gl.disable(this.gl.STENCIL_TEST); 53 | } 54 | this.gl.useProgram(shader.webGlProgram); 55 | shader.pass({ mesh, camera, material, scene }); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/materials/surface/BlinnPhongMaterial.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { DataType } from "../../DataTypeEnum"; 4 | import { 5 | define, 6 | readyRequire, 7 | structure, 8 | texture, 9 | uniform, 10 | } from "../../Decorators"; 11 | 12 | import { Program, shaderPassLib } from "../../shader/Program"; 13 | import { ShaderBuilder } from "../../shader/ShaderBuilder"; 14 | import { ShaderSource } from "../../shader/shaders"; 15 | import { CubeTexture } from "../../textures/CubeTexture"; 16 | import { Texture } from "../../textures/Texture"; 17 | import { ISurfaceMaterial } from "./ISurfaceMaterial"; 18 | 19 | @structure("uMaterial") 20 | export class BlinnPhongMaterial extends ISurfaceMaterial { 21 | protected _diffuse: vec3 = vec3.fromValues(0.8, 0.8, 0.8); 22 | 23 | protected _specular: vec3 = vec3.fromValues(0.3, 0.3, 0.3); 24 | 25 | protected _specularExponent: number = 64; 26 | 27 | // @texture("specularTexture") 28 | protected _specularMap: Texture; 29 | 30 | @uniform(DataType.vec3) 31 | public get diffuse() {return this._diffuse; } 32 | 33 | @uniform(DataType.vec3) 34 | public get specular() {return this._specular; } 35 | 36 | @uniform(DataType.float) 37 | public get specularExponent() { 38 | return this._specularExponent; 39 | } 40 | 41 | public setDiffuse(_diffuse: vec3) { 42 | this._diffuse = _diffuse; 43 | return this; 44 | } 45 | 46 | public setSpecular(_specular: vec3) { 47 | this._specular = _specular; 48 | return this; 49 | } 50 | 51 | public setSpecularExponent(_specularExponent: number) { 52 | this._specularExponent = _specularExponent; 53 | return this; 54 | } 55 | 56 | protected initShader(gl: WebGLRenderingContext) { 57 | return new ShaderBuilder() 58 | .addDefinition(ShaderSource.definitions__material_blinnphong_glsl) 59 | .setLightModel(ShaderSource.light_model__blinn_phong_glsl) 60 | .setExtraRenderParamHolder("mvp", { 61 | uniforms: { 62 | modelViewProjectionMatrix: 63 | shaderPassLib.uniforms.modelViewProjectionMatrix, 64 | }, 65 | }) 66 | .setExtraRenderParamHolder("pcss", { 67 | defines: shaderPassLib.defines, 68 | }) 69 | .build(gl); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/types.glsl: -------------------------------------------------------------------------------- 1 | vec3 calculateDirLight( 2 | DirectLight light, 3 | Material material, 4 | vec3 position, 5 | vec3 normal, 6 | vec3 eyePos 7 | ) { 8 | return calculateLight( 9 | material, 10 | normalize(eyePos - position), 11 | normal, 12 | -light.direction, 13 | light.color, 14 | light.idensity 15 | ); 16 | } 17 | 18 | vec3 calculatePointLight( 19 | PointLight light, 20 | Material material, 21 | vec3 position, 22 | vec3 normal, 23 | vec3 eyePos 24 | ) { 25 | float lightDis = length(light.position - position); 26 | lightDis /= light.radius; 27 | float atten_min = 1.0 / (light.constantAtten + light.linearAtten + light.squareAtten); 28 | float atten_max = 1.0 / light.constantAtten; 29 | float atten = 1.0 / (light.constantAtten + light.linearAtten * lightDis + light.squareAtten * lightDis * lightDis); 30 | float idensity = light.idensity * (atten - atten_min) / (atten_max - atten_min); 31 | //idensity *= step(lightDis, 1.0); 32 | return calculateLight( 33 | material, 34 | normalize(eyePos - position), 35 | normal, 36 | normalize(light.position - position), 37 | light.color, 38 | idensity 39 | ); 40 | } 41 | 42 | vec3 calculateSpotLight( 43 | SpotLight light, 44 | Material material, 45 | vec3 position, 46 | vec3 normal, 47 | vec3 eyePos 48 | ) { 49 | vec3 lightDir = normalize(light.position - position); 50 | float spotFactor = dot(-lightDir, light.spotDir); 51 | if (spotFactor < light.coneAngleCos) { 52 | return vec3(0.0); 53 | } 54 | float lightDis = length(light.position - position); 55 | lightDis /= light.radius; 56 | float atten_min = 1.0 / (light.constantAtten + light.linearAtten + light.squareAtten); 57 | float atten_max = 1.0 / light.constantAtten; 58 | float atten = 1.0 / (light.constantAtten + light.linearAtten * lightDis + light.squareAtten * lightDis * lightDis); 59 | float idensity = light.idensity * (atten - atten_min) / (atten_max - atten_min); 60 | 61 | idensity *= (spotFactor - light.coneAngleCos) / (1.0 - light.coneAngleCos); 62 | // idensity *= step(light.radius, lightDis); 63 | return calculateLight( 64 | material, 65 | normalize(eyePos - position), 66 | normal, 67 | lightDir, 68 | light.color, 69 | idensity 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /scripts/glslLoader.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let os = require('os') 3 | 4 | let walkSync = function(dir, filelist) { 5 | files = fs.readdirSync(dir); 6 | filelist = filelist || []; 7 | files.forEach(function(file) { 8 | if (fs.statSync(dir + '/' + file).isDirectory()) { 9 | filelist = walkSync(dir + '/' + file + '/', filelist); 10 | } else { 11 | filelist.push(dir + '/' + file); 12 | } 13 | }); 14 | return filelist; 15 | }; 16 | 17 | function exportShaderSource(results, sources, stringEnum) { 18 | results.push(`export type ${stringEnum} = `); 19 | for (let i in sources) { 20 | let line = ' typeof ShaderSource.' + sources[i]; 21 | if (i == sources.length - 1) { 22 | line += ';'; 23 | } else { 24 | line += ' |' 25 | } 26 | results.push(line); 27 | } 28 | } 29 | 30 | function convertGLSL() { 31 | let srcs = walkSync('src/shader/sources'); 32 | let libs = []; 33 | let shadingFrag = []; 34 | let shadingVert = []; 35 | let results = [ 36 | '// Warning: this file is automaticaly generated by util/glslLoader.js and executed by nodejs', 37 | '/* tslint:disable:variable-name max-line-length no-trailing-whitespace*/', 38 | 'export namespace ShaderSource {', 39 | ]; 40 | for (let i in srcs) { 41 | let varName = srcs[i].replace(/[\/|\.]/g, '_'); 42 | varName = varName.replace('src_shader_sources_', ''); 43 | if (varName.match(/glsl$/)) { 44 | libs.push(varName); 45 | } else if (varName.match(/frag$/)) { 46 | shadingFrag.push(varName); 47 | } else if (varName.match(/vert$/)) { 48 | shadingVert.push(varName); 49 | } else { 50 | continue; 51 | } 52 | let content = fs.readFileSync(srcs[i], 'utf-8'); 53 | content = content.split('\n'); 54 | content = content.join(os.EOL); 55 | let out = ' export const ' + varName + ' = `' + os.EOL + content + '`;'; 56 | results.push(out); 57 | } 58 | results.push('}'); 59 | 60 | // export shader libs as string enums; 61 | exportShaderSource(results, libs, "ShaderLib") 62 | 63 | // export shading methods as string enums; 64 | exportShaderSource(results, shadingVert, "ShadingVert") 65 | 66 | // export shading methods as string enums; 67 | exportShaderSource(results, shadingFrag, "ShadingFrag") 68 | 69 | let resultStr = results.join(os.EOL); 70 | resultStr += os.EOL; 71 | fs.writeFileSync('src/shader/shaders.ts', resultStr); 72 | } 73 | 74 | exports.convertGLSL = convertGLSL; -------------------------------------------------------------------------------- /examples/global.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "cameras/Camera"; 2 | import * as CanvasToy from "CanvasToy"; 3 | import { vec2 } from "gl-matrix"; 4 | 5 | export const examples = []; 6 | export function createSkyBox(renderer: CanvasToy.Renderer, cubeTexture) { 7 | return new CanvasToy.Mesh( 8 | new CanvasToy.CubeGeometry(renderer.gl), 9 | [new CanvasToy.SkyMaterial(renderer.gl, cubeTexture)], 10 | ); 11 | } 12 | 13 | export function onMouseOnStart(renderer: CanvasToy.Renderer) { 14 | renderer.canvas.onmouseover = () => { 15 | renderer.start(); 16 | }; 17 | renderer.canvas.onmouseleave = () => { 18 | renderer.stop(); 19 | }; 20 | } 21 | 22 | export function onMouseEvent(renderer: CanvasToy.Renderer, camera: Camera) { 23 | const canvas = renderer.canvas; 24 | canvas.onmousedown = (ev: MouseEvent) => { 25 | if (ev.button === 2) { 26 | camera.controlEnable = true; 27 | canvas.oncontextmenu = (ev) => { 28 | ev.preventDefault(); 29 | }; 30 | canvas.style.cursor = "none"; 31 | canvas.requestPointerLock(); 32 | } 33 | }; 34 | canvas.onmouseup = (ev: MouseEvent) => { 35 | if (ev.button === 2) { 36 | camera.controlEnable = false; 37 | canvas.style.cursor = "auto"; 38 | canvas.ownerDocument.exitPointerLock(); 39 | } 40 | }; 41 | canvas.onmousemove = (ev: MouseEvent) => { 42 | const mouseSensitivity = 0.05; 43 | if (camera.controlEnable === true) { 44 | const deltaAngle = vec2.fromValues( 45 | ev.movementX * mouseSensitivity, 46 | -ev.movementY * mouseSensitivity, 47 | ); 48 | camera.changeDirectionByAngle(deltaAngle); 49 | } 50 | }; 51 | canvas.onwheel = (ev: WheelEvent) => { 52 | const zoomSensitivity = 0.01; 53 | if (camera.controlEnable === true) { 54 | ev.preventDefault(); 55 | camera.changeZoom(ev.deltaY * zoomSensitivity); 56 | } 57 | }; 58 | } 59 | 60 | export function createCanvas() { 61 | const table = document.getElementById("table"); 62 | const newCanvas = document.createElement("canvas"); 63 | newCanvas.width = 960; 64 | newCanvas.height = 540; 65 | newCanvas.style.background = "black"; 66 | table.appendChild(newCanvas); 67 | return newCanvas; 68 | } 69 | -------------------------------------------------------------------------------- /examples/basic/lightesAndGeometries/index.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "CanvasToy"; 2 | import { vec3 } from "gl-matrix"; 3 | import { createCanvas, onMouseOnStart } from "global"; 4 | 5 | const renderer = new CanvasToy.Renderer(createCanvas()); 6 | 7 | const scene = new CanvasToy.Scene(); 8 | const center = new CanvasToy.Object3d(); 9 | const camera = new CanvasToy.PerspectiveCamera() 10 | .setParent(center) 11 | .translate(vec3.fromValues(0, 5, 5)) 12 | .rotateX(-Math.PI / 4); 13 | const checkerBoard = new CanvasToy.StandardMaterial(renderer.gl).setDebugMode(true); 14 | const objectMaterial = new CanvasToy.StandardMaterial(renderer.gl).setMetallic(0) 15 | .setMainTexture(new CanvasToy.Texture2D(renderer.gl, "resources/images/wood.jpg")); 16 | // objectMaterial.castShadow = false; 17 | const ground = new CanvasToy.Mesh(new CanvasToy.TileGeometry(renderer.gl).build(), [checkerBoard]) 18 | .setPosition(vec3.fromValues(0, -2, 0)).rotateX(-Math.PI / 2).setScaling(vec3.fromValues(10, 10, 10)); 19 | 20 | const box = new CanvasToy.Mesh(new CanvasToy.CubeGeometry(renderer.gl).build(), [objectMaterial]) 21 | .setPosition(vec3.fromValues(-2, -1, 0)).setScaling(vec3.fromValues(0.5, 0.5, 0.5)); 22 | const sphere = new CanvasToy.Mesh( 23 | new CanvasToy.SphereGeometry(renderer.gl) 24 | .setWidthSegments(50) 25 | .setHeightSegments(50) 26 | .build(), 27 | [objectMaterial]) 28 | .setPosition(vec3.fromValues(2, 0, 0)).setScaling(vec3.fromValues(0.5, 0.5, 0.5)); 29 | const directLight = new CanvasToy.DirectionalLight(renderer) 30 | .setIdensity(1) 31 | .translate(vec3.fromValues(0, 5, 5)) 32 | .lookAt(vec3.create()); 33 | // .setConeAngle(Math.PI / 3); 34 | const spotLight = new CanvasToy.SpotLight(renderer) 35 | .setIdensity(2) 36 | .translate(vec3.fromValues(0, 10, -10)) 37 | .lookAt(vec3.create()); 38 | 39 | const pointLight = new CanvasToy.PointLight(renderer) 40 | .translate(vec3.fromValues(0, 3, 0)) 41 | .setRadius(30) 42 | .setIdensity(1); 43 | 44 | scene.addLight(pointLight); 45 | scene.addObject(ground, box, sphere, center, camera); 46 | let time = 0; 47 | // spotLight.translate(vec3.fromValues(0, 0, 0.01)); 48 | 49 | scene.addOnUpdateListener((delta) => { 50 | time += delta; 51 | box.translate(vec3.fromValues(0, 0.04 * Math.sin(time / 400), 0)); 52 | sphere.translate(vec3.fromValues(0, -0.04 * Math.sin(time / 400), 0)); 53 | center.rotateY(0.01); 54 | }); 55 | scene.ambientLight = vec3.fromValues(0.2, 0.2, 0.2); 56 | renderer.render(scene, camera); 57 | renderer.stop(); 58 | onMouseOnStart(renderer); 59 | -------------------------------------------------------------------------------- /src/shader/sources/light_model/pbs_ggx.glsl: -------------------------------------------------------------------------------- 1 | float tangent_2(float cos_2) { 2 | return (1. - cos_2) / cos_2; 3 | } 4 | 5 | float Smith_G1(float NdotV, float roughness) { 6 | float tan_2 = tangent_2(NdotV * NdotV); 7 | float root = roughness * roughness * tan_2; 8 | return 2.0 / (1. + sqrt(1. + root)); 9 | } 10 | 11 | float GGX_D(float HdotN, float roughness) { 12 | float cos_2 = HdotN * HdotN; 13 | float tan_2 = tangent_2(cos_2); 14 | 15 | float root = roughness / (cos_2 * (roughness * roughness + tan_2)); 16 | return root * root / acos(-1.); 17 | } 18 | 19 | vec3 calculateLight( 20 | Material material, 21 | vec3 viewDir, 22 | vec3 normal, 23 | vec3 lightDir, 24 | vec3 lightColor, 25 | float idensity 26 | ) { 27 | 28 | vec3 halfVec = normalize(lightDir + viewDir); 29 | 30 | float LdotN = dot(lightDir, normal); 31 | float VdotN = dot(viewDir, normal); 32 | float HdotN = dot(halfVec, normal); 33 | float LdotH = dot(lightDir, halfVec); 34 | float VdotH = dot(viewDir, halfVec); 35 | 36 | if (VdotN < 0. || LdotN < 0.) { 37 | return vec3(0.); 38 | } 39 | 40 | float OneMinusLdotH = 1. - LdotH; 41 | float OneMinusLdotHSqr = OneMinusLdotH * OneMinusLdotH; 42 | 43 | vec3 albedo = material.albedo * lightColor; 44 | 45 | vec3 fresnel = albedo + (1. - albedo) * OneMinusLdotHSqr * OneMinusLdotHSqr * OneMinusLdotH; 46 | 47 | float d = GGX_D(HdotN, material.roughness); 48 | float g = Smith_G1(VdotN, material.roughness) * Smith_G1(LdotN, material.roughness); 49 | vec3 specbrdf = fresnel * (g * d / (4. * VdotN * LdotN)); 50 | 51 | float OneMinusLdotN = 1. - LdotN; 52 | float OneMinusLdotNSqr = OneMinusLdotN * OneMinusLdotN; 53 | 54 | float OneMinusVdotN = 1. - VdotN; 55 | float OneMinusVdotNSqr = OneMinusVdotN * OneMinusVdotN; 56 | 57 | float fd90 = 0.5 + 2.0 * material.roughness * (LdotH * LdotH); 58 | vec3 diffbrdf = albedo * (1.0 + (fd90 - 1.0) * OneMinusLdotN * OneMinusLdotNSqr * OneMinusLdotNSqr) * 59 | (1.0 + (fd90 - 1.0) * OneMinusVdotN * OneMinusVdotNSqr * OneMinusVdotNSqr); 60 | 61 | 62 | vec3 color = (material.metallic * 0.96 + 0.04) * specbrdf + ((1. - material.metallic) * 0.96) * diffbrdf; 63 | return color * LdotN * idensity; 64 | } 65 | 66 | vec3 calculateImageBasedLight( 67 | Material material, 68 | vec3 lightDir, 69 | vec3 normal, 70 | vec3 viewDir, 71 | vec3 specularColor, 72 | vec3 diffuseColor 73 | ) { 74 | // specularColor = mix(material.albedo, specularColor, material.metallic * 0.5 + 0.5); 75 | vec3 color = mix(specularColor, diffuseColor, material.roughness); 76 | return color * material.albedo; 77 | } 78 | -------------------------------------------------------------------------------- /src/textures/CubeTexture.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from "./Texture"; 2 | 3 | export class CubeTexture extends Texture { 4 | public images: HTMLImageElement[] = []; 5 | 6 | private _wrapR: number; 7 | 8 | constructor( 9 | gl: WebGLRenderingContext, 10 | urls?: { 11 | xpos: string; 12 | xneg: string; 13 | ypos: string; 14 | yneg: string; 15 | zpos: string; 16 | zneg: string; 17 | }, 18 | ) { 19 | super(gl); 20 | this.setTarget(gl.TEXTURE_CUBE_MAP); 21 | if (!!urls) { 22 | this.images = [0, 0, 0, 0, 0, 0].map(() => new Image()); 23 | this.images[0].src = urls.xpos; 24 | this.images[1].src = urls.xneg; 25 | this.images[2].src = urls.ypos; 26 | this.images[3].src = urls.yneg; 27 | this.images[4].src = urls.zpos; 28 | this.images[5].src = urls.zneg; 29 | this.setAsyncFinished( 30 | Promise.all( 31 | this.images.map((image) => { 32 | return this.createLoadPromise(image); 33 | }), 34 | ).then(() => { 35 | return Promise.resolve(this); 36 | }), 37 | ); 38 | } 39 | } 40 | 41 | public get wrapR() { 42 | return this._wrapR; 43 | } 44 | 45 | public setWrapR(_wrapR: number) { 46 | this._wrapR = _wrapR; 47 | return this; 48 | } 49 | 50 | public apply(gl: WebGLRenderingContext) { 51 | super.apply(gl); 52 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0); 53 | for (let i = 0; i < this.images.length; ++i) { 54 | gl.texImage2D( 55 | gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 56 | 0, 57 | this.format, 58 | this.format, 59 | this.type, 60 | this.images[i], 61 | ); 62 | } 63 | return this; 64 | } 65 | 66 | public applyForRendering(gl: WebGLRenderingContext, width, height) { 67 | super.apply(gl); 68 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0); 69 | for (let i = 0; i < 6; ++i) { 70 | gl.texImage2D( 71 | gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 72 | 0, 73 | this.format, 74 | width, 75 | height, 76 | 0, 77 | this.format, 78 | this.type, 79 | null, 80 | ); 81 | } 82 | return this; 83 | } 84 | 85 | private createLoadPromise(image: HTMLImageElement) { 86 | return new Promise((resolve, reject) => { 87 | if (!image) { 88 | resolve(this); 89 | } else { 90 | image.onload = () => resolve(this); 91 | image.onerror = () => reject(this); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/deferredRendering/index.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "CanvasToy"; 2 | import { vec3 } from "gl-matrix"; 3 | import { createCanvas, onMouseOnStart } from "global"; 4 | 5 | const renderer = new CanvasToy.Renderer(createCanvas()); 6 | const scene = new CanvasToy.Scene(); 7 | const camera = new CanvasToy.PerspectiveCamera() 8 | .setPosition(vec3.fromValues(0, 100, 100)) 9 | .lookAt(vec3.fromValues(0, 0, -40)); 10 | const tile = new CanvasToy.Mesh(new CanvasToy.RectGeometry(renderer.gl), [ 11 | new CanvasToy.StandardMaterial(renderer.gl).setMainTexture( 12 | new CanvasToy.Texture2D(renderer.gl, "resources/images/wood.jpg"), 13 | ), 14 | ]) 15 | .translate(vec3.fromValues(0, -10, -40)) 16 | .rotateX(-Math.PI / 2) 17 | .setScaling(vec3.fromValues(200, 200, 200)); 18 | scene.addObject(camera, tile); 19 | const teapotProto = CanvasToy.OBJLoader.load( 20 | renderer.gl, 21 | "resources/models/teapot/teapot.obj", 22 | ); 23 | 24 | teapotProto.setAsyncFinished( 25 | teapotProto.asyncFinished().then(() => { 26 | const material = (teapotProto.children[0] as CanvasToy.Mesh) 27 | .materials[0] as CanvasToy.StandardMaterial; 28 | material.setAlbedo(vec3.fromValues(1, 0.8, 0.2)); 29 | material.setCastShadow(false); 30 | for (let i = 0; i < 40; ++i) { 31 | const teapot = new CanvasToy.Mesh( 32 | (teapotProto.children[0] as CanvasToy.Mesh).geometry, 33 | (teapotProto.children[0] as CanvasToy.Mesh).materials, 34 | ); 35 | scene.addObject(teapot); 36 | teapot.translate( 37 | vec3.fromValues((i % 10) * 40 - 200, 0, -40 - Math.floor(i / 10) * 40), 38 | ); 39 | let time = 0; 40 | const spin = 0.03 * (Math.random() - 0.5); 41 | const light = new CanvasToy.PointLight(renderer) 42 | .setPosition( 43 | vec3.fromValues( 44 | Math.random() * 200.0 - 50, 45 | 4, 46 | Math.random() * 200.0 - 150, 47 | ), 48 | ) 49 | .setColor(vec3.fromValues(Math.random(), Math.random(), Math.random())) 50 | .setRadius(50) 51 | .setShadowLevel(CanvasToy.ShadowLevel.None); 52 | scene.addLight(light); 53 | const vx = Math.random() * 3.0; 54 | const vy = Math.random() * 3.0; 55 | scene.addOnUpdateListener(() => { 56 | time += 1 / 60; 57 | teapot.rotateY(spin); 58 | light.translate( 59 | vec3.fromValues(-Math.sin(time * vx), 0, -Math.cos(time * vy)), 60 | ); 61 | }); 62 | } 63 | renderer.forceDeferred(); 64 | renderer.render(scene, camera); 65 | return Promise.resolve(teapotProto); 66 | }), 67 | ); 68 | renderer.stop(); 69 | onMouseOnStart(renderer); 70 | -------------------------------------------------------------------------------- /examples/basic/renderToTexture/depth.ts: -------------------------------------------------------------------------------- 1 | // 2 | // examples.push((canvas: HTMLCanvasElement) => { 3 | // const renderer = new CanvasToy.Renderer(canvas); 4 | // renderer.gl.enable(renderer.gl.CULL_FACE); 5 | // const scenes = Array(2, 0).map(() => new CanvasToy.Scene()); 6 | // const cameras = Array(2, 0).map(() => new CanvasToy.PerspectiveCamera()); 7 | // const light = new CanvasToy.PointLight(); 8 | // const cube = new CanvasToy.Mesh(new CanvasToy.CubeGeometry(renderer.gl), 9 | // [new CanvasToy.StandardMaterial(renderer.gl)]).rotateY(2).translate(vec3.fromValues(0, 0, -3)); 10 | // 11 | // const image = new Image(); 12 | // cube.materials[0].mainTexture = new CanvasToy.Texture2D(renderer.gl, "resources/images/chrome.png") 13 | // .setFormat(renderer.gl.RGBA); 14 | // 15 | // scenes[0].ambientLight = vec3.fromValues(0.1, 0.1, 0.1); 16 | // scenes[1].ambientLight = vec3.fromValues(0.1, 0.1, 0.1); 17 | // light.setPosition(vec3.fromValues(100, 0, 100)); 18 | // scenes[0].addLight(light).addObject(cameras[0]).addObject(cube); 19 | // 20 | // const fbo = renderer.createFrameBuffer(); 21 | // fbo.attachments.color.disable(); 22 | // fbo.attachments.depth 23 | // .setType(renderer.gl, CanvasToy.AttachmentType.Texture) 24 | // .targetTexture 25 | // .setType(renderer.gl.UNSIGNED_SHORT) 26 | // .setFormat(renderer.gl.DEPTH_COMPONENT); 27 | // 28 | // const rttTexture = fbo.attachments.depth.targetTexture; 29 | // 30 | // const depthMaterial = new CanvasToy.StandardMaterial(renderer.gl, { mainTexture: rttTexture }); 31 | // depthMaterial.program = new CanvasToy.StandardShaderBuilder() 32 | // .setInterplotationMethod(CanvasToy.InterplotationMethod.DepthPhong) 33 | // .build(renderer.gl); 34 | // 35 | // const rect = new CanvasToy.Mesh( 36 | // new CanvasToy.RectGeometry(renderer.gl), 37 | // [depthMaterial]).setScaling(vec3.fromValues(canvas.width / canvas.height, 1, 1)); 38 | // cube.registUpdate(() => { 39 | // cube.rotateY(0.02); 40 | // }); 41 | // cameras[0].setNear(1); 42 | // cameras[1].setPosition(vec3.fromValues(0, 0, 2)); 43 | // scenes[1].addLight(light).addObject(cameras[1]).addObject(rect); 44 | // scenes[0].addLight(light); 45 | // renderer.renderFBO(scenes[0], cameras[0]); 46 | // renderer.render(scenes[1], cameras[1]); 47 | // 48 | // depthMaterial.program.addUniform("cameraNear", { 49 | // type: CanvasToy.DataType.float, 50 | // updator: (obj, camera: CanvasToy.PerspectiveCamera) => camera.near, 51 | // }); 52 | // depthMaterial.program.addUniform("cameraFar", { 53 | // type: CanvasToy.DataType.float, 54 | // updator: (obj, camera: CanvasToy.PerspectiveCamera) => camera.far, 55 | // }); 56 | // return renderer; 57 | // }); 58 | -------------------------------------------------------------------------------- /src/shader/ShaderBuilder.ts: -------------------------------------------------------------------------------- 1 | import { IRenderParamHolder, Program } from "./Program"; 2 | import { ShaderLib, ShaderSource, ShadingFrag, ShadingVert } from "./shaders"; 3 | 4 | export class ShaderBuilder { 5 | 6 | private definitions: ShaderLib[] = [ 7 | ShaderSource.definitions__light_glsl, 8 | ]; 9 | 10 | private vertLibs: ShaderLib[] = []; 11 | 12 | private fragLibs: ShaderLib[] = [ 13 | ShaderSource.calculators__linearlize_depth_glsl, 14 | ShaderSource.calculators__types_glsl, 15 | ShaderSource.calculators__unpackFloat1x32_glsl, 16 | ShaderSource.calculators__shadow_factor_glsl, 17 | ShaderSource.debug__checkBox_glsl, 18 | ]; 19 | 20 | private lightModel: ShaderLib = ShaderSource.light_model__pbs_ggx_glsl; 21 | 22 | private shadingVert: ShadingVert = ShaderSource.interploters__forward__phong_vert; 23 | 24 | private shadingFrag: ShadingFrag = ShaderSource.interploters__forward__phong_frag; 25 | 26 | private extraRenderParamHolders: { [index: string]: IRenderParamHolder } = {}; 27 | 28 | public resetShaderLib() { 29 | this.lightModel = undefined; 30 | this.definitions = []; 31 | this.vertLibs = []; 32 | this.fragLibs = []; 33 | return this; 34 | } 35 | 36 | public addShaderLib(...lib: ShaderLib[]) { 37 | this.vertLibs.push(...lib); 38 | this.fragLibs.push(...lib); 39 | return this; 40 | } 41 | 42 | public addDefinition(...lib: ShaderLib[]) { 43 | this.definitions.push(...lib); 44 | return this; 45 | } 46 | 47 | public addShaderLibVert(...lib: ShaderLib[]) { 48 | this.vertLibs.push(...lib); 49 | return this; 50 | } 51 | 52 | public addShaderLibFrag(...lib: ShaderLib[]) { 53 | this.fragLibs.push(...lib); 54 | return this; 55 | } 56 | 57 | public setLightModel(model: ShaderLib) { 58 | this.lightModel = model; 59 | return this; 60 | } 61 | 62 | public setShadingVert(vert: ShadingVert) { 63 | this.shadingVert = vert; 64 | return this; 65 | } 66 | 67 | public setShadingFrag(frag: ShadingFrag) { 68 | this.shadingFrag = frag; 69 | return this; 70 | } 71 | 72 | public setExtraRenderParamHolder(name: string, paramHolder: IRenderParamHolder) { 73 | this.extraRenderParamHolders[name] = paramHolder; 74 | return this; 75 | } 76 | 77 | public build(gl: WebGLRenderingContext): Program { 78 | return new Program(gl, { 79 | vertexShader: this.definitions.join("\n") + "\n" + this.vertLibs.join("\n") + this.shadingVert, 80 | fragmentShader: this.definitions.join("\n") + "\n" + (this.lightModel ? this.lightModel + "\n" : "") 81 | + this.fragLibs.join("\n") + this.shadingFrag, 82 | }, 83 | this.extraRenderParamHolders, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/cameras/OrthoCamera.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | import { Camera } from "./Camera"; 3 | 4 | export class OrthoCamera extends Camera { 5 | 6 | protected _left: number = -20; 7 | protected _right: number = 20; 8 | protected _bottom: number = -20; 9 | protected _top: number = 20; 10 | protected _baseSize: number = 20; 11 | 12 | protected _ratio: number; 13 | 14 | constructor(parameters: { 15 | left?: number, 16 | right?: number, 17 | bottom?: number, 18 | top?: number, 19 | near?: number, 20 | far?: number, 21 | } = {}) { 22 | super(); 23 | this._left = parameters.left || this._left; 24 | this._right = parameters.right || this._right; 25 | this._bottom = parameters.bottom || this._bottom; 26 | this._top = parameters.top || this._top; 27 | this._near = parameters.near || this._near; 28 | this._far = parameters.far || this._far; 29 | mat4.ortho( 30 | this._projectionMatrix, 31 | this._left, 32 | this._right, 33 | this._bottom, 34 | this._top, 35 | this._near, 36 | this._far, 37 | ); 38 | } 39 | 40 | public setLeft(left: number) { 41 | if (left !== this._left) { 42 | this._left = left; 43 | this.compuseProjectionMatrix(); 44 | } 45 | } 46 | 47 | public get left() { 48 | return this._left; 49 | } 50 | 51 | public get right() { 52 | return this._right; 53 | } 54 | 55 | public get top() { 56 | return this._top; 57 | } 58 | 59 | public get bottom() { 60 | return this._bottom; 61 | } 62 | 63 | public compuseProjectionMatrix() { 64 | mat4.ortho( 65 | this._projectionMatrix, 66 | this._left, 67 | this._right, 68 | this._bottom, 69 | this._top, 70 | this._near, 71 | this._far, 72 | ); 73 | } 74 | 75 | public deCompuseProjectionMatrix() { 76 | // TODO: de compute ortho camera 77 | } 78 | 79 | public setAspectRadio(radio: number) { 80 | this._ratio = radio; 81 | this._left = -radio * this._baseSize; 82 | this._right = radio * this._baseSize; 83 | this._top = this._baseSize; 84 | this._bottom = -this._baseSize; 85 | this.compuseProjectionMatrix(); 86 | return this; 87 | } 88 | 89 | public changeZoom(offset: number) { 90 | let zoom = this._baseSize + offset; 91 | if (zoom >= 30.0) { 92 | zoom = 30.0; 93 | } 94 | if (zoom <= 5.0) { 95 | zoom = 5.0; 96 | } 97 | this._baseSize = zoom; 98 | this.setAspectRadio(this._ratio); 99 | return this; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/deferred/tiledLightPoint.frag: -------------------------------------------------------------------------------- 1 | #define MAX_TILE_LIGHT_NUM 32 2 | 3 | precision highp float; 4 | 5 | uniform float uHorizontalTileNum; 6 | uniform float uVerticalTileNum; 7 | uniform float uLightListLengthSqrt; 8 | 9 | uniform mat4 inverseProjection; 10 | 11 | uniform sampler2D uLightIndex; 12 | uniform sampler2D uLightOffsetCount; 13 | uniform sampler2D uLightPositionRadius; 14 | uniform sampler2D uLightColorIdensity; 15 | 16 | uniform sampler2D normalRoughnessTex; 17 | uniform sampler2D albedoMetallicTex; 18 | uniform sampler2D depthTex; 19 | 20 | uniform float cameraNear; 21 | uniform float cameraFar; 22 | 23 | 24 | varying vec3 vPosition; 25 | 26 | vec3 decodeNormal(vec2 n) 27 | { 28 | vec3 normal; 29 | normal.z = dot(n, n) * 2.0 - 1.0; 30 | normal.xy = normalize(n) * sqrt(1.0 - normal.z * normal.z); 31 | return normal; 32 | } 33 | 34 | vec3 decodePosition(float depth) { 35 | vec4 clipSpace = vec4(vPosition.xy, depth * 2.0 - 1.0, 1.0); 36 | vec4 homogenous = inverseProjection * clipSpace; 37 | return homogenous.xyz / homogenous.w; 38 | } 39 | 40 | void main() { 41 | vec2 uv = vPosition.xy * 0.5 + vec2(0.5); 42 | vec2 gridIndex = uv; 43 | vec4 lightIndexInfo = texture2D(uLightOffsetCount, gridIndex); 44 | float lightStartIndex = lightIndexInfo.r; 45 | float lightNum = lightIndexInfo.w; 46 | vec4 tex1 = texture2D(normalRoughnessTex, uv); 47 | vec4 tex2 = texture2D(albedoMetallicTex, uv); 48 | 49 | vec3 normal = tex1.xyz; 50 | Material material; 51 | material.roughness = tex1.w; 52 | material.albedo = tex2.xyz; 53 | float depth = unpackFloat1x32(texture2D(depthTex, uv)); 54 | vec3 viewPosition = decodePosition(depth); 55 | vec3 totalColor = vec3(0.0); 56 | int realCount = 0; 57 | for(int i = 0; i < MAX_TILE_LIGHT_NUM; i++) { 58 | if (float(i) > lightNum - 0.5) { 59 | break; 60 | } 61 | float fixlightId = texture2D(uLightIndex, vec2((lightStartIndex + float(i)) / uLightListLengthSqrt, 0.5)).x; 62 | vec4 lightPosR = texture2D(uLightPositionRadius, vec2(fixlightId, 0.5)); 63 | vec4 lightColorIden = texture2D(uLightColorIdensity, vec2(fixlightId, 0.5)); 64 | 65 | vec3 lightDir = normalize(lightPosR.xyz - viewPosition); 66 | 67 | float dist = distance(lightPosR.xyz, viewPosition); 68 | 69 | PointLight light; 70 | light.color = lightColorIden.xyz; 71 | light.idensity = lightColorIden.w; 72 | light.radius = lightPosR.w; 73 | light.position = lightPosR.xyz; 74 | light.squareAtten = 0.01; 75 | light.linearAtten = 0.01; 76 | light.constantAtten = 0.01; 77 | 78 | if (dist < light.radius) { 79 | totalColor += calculatePointLight( 80 | light, 81 | material, 82 | viewPosition, 83 | normal, 84 | vec3(0.0) 85 | ); 86 | } 87 | } 88 | vec3 test = vec3(float(realCount) / 32.0); 89 | gl_FragColor = vec4(totalColor, 1.0); 90 | } 91 | -------------------------------------------------------------------------------- /src/materials/surface/StandardMaterial.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { DataType } from "../../DataTypeEnum"; 4 | import { define, structure, texture, uniform } from "../../Decorators"; 5 | import { Program, shaderPassLib } from "../../shader/Program"; 6 | import { ShaderBuilder } from "../../shader/ShaderBuilder"; 7 | import { ShaderSource } from "../../shader/shaders"; 8 | import { CubeTexture } from "../../textures/CubeTexture"; 9 | import { Texture } from "../../textures/Texture"; 10 | import { Texture2D } from "../../textures/Texture2D"; 11 | 12 | import { BlinnPhongMaterial } from "./BlinnPhongMaterial"; 13 | import { ISurfaceMaterial } from "./ISurfaceMaterial"; 14 | 15 | @structure("uMaterial") 16 | export class StandardMaterial extends ISurfaceMaterial { 17 | public static fromLaggard( 18 | gl: WebGLRenderingContext, 19 | blinnPhong: BlinnPhongMaterial, 20 | ) { 21 | const standard = new StandardMaterial(gl); 22 | standard.name = blinnPhong.name; 23 | standard 24 | .setAlbedo(blinnPhong.diffuse) 25 | .setAmbient(blinnPhong.ambient) 26 | .setAlphaMap(blinnPhong.alphaMap) 27 | .setBumpMap(blinnPhong.bumpMap) 28 | .setTransparency(blinnPhong.transparency) 29 | .setMainTexture(blinnPhong.mainTexture) 30 | .setCastShadow(blinnPhong.blockShadow) 31 | .setRecieveShadow(blinnPhong.receiveShadow) 32 | .setDebugMode(blinnPhong.debugMode) 33 | .setEnvironmentMap(blinnPhong.environmentMap); 34 | return standard; 35 | } 36 | 37 | protected _albedo: vec3 = vec3.fromValues(0.8, 0.8, 0.8); 38 | 39 | protected _metallic: number = 0.8; 40 | 41 | @define("_METALLIC_TEXTURE") 42 | @texture("uMetallicTexture") 43 | protected _metallicTexture: Texture2D; 44 | 45 | protected _roughness: number = 0.5; 46 | 47 | @uniform(DataType.vec3) 48 | public get albedo() { 49 | return this._albedo; 50 | } 51 | 52 | @uniform(DataType.float) 53 | public get metallic() { 54 | return this._metallic; 55 | } 56 | 57 | @uniform(DataType.float) 58 | public get roughness() { 59 | return this._roughness; 60 | } 61 | 62 | public get stencilMap() { 63 | return this.stencilMap; 64 | } 65 | 66 | public setAlbedo(_albedo: vec3) { 67 | this._albedo = _albedo; 68 | return this; 69 | } 70 | 71 | public setMetallic(_metallic: number) { 72 | this._metallic = _metallic; 73 | return this; 74 | } 75 | 76 | public setRoughness(_roughness: number) { 77 | this._roughness = _roughness; 78 | return this; 79 | } 80 | 81 | protected initShader(gl: WebGLRenderingContext) { 82 | return new ShaderBuilder() 83 | .addDefinition(ShaderSource.definitions__material_pbs_glsl) 84 | .setLightModel(ShaderSource.light_model__pbs_ggx_glsl) 85 | .setExtraRenderParamHolder("mvp", { 86 | uniforms: { 87 | modelViewProjectionMatrix: 88 | shaderPassLib.uniforms.modelViewProjectionMatrix, 89 | }, 90 | }) 91 | .setExtraRenderParamHolder("pcss", { 92 | defines: shaderPassLib.defines, 93 | }) 94 | .build(gl); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Util.ts: -------------------------------------------------------------------------------- 1 | namespace jasmine { 2 | export interface ToyMatchers extends Matchers { 3 | toBeEqualish(expected: any, expectationFailOutput?: any): boolean; 4 | } 5 | } 6 | 7 | namespace Testing { 8 | export const EPSILON = 0.00001; 9 | export function createCanvas(width: number, height: number) { 10 | const canvas = document.createElement("canvas"); 11 | canvas.width = width; 12 | canvas.height = height; 13 | canvas.style.backgroundColor = "black"; 14 | document.body.appendChild(canvas); 15 | return canvas; 16 | } 17 | } 18 | 19 | beforeAll(() => { 20 | jasmine.addMatchers({ 21 | toBeEqualish: () => { 22 | const result = { 23 | compare: (actual: any, expected: any) => { 24 | if (typeof (actual) === "number") { 25 | return { 26 | pass: Math.abs(actual - expected) < Testing.EPSILON, 27 | }; 28 | } 29 | if (actual.length !== expected.length) { 30 | return { 31 | pass: false, 32 | }; 33 | } 34 | for (let i = 0; i < actual.length; i++) { 35 | if (isNaN(actual[i]) !== isNaN(expected[i])) { 36 | return { 37 | pass: false, 38 | }; 39 | } 40 | if (Math.abs(actual[i] - expected[i]) >= Testing.EPSILON) { 41 | return { 42 | pass: false, 43 | }; 44 | } 45 | } 46 | return { 47 | pass: true, 48 | }; 49 | }, 50 | }; 51 | return result; 52 | }, 53 | toBeNotEqualish: () => { 54 | const result = { 55 | compare: (actual: any, expected: any) => { 56 | if (typeof (actual) === "number") { 57 | return { 58 | pass: Math.abs(actual - expected) > Testing.EPSILON, 59 | }; 60 | } 61 | if (actual.length !== expected.length) { 62 | return { 63 | pass: true, 64 | }; 65 | } 66 | for (let i = 0; i < actual.length; i++) { 67 | if (isNaN(actual[i]) !== isNaN(expected[i])) { 68 | return { 69 | pass: true, 70 | }; 71 | } 72 | if (Math.abs(actual[i] - expected[i]) >= Testing.EPSILON) { 73 | return { 74 | pass: true, 75 | }; 76 | } 77 | } 78 | return { 79 | pass: false, 80 | }; 81 | }, 82 | }; 83 | return result; 84 | }, 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/geometries/CubeGeometry.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Geometry } from "./Geometry"; 3 | 4 | export class CubeGeometry extends Geometry { 5 | 6 | constructor(gl: WebGLRenderingContext) { 7 | super(gl); 8 | this.attributes.position.data = [ 9 | -1.0, -1.0, 1.0, 10 | 1.0, -1.0, 1.0, 11 | 1.0, 1.0, 1.0, 12 | -1.0, 1.0, 1.0, 13 | 14 | // Back face 15 | -1.0, -1.0, -1.0, 16 | -1.0, 1.0, -1.0, 17 | 1.0, 1.0, -1.0, 18 | 1.0, -1.0, -1.0, 19 | 20 | // Top face 21 | -1.0, 1.0, -1.0, 22 | -1.0, 1.0, 1.0, 23 | 1.0, 1.0, 1.0, 24 | 1.0, 1.0, -1.0, 25 | 26 | // Bottom face 27 | -1.0, -1.0, -1.0, 28 | 1.0, -1.0, -1.0, 29 | 1.0, -1.0, 1.0, 30 | -1.0, -1.0, 1.0, 31 | 32 | // Right face 33 | 1.0, -1.0, -1.0, 34 | 1.0, 1.0, -1.0, 35 | 1.0, 1.0, 1.0, 36 | 1.0, -1.0, 1.0, 37 | 38 | // Left face 39 | -1.0, -1.0, -1.0, 40 | -1.0, -1.0, 1.0, 41 | -1.0, 1.0, 1.0, 42 | -1.0, 1.0, -1.0, 43 | ]; 44 | 45 | this.attributes.aMainUV.data = [ 46 | 0, 0, 47 | 1, 0, 48 | 1, 1, 49 | 0, 1, 50 | 51 | 0, 0, 52 | 1, 0, 53 | 1, 1, 54 | 0, 1, 55 | 56 | 0, 0, 57 | 1, 0, 58 | 1, 1, 59 | 0, 1, 60 | 61 | 0, 0, 62 | 1, 0, 63 | 1, 1, 64 | 0, 1, 65 | 66 | 0, 0, 67 | 1, 0, 68 | 1, 1, 69 | 0, 1, 70 | 71 | 0, 0, 72 | 1, 0, 73 | 1, 1, 74 | 0, 1, 75 | ]; 76 | 77 | this.attributes.aNormal.data = [ 78 | // front 79 | 0.0, 0.0, 1.0, 80 | 0.0, 0.0, 1.0, 81 | 0.0, 0.0, 1.0, 82 | 0.0, 0.0, 1.0, 83 | 84 | // back 85 | 0.0, 0.0, -1.0, 86 | 0.0, 0.0, -1.0, 87 | 0.0, 0.0, -1.0, 88 | 0.0, 0.0, -1.0, 89 | 90 | // top 91 | 0.0, 1.0, 0.0, 92 | 0.0, 1.0, 0.0, 93 | 0.0, 1.0, 0.0, 94 | 0.0, 1.0, 0.0, 95 | 96 | // bottom 97 | 0.0, -1.0, 0.0, 98 | 0.0, -1.0, 0.0, 99 | 0.0, -1.0, 0.0, 100 | 0.0, -1.0, 0.0, 101 | 102 | 1.0, 0.0, 0.0, 103 | 1.0, 0.0, 0.0, 104 | 1.0, 0.0, 0.0, 105 | 1.0, 0.0, 0.0, 106 | 107 | -1.0, 0.0, 0.0, 108 | -1.0, 0.0, 0.0, 109 | -1.0, 0.0, 0.0, 110 | -1.0, 0.0, 0.0, 111 | 112 | ]; 113 | 114 | this.faces.data = [ 115 | 0, 1, 2, 0, 2, 3, // front 116 | 4, 5, 6, 4, 6, 7, // back 117 | 8, 9, 10, 8, 10, 11, // top 118 | 12, 13, 14, 12, 14, 15, // bottom 119 | 16, 17, 18, 16, 18, 19, // right 120 | 20, 21, 22, 20, 22, 23, // left 121 | ]; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/lights/DirectionalLight.ts: -------------------------------------------------------------------------------- 1 | import { mat4, quat, vec3 } from "gl-matrix"; 2 | import { Camera } from "../cameras/Camera"; 3 | import { OrthoCamera } from "../cameras/OrthoCamera"; 4 | import { DataType } from "../DataTypeEnum"; 5 | import { uniform } from "../Decorators"; 6 | import { BoundingBox2D } from "../Intersections/BoundingBox"; 7 | import { FrameBuffer } from "../renderer/FrameBuffer"; 8 | import { Renderer } from "../renderer/Renderer"; 9 | import { ProcessingFrameBuffer } from "../renderer/SwapFramebuffer"; 10 | import { Texture } from "../textures/Texture"; 11 | import { Light } from "./Light"; 12 | 13 | export class DirectionalLight extends Light { 14 | 15 | protected _shadowFrameBuffer: ProcessingFrameBuffer; 16 | 17 | constructor(renderer: Renderer) { 18 | super(renderer); 19 | this.setShadowSize(1024); 20 | } 21 | 22 | public get shadowMap() { 23 | return this._shadowFrameBuffer.active.attachments.color.targetTexture; 24 | } 25 | 26 | public get shadowFrameBuffers() { 27 | return [this._shadowFrameBuffer]; 28 | } 29 | 30 | @uniform(DataType.vec3) 31 | public get direction(): vec3 { 32 | return vec3.transformQuat(vec3.create(), vec3.fromValues(0, 0, -1), 33 | mat4.getRotation(quat.create(), this._matrix), 34 | ); 35 | } 36 | 37 | public getProjecttionBoundingBox2D(camera: Camera): BoundingBox2D { 38 | return { 39 | left: -1, 40 | right: 1, 41 | top: 1, 42 | bottom: -1, 43 | }; 44 | } 45 | 46 | public setDirection(_direction: vec3) { 47 | const lookPoint = vec3.add(vec3.create(), this._position, _direction); 48 | this.lookAt(lookPoint); 49 | return this; 50 | } 51 | 52 | public setShadowSize(_size: number) { 53 | super.setShadowSize(_size); 54 | if (this._shadowFrameBuffer !== null) { 55 | this._shadowFrameBuffer.setWidth(_size).setHeight(_size).attach(this.gl); 56 | } 57 | return this; 58 | } 59 | 60 | public clearShadowFrameBuffer() { 61 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this._shadowFrameBuffer.active.glFramebuffer); 62 | this.gl.enable(this.gl.DEPTH_TEST); 63 | this.gl.depthFunc(this.gl.LEQUAL); 64 | this.gl.clearColor(this.far, 0, 0, 0); 65 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT | this.gl.COLOR_BUFFER_BIT); 66 | } 67 | 68 | protected init(renderer: Renderer) { 69 | if (!this._shadowFrameBuffer) { 70 | this._shadowFrameBuffer = new ProcessingFrameBuffer(this.gl) 71 | .onInit((frameBuffer: FrameBuffer) => { 72 | frameBuffer 73 | .setWidth(this._shadowSize) 74 | .setHeight(this._shadowSize); 75 | frameBuffer.attachments.color.targetTexture 76 | .setType(this.gl.FLOAT) 77 | .setFormat(this.gl.RGBA) 78 | .setMinFilter(this.gl.NEAREST) 79 | .setMagFilter(this.gl.NEAREST) 80 | .setWrapS(this.gl.REPEAT) 81 | .setWrapT(this.gl.REPEAT) 82 | .apply(this.gl); 83 | frameBuffer.attach(this.gl); 84 | }, 85 | ); 86 | } 87 | this._projectCamera = new OrthoCamera() 88 | .setParent(this) 89 | .setLocalPosition(vec3.create()) 90 | .setAspectRadio(1); 91 | return this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/cameras/Camera.ts: -------------------------------------------------------------------------------- 1 | import { mat4, vec2, vec3 } from "gl-matrix"; 2 | import { DataType } from "../DataTypeEnum"; 3 | import { uniform } from "../Decorators"; 4 | import { Object3d } from "../Object3d"; 5 | 6 | export enum CameraDirection { 7 | forward, 8 | bakc, 9 | left, 10 | right, 11 | } 12 | 13 | export abstract class Camera extends Object3d { 14 | 15 | protected _upVector: vec3 = vec3.fromValues(0, 1, 0); 16 | protected _centerVector: vec3 = vec3.fromValues(0, 0, -1); 17 | protected _rightVector: vec3 = vec3.fromValues(1, 0, 0); 18 | protected _projectionMatrix: mat4 = mat4.create(); 19 | 20 | protected _near: number = 0.1; 21 | 22 | protected _far: number = 500; 23 | 24 | private _controlEnable: boolean = false; 25 | private _cameraPitch: number = 0.0; 26 | private _cameraYaw: number = -90.0; 27 | private _cameraSpeed: number = 2.5; 28 | 29 | constructor() { 30 | super(); 31 | } 32 | 33 | @uniform(DataType.vec3, "cameraPos") 34 | public get position() { 35 | return this._position; 36 | } 37 | 38 | @uniform(DataType.float, "cameraNear") 39 | public get near() { 40 | return this._near; 41 | } 42 | 43 | @uniform(DataType.float, "cameraFar") 44 | public get far() { 45 | return this._far; 46 | } 47 | 48 | public get eyeVector() { 49 | return vec3.clone(this._centerVector); 50 | } 51 | 52 | public get upVector() { 53 | return vec3.clone(this._upVector); 54 | } 55 | 56 | public get centerVector() { 57 | return vec3.clone(this._centerVector); 58 | } 59 | 60 | public get rightVector() { 61 | return vec3.clone(this._rightVector); 62 | } 63 | 64 | public get projectionMatrix() { 65 | return this._projectionMatrix; 66 | } 67 | 68 | public setNear(near: number) { 69 | if (near !== this._near) { 70 | this._near = near; 71 | this.compuseProjectionMatrix(); 72 | } 73 | return this; 74 | } 75 | 76 | public setFar(far: number) { 77 | if (far !== this._far) { 78 | this._far = far; 79 | this.compuseProjectionMatrix(); 80 | } 81 | return this; 82 | } 83 | 84 | public set controlEnable(enable: boolean) { 85 | this._controlEnable = enable; 86 | } 87 | public get controlEnable() { 88 | return this._controlEnable; 89 | } 90 | 91 | public changeDirectionByAngle(deltaAngle: vec2) { 92 | this._cameraYaw += deltaAngle[0]; 93 | this._cameraPitch += deltaAngle[1]; 94 | if (this._cameraPitch > 89.0) { 95 | this._cameraPitch = 89.0; 96 | } 97 | if (this._cameraPitch < -89.0) { 98 | this._cameraPitch = -89.0; 99 | } 100 | const newEyeVector = vec3.fromValues( 101 | Math.cos(this._cameraPitch * Math.PI / 180.0) * Math.cos(this._cameraYaw * Math.PI / 180.0), 102 | Math.sin(this._cameraPitch * Math.PI / 180.0), 103 | Math.cos(this._cameraPitch * Math.PI / 180.0) * Math.sin(this._cameraYaw * Math.PI / 180.0), 104 | ); 105 | this._centerVector = newEyeVector; 106 | super.lookAt(newEyeVector); 107 | } 108 | 109 | public abstract changeZoom(offset: number); 110 | 111 | public genOtherMatrixs() { 112 | super.genOtherMatrixs(); 113 | this.compuseProjectionMatrix(); 114 | } 115 | 116 | public abstract compuseProjectionMatrix(); 117 | 118 | public abstract deCompuseProjectionMatrix(); 119 | 120 | public abstract setAspectRadio(radio: number): Camera; 121 | } 122 | -------------------------------------------------------------------------------- /src/geometries/SphereGeometry.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { Geometry } from "./Geometry"; 4 | 5 | export class SphereGeometry extends Geometry { 6 | private _radius: number = 1; 7 | private _widthSegments: number = 8; 8 | private _heightSegments: number = 6; 9 | private _phiStart: number = 0; 10 | private _phiLength: number = Math.PI * 2; 11 | private _thetaStart: number = 0; 12 | private _thetaLength: number = Math.PI; 13 | 14 | constructor(gl: WebGLRenderingContext) { 15 | super(gl); 16 | } 17 | 18 | public build() { 19 | 20 | // thanks to the algorithm by there.js 21 | let iy = 0; 22 | let ix = 0; 23 | let index = 0; 24 | const grid = []; 25 | const thetaEnd = this._thetaStart + this._thetaLength; 26 | for (iy = 0; iy <= this._heightSegments; iy++) { 27 | const verticesRow = []; 28 | const v = iy / this._heightSegments; 29 | for (ix = 0; ix <= this._widthSegments; ix++) { 30 | const aMainUV = [ix / this._widthSegments, 1 - iy / this._heightSegments]; 31 | const position = [ 32 | - this._radius * Math.cos(this._phiStart + aMainUV[0] * this._phiLength) 33 | * Math.sin(this._thetaStart + v * this._thetaLength), 34 | this._radius * Math.cos(this._thetaStart + aMainUV[1] * this._thetaLength), 35 | this._radius * Math.sin(this._phiStart + aMainUV[0] * this._phiLength) 36 | * Math.sin(this._thetaStart + v * this._thetaLength), 37 | ]; 38 | const aNormal = vec3.normalize(vec3.create(), position); 39 | this.addVertex({ position, aNormal, aMainUV }); 40 | verticesRow.push(index++); 41 | } 42 | grid.push(verticesRow); 43 | } 44 | for (iy = 0; iy < this._heightSegments; iy++) { 45 | for (ix = 0; ix < this._widthSegments; ix++) { 46 | const a = grid[iy][ix + 1]; 47 | const b = grid[iy][ix]; 48 | const c = grid[iy + 1][ix]; 49 | const d = grid[iy + 1][ix + 1]; 50 | if (iy !== 0 || this._thetaStart > 0) { 51 | this.faces.data.push(a, b, d); 52 | } 53 | if (iy !== this._heightSegments - 1 || thetaEnd < Math.PI) { 54 | this.faces.data.push(b, c, d); 55 | } 56 | } 57 | } 58 | return this; 59 | } 60 | 61 | public get radius(): number { return this._radius; } 62 | public get widthSegments(): number { return this._widthSegments; } 63 | public get heightSegments(): number { return this._heightSegments; } 64 | public get phiStart(): number { return this._phiStart; } 65 | public get phiLength(): number { return this._phiLength; } 66 | public get thetaStart(): number { return this._thetaStart; } 67 | public get thetaLength(): number { return this._thetaLength; } 68 | 69 | public setRadius(radius: number) { 70 | this._radius = radius; 71 | return this; 72 | } 73 | public setWidthSegments(widthSegments: number) { 74 | this._widthSegments = widthSegments; 75 | return this; 76 | } 77 | public setHeightSegments(heightSegments: number) { 78 | this._heightSegments = heightSegments; 79 | return this; 80 | } 81 | public setPhiStart(phiStart: number) { 82 | this._phiStart = phiStart; 83 | return this; 84 | } 85 | public setPhiLength(phiLength: number) { 86 | this._phiLength = phiLength; 87 | return this; 88 | } 89 | public setThetaStart(thetaStart: number) { 90 | this._thetaStart = thetaStart; 91 | return this; 92 | } 93 | public setThetaLength(thetaLength: number) { 94 | this._thetaLength = thetaLength; 95 | return this; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/shader/sources/calculators/shadow_factor.glsl: -------------------------------------------------------------------------------- 1 | #ifdef RECEIVE_SHADOW 2 | 3 | vec4 texture2DbilinearEXP(sampler2D shadowMap, vec2 uv, float texelSize) { 4 | vec2 f = fract(uv / texelSize - 0.5); 5 | vec2 centroidUV = (floor(uv / texelSize - 0.5)) * texelSize; 6 | 7 | vec4 lb = texture2D(shadowMap, centroidUV + texelSize * vec2(0.0, 0.0)); 8 | vec4 lt = texture2D(shadowMap, centroidUV + texelSize * vec2(0.0, 1.0)); 9 | vec4 rb = texture2D(shadowMap, centroidUV + texelSize * vec2(1.0, 0.0)); 10 | vec4 rt = texture2D(shadowMap, centroidUV + texelSize * vec2(1.0, 1.0)); 11 | vec4 a = lb + log(mix(vec4(1.0), exp(lt - lb), f.y)); 12 | vec4 b = rb + log(mix(vec4(1.0), exp(rt - rb), f.y)); 13 | vec4 z = a + log(mix(vec4(1.0), exp(b - a), f.x)); 14 | return z; 15 | } 16 | 17 | vec4 texture2Dbilinear(sampler2D shadowMap, vec2 uv, float texelSize) { 18 | vec2 f = fract(uv / texelSize - 0.5); 19 | vec2 centroidUV = (floor(uv / texelSize - 0.5)) * texelSize; 20 | 21 | vec4 lb = texture2D(shadowMap, centroidUV + texelSize * vec2(0.0, 0.0)); 22 | vec4 lt = texture2D(shadowMap, centroidUV + texelSize * vec2(0.0, 1.0)); 23 | vec4 rb = texture2D(shadowMap, centroidUV + texelSize * vec2(1.0, 0.0)); 24 | vec4 rt = texture2D(shadowMap, centroidUV + texelSize * vec2(1.0, 1.0)); 25 | vec4 a = mix(lb, lt, f.y); 26 | vec4 b = mix(rb, rt, f.y); 27 | vec4 z = mix(a, b, f.x); 28 | return z; 29 | } 30 | 31 | float texture2Dfilter(sampler2D shadowMap, vec2 uv, float texelSize) { 32 | vec2 info = texture2Dbilinear(shadowMap, uv, texelSize).xy; 33 | float base = info.r; 34 | float kernelSize = info.g; 35 | float sum = 0.0; 36 | for (int i = 0; i < FILTER_SIZE; ++i) { 37 | for (int j = 0; j < FILTER_SIZE; ++j) { 38 | vec2 subuv = uv + vec2(float(i) + 0.5 - float(FILTER_SIZE) / 2.0, float(j) + 0.5 - float(FILTER_SIZE) / 2.0) * texelSize * kernelSize; 39 | float z = texture2Dbilinear(shadowMap, subuv, texelSize).r; 40 | float expd = exp(z - base); 41 | sum += expd; 42 | } 43 | } 44 | sum /= float(FILTER_SIZE * FILTER_SIZE); 45 | return base + log(sum); 46 | } 47 | 48 | float pcf(sampler2D shadowMap, vec2 uv, float depth, float bias, float texelSize) { 49 | vec2 info = texture2Dbilinear(shadowMap, uv, texelSize).xy; 50 | float kernelSize = 1.0; 51 | float sum = 0.0; 52 | for (int i = 0; i < FILTER_SIZE; ++i) { 53 | for (int j = 0; j < FILTER_SIZE; ++j) { 54 | float z = texture2Dbilinear(shadowMap, uv + kernelSize * vec2(float(i) + 0.5 - float(FILTER_SIZE) / 2.0, float(j) + 0.5 - float(FILTER_SIZE) / 2.0).x * texelSize, texelSize).r; 55 | sum += step(depth - bias, z) / float(FILTER_SIZE * FILTER_SIZE); 56 | } 57 | } 58 | return sum; 59 | } 60 | 61 | float getSpotDirectionShadow(vec2 clipPos, sampler2D shadowMap, float linearDepth, float lambertian, float texelSize, int shadowLevel, float softness) 62 | { 63 | if (shadowLevel == SHADOW_LEVEL_NONE) { 64 | return 1.0; 65 | } else { 66 | vec2 uv = clipPos * 0.5 + 0.5; 67 | float bias = clamp(0.2 * tan(acos(lambertian)), 0.0, 1.0); 68 | if (shadowLevel == SHADOW_LEVEL_HARD) { 69 | return step(linearDepth, texture2D(shadowMap, uv).r + bias); 70 | } else { 71 | float z = texture2DbilinearEXP(shadowMap, uv, texelSize).r; 72 | float s = exp(z - linearDepth * softness); 73 | return min(s, 1.0); 74 | } 75 | } 76 | } 77 | 78 | float getPointShadow(vec3 cubePos, samplerCube shadowMap, float linearDepth, float lambertian, float texelSize, int shadowLevel, float softness) 79 | { 80 | float bias = clamp(0.2 * tan(acos(lambertian)), 0.0, 1.0); 81 | if (shadowLevel == SHADOW_LEVEL_NONE) { 82 | return 1.0; 83 | } else { 84 | // if (shadowLevel == SHADOW_LEVEL_HARD) { 85 | return step(linearDepth, textureCube(shadowMap, cubePos).r + bias); 86 | //else { 87 | // TODO: perform cubemap interpolation for soft-level shadow map for point light 88 | //} 89 | } 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /tests/unit/Loader/OBJLoader-Test.ts: -------------------------------------------------------------------------------- 1 | import * as CanvasToy from "../../../src/CanvasToy"; 2 | 3 | class OBJLoaderIm extends CanvasToy.OBJLoader { 4 | public static testRegulars() { 5 | let pattern: RegExp; 6 | let str: string; 7 | describe("faceSplitVertPattern", () => { 8 | beforeEach(() => { 9 | pattern = OBJLoaderIm.faceSplitVertPattern; 10 | }); 11 | it("should match spliters", () => { 12 | str = "1 -4/-5 9//213 321 87/9/43"; 13 | const result = str.match(pattern); 14 | expect(result[0]).toBe("1"); 15 | expect(result[1]).toBe("-4/-5"); 16 | expect(result[2]).toBe("9//213"); 17 | expect(result[3]).toBe("321"); 18 | expect(result[4]).toBe("87/9/43"); 19 | }); 20 | }); 21 | describe("facePerVertPattern", () => { 22 | beforeEach(() => { 23 | pattern = OBJLoaderIm.facePerVertPattern; 24 | }); 25 | it("should split pos-only vertex", () => { 26 | str = "1"; 27 | const result = str.match(pattern); 28 | expect(result[1]).toBe("1"); 29 | expect(result[2]).toBe(""); 30 | expect(result[3]).toBe(""); 31 | }); 32 | it("should split pos-normal vertex", () => { 33 | str = "1/3"; 34 | const result = str.match(pattern); 35 | expect(result[1]).toBe("1"); 36 | expect(result[2]).toBe("3"); 37 | expect(result[3]).toBe(""); 38 | }); 39 | it("should split pos-uv vertex", () => { 40 | str = "1//4"; 41 | const result = str.match(pattern); 42 | expect(result[1]).toBe("1"); 43 | expect(result[2]).toBe(""); 44 | expect(result[3]).toBe("4"); 45 | }); 46 | }); 47 | // TODO(Daniel): objectSplitPattern 48 | describe("vertexPattern", () => { 49 | beforeEach(() => { 50 | pattern = OBJLoaderIm.vertexPattern; 51 | }); 52 | it("should match vertex", () => { 53 | str = ` 54 | v 0.2313 324 -78 55 | 1 2 4 56 | `; 57 | const result = str.match(pattern); 58 | expect(result[0]).toBe("v 0.2313 324 -78"); 59 | }); 60 | }); 61 | describe("normalPattern", () => { 62 | beforeEach(() => { 63 | pattern = OBJLoaderIm.normalPattern; 64 | }); 65 | it("should match normal", () => { 66 | str = ` 67 | vn 0.2313 324 -78 68 | 1 2 4 69 | `; 70 | const result = str.match(pattern); 71 | expect(result[0]).toBe("vn 0.2313 324 -78"); 72 | }); 73 | }); 74 | describe("uvPattern", () => { 75 | beforeEach(() => { 76 | pattern = OBJLoaderIm.uvPattern; 77 | }); 78 | it("should match uv", () => { 79 | str = ` 80 | vt 0.2313 324 -78 81 | 1 2 4 82 | `; 83 | const result = str.match(pattern); 84 | expect(result[0]).toBe("vt 0.2313 324 -78"); 85 | }); 86 | }); 87 | describe("indexPattern", () => { 88 | beforeEach(() => { 89 | pattern = OBJLoaderIm.indexPattern; 90 | }); 91 | it("should split pos-only vertex", () => { 92 | str = ` 93 | f 1/2/3 2/3/4 4/5/6 7/8/9 94 | 1 2 4 95 | `; 96 | const result = str.match(pattern); 97 | expect(result[0]).toBe("f 1/2/3 2/3/4 4/5/6 7/8/9"); 98 | }); 99 | }); 100 | } 101 | } 102 | 103 | describe("OBJLoader Test", () => { 104 | OBJLoaderIm.testRegulars(); 105 | }); 106 | -------------------------------------------------------------------------------- /src/textures/Texture.ts: -------------------------------------------------------------------------------- 1 | import { IAsyncResource } from "../IAsyncResource"; 2 | 3 | export class Texture implements IAsyncResource { 4 | protected _glTexture: WebGLTexture; 5 | 6 | protected _asyncFinished: Promise; 7 | protected _image: HTMLImageElement; 8 | 9 | private _target: number; 10 | private _format: number; 11 | private _wrapS: number; 12 | private _wrapT: number; 13 | private _magFilter: number; 14 | private _minFilter: number; 15 | private _type: number; 16 | 17 | constructor(gl: WebGLRenderingContext, url?: string) { 18 | if (!!url) { 19 | this._image = new Image(); 20 | const image = this._image; 21 | this.setAsyncFinished( 22 | new Promise((resolve, reject) => { 23 | image.onload = () => resolve(this); 24 | image.onerror = () => reject(this); 25 | this._image.src = url; 26 | }), 27 | ); 28 | } 29 | this.setTarget(gl.TEXTURE_2D) 30 | .setFormat(gl.RGB) 31 | .setWrapS(gl.CLAMP_TO_EDGE) 32 | .setWrapT(gl.CLAMP_TO_EDGE) 33 | .setMagFilter(gl.NEAREST) 34 | .setMinFilter(gl.NEAREST) 35 | .setType(gl.UNSIGNED_BYTE); 36 | this._glTexture = gl.createTexture(); 37 | } 38 | 39 | public get glTexture() { 40 | return this._glTexture; 41 | } 42 | 43 | public get image() { 44 | return this._image; 45 | } 46 | 47 | public get target() { 48 | return this._target; 49 | } 50 | 51 | public get format() { 52 | return this._format; 53 | } 54 | 55 | public get wrapS() { 56 | return this._wrapS; 57 | } 58 | 59 | public get wrapT() { 60 | return this._wrapT; 61 | } 62 | 63 | public get magFilter() { 64 | return this._magFilter; 65 | } 66 | 67 | public get minFilter() { 68 | return this._minFilter; 69 | } 70 | 71 | public get type() { 72 | return this._type; 73 | } 74 | 75 | public setTarget(_target: number) { 76 | this._target = _target; 77 | return this; 78 | } 79 | 80 | public setFormat(_format: number) { 81 | this._format = _format; 82 | return this; 83 | } 84 | 85 | public setWrapS(_wrapS: number) { 86 | this._wrapS = _wrapS; 87 | return this; 88 | } 89 | 90 | public setWrapT(_wrapT: number) { 91 | this._wrapT = _wrapT; 92 | return this; 93 | } 94 | 95 | public setMagFilter(_magFilter: number) { 96 | this._magFilter = _magFilter; 97 | return this; 98 | } 99 | 100 | public setMinFilter(_minFilter: number) { 101 | this._minFilter = _minFilter; 102 | return this; 103 | } 104 | 105 | public setType(_type: number) { 106 | this._type = _type; 107 | return this; 108 | } 109 | 110 | public setAsyncFinished(promise: Promise) { 111 | this._asyncFinished = promise; 112 | return this; 113 | } 114 | 115 | public asyncFinished() { 116 | return this._asyncFinished; 117 | } 118 | 119 | public apply(gl: WebGLRenderingContext) { 120 | gl.bindTexture(this.target, this.glTexture); 121 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 122 | gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, this.wrapS); 123 | gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, this.wrapT); 124 | gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, this.magFilter); 125 | gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, this.minFilter); 126 | return this; 127 | } 128 | 129 | public applyForRendering( 130 | gl: WebGLRenderingContext, 131 | width: number, 132 | height: number, 133 | ) { 134 | gl.bindTexture(this.target, this.glTexture); 135 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); 136 | gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, this.wrapS); 137 | gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, this.wrapT); 138 | gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, this.magFilter); 139 | gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, this.minFilter); 140 | gl.texImage2D( 141 | this.target, 142 | 0, 143 | this.format, 144 | width, 145 | height, 146 | 0, 147 | this.format, 148 | this.type, 149 | null, 150 | ); 151 | return this; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/lights/Light.ts: -------------------------------------------------------------------------------- 1 | import { mat4, vec3 } from "gl-matrix"; 2 | import { Camera } from "../cameras/Camera"; 3 | import { DataType } from "../DataTypeEnum"; 4 | import { ifdefine, uniform } from "../Decorators"; 5 | import { Geometry } from "../geometries/Geometry"; 6 | import { BoundingBox2D } from "../Intersections/BoundingBox"; 7 | import { Object3d } from "../Object3d"; 8 | import { WebGLExtension } from "../renderer/IExtension"; 9 | import { Renderer } from "../renderer/Renderer"; 10 | import { ProcessingFrameBuffer } from "../renderer/SwapFramebuffer"; 11 | import { IBuildinRenderParamMaps } from "../shader/Program"; 12 | import { Texture } from "../textures/Texture"; 13 | import { ShadowLevel } from "./ShadowLevel"; 14 | 15 | export abstract class Light extends Object3d { 16 | public volume: Geometry; 17 | 18 | protected _color = vec3.fromValues(1, 1, 1); 19 | 20 | protected _idensity = 1; 21 | 22 | protected _pcssArea: number = 5; 23 | 24 | @uniform(DataType.int, "shadowLevel") 25 | protected _shadowLevel: ShadowLevel = ShadowLevel.PCSS; 26 | 27 | @uniform(DataType.float, "softness") 28 | protected _shadowSoftness = 1.0; 29 | 30 | protected _projectCamera: Camera; 31 | 32 | protected _shadowSize: number = 512; 33 | 34 | protected gl: WebGLRenderingContext; 35 | 36 | protected ext: WebGLExtension; 37 | 38 | constructor(renderer: Renderer) { 39 | super(); 40 | this.gl = renderer.gl; 41 | this.ext = renderer.ext; 42 | this.init(renderer); 43 | } 44 | 45 | public abstract getProjecttionBoundingBox2D(camera: Camera): BoundingBox2D; 46 | 47 | public setColor(color: vec3) { 48 | this._color = color; 49 | return this; 50 | } 51 | 52 | public setIdensity(idensity: number) { 53 | this._idensity = idensity; 54 | return this; 55 | } 56 | 57 | public setShadowLevel(shadowLevel: ShadowLevel) { 58 | this._shadowLevel = shadowLevel; 59 | return this; 60 | } 61 | 62 | public setShadowSize(shadowSize: number) { 63 | this._shadowSize = shadowSize; 64 | return this; 65 | } 66 | 67 | public setShadowSoftness(_shadowSoftness: number) { 68 | this._shadowSoftness = _shadowSoftness; 69 | return this; 70 | } 71 | 72 | public setPCSSArea(_pcssArea: number) { 73 | this._pcssArea = _pcssArea; 74 | return this; 75 | } 76 | 77 | public get shadowLevel() { 78 | return this._shadowLevel; 79 | } 80 | 81 | public get shadowSoftness() { 82 | return this._shadowSoftness; 83 | } 84 | 85 | public getDeferredInfo(layer: number, renderCamera: Camera) { 86 | switch (layer) { 87 | case 0: 88 | return [this._color[0], this._color[1], this._color[2], this._idensity]; 89 | default: 90 | throw Error("deferred Info " + layer + " undifined"); 91 | } 92 | } 93 | 94 | @ifdefine("RECEIVE_SHADOW") 95 | @uniform(DataType.float, "shadowMapSize") 96 | public get shadowSize() { 97 | return this._shadowSize; 98 | } 99 | 100 | public abstract get shadowMap(): Texture; 101 | 102 | @uniform(DataType.vec3) 103 | public get color() { 104 | return this._color; 105 | } 106 | 107 | @uniform(DataType.float) 108 | public get idensity() { 109 | return this._idensity; 110 | } 111 | 112 | @ifdefine("RECEIVE_SHADOW") 113 | @uniform(DataType.mat4) 114 | public get projectionMatrix() { 115 | return this._projectCamera.projectionMatrix; 116 | } 117 | 118 | @ifdefine("RECEIVE_SHADOW") 119 | @uniform(DataType.mat4) 120 | public get viewMatrix() { 121 | return this._worldToObjectMatrix; 122 | } 123 | 124 | @uniform(DataType.float, "lightArea") 125 | public get pcssArea() { 126 | return this._pcssArea; 127 | } 128 | 129 | public get far() { 130 | return this._projectCamera.far; 131 | } 132 | 133 | public get near() { 134 | return this._projectCamera.near; 135 | } 136 | 137 | /** 138 | * Get all shadow framebuffers owned by this light, 139 | * as some type of Light may have more than one shadow frameBuffer 140 | */ 141 | public abstract get shadowFrameBuffers(): ProcessingFrameBuffer[]; 142 | 143 | public drawWithLightCamera(renderParam: IBuildinRenderParamMaps) { 144 | renderParam.camera = this._projectCamera; 145 | renderParam.light = this; 146 | renderParam.material.shader.pass(renderParam); 147 | } 148 | 149 | public abstract clearShadowFrameBuffer(); 150 | 151 | protected abstract init(render: Renderer); 152 | } 153 | -------------------------------------------------------------------------------- /src/shader/sources/interploters/forward/phong.frag: -------------------------------------------------------------------------------- 1 | uniform Material uMaterial; 2 | uniform vec3 cameraPos; 3 | 4 | varying vec2 vMainUV; 5 | varying vec4 clipPos; 6 | 7 | varying vec3 vNormal; 8 | varying vec3 vPosition; 9 | 10 | #ifdef _MAIN_TEXTURE 11 | uniform sampler2D uMainTexture; 12 | #endif 13 | 14 | #ifdef _ENVIRONMENT_MAP 15 | uniform float reflectivity; 16 | uniform samplerCube uCubeTexture; 17 | #endif 18 | 19 | #if (directLightsNum > 0) 20 | uniform DirectLight directLights[directLightsNum]; 21 | uniform sampler2D directLightShadowMap[directLightsNum]; 22 | #endif 23 | 24 | #if (pointLightsNum > 0) 25 | uniform PointLight pointLights[pointLightsNum]; 26 | uniform samplerCube pointLightShadowMap[pointLightsNum]; 27 | #endif 28 | 29 | #if (spotLightsNum > 0) 30 | uniform SpotLight spotLights[spotLightsNum]; 31 | uniform sampler2D spotLightShadowMap[spotLightsNum]; 32 | #endif 33 | 34 | #ifdef RECEIVE_SHADOW 35 | 36 | #if (directLightsNum > 0) 37 | varying vec4 directShadowCoord[directLightsNum]; 38 | varying float directLightDepth[directLightsNum]; 39 | #endif 40 | 41 | #if (spotLightsNum > 0) 42 | varying vec4 spotShadowCoord[spotLightsNum]; 43 | varying float spotLightDepth[spotLightsNum]; 44 | #endif 45 | 46 | #endif 47 | 48 | void main () { 49 | 50 | #ifdef _MAIN_TEXTURE 51 | gl_FragColor = texture2D(uMainTexture, vMainUV); 52 | #else 53 | #ifdef _DEBUG 54 | gl_FragColor = vec4(vec3(checkerBoard(vMainUV, 0.1)), 1.0); 55 | #else 56 | gl_FragColor = vec4(1.0); 57 | #endif 58 | #endif 59 | vec3 color = vec3(0.0); 60 | vec3 normal = normalize(vNormal); 61 | vec3 totalLighting = uMaterial.ambient; 62 | #ifdef _ENVIRONMENT_MAP 63 | vec3 viewDir = normalize(vPosition - cameraPos); 64 | vec3 skyUV = normalize(reflect(viewDir, vNormal)); 65 | vec3 imageLightColor = textureCube(uCubeTexture, skyUV).xyz; 66 | color += calculateImageBasedLight(uMaterial, skyUV, normal, viewDir, imageLightColor, vec3(0.5)); 67 | #endif 68 | #if (directLightsNum > 0) 69 | for (int index = 0; index < directLightsNum; index++) { 70 | vec3 lighting = calculateDirLight( 71 | directLights[index], 72 | uMaterial, 73 | vPosition, 74 | normal, 75 | cameraPos 76 | ); 77 | #ifdef RECEIVE_SHADOW 78 | float lambertian = dot(-directLights[index].direction, normal); 79 | float shadowFactor = getSpotDirectionShadow( 80 | directShadowCoord[index].xy / directShadowCoord[index].w, 81 | directLightShadowMap[index], 82 | directLightDepth[index], 83 | lambertian, 84 | 1.0 / directLights[index].shadowMapSize, 85 | directLights[index].shadowLevel, 86 | directLights[index].softness 87 | ); 88 | lighting *= shadowFactor; 89 | #endif 90 | totalLighting += lighting; 91 | } 92 | #endif 93 | #if (pointLightsNum > 0) 94 | for (int index = 0; index < pointLightsNum; index++) { 95 | vec3 lighting = calculatePointLight( 96 | pointLights[index], 97 | uMaterial, 98 | vPosition, 99 | normal, 100 | cameraPos 101 | ); 102 | #ifdef RECEIVE_SHADOW 103 | vec3 offset = vPosition - pointLights[index].position; 104 | vec3 cubePos = normalize(offset); 105 | float linearDepth = length(offset); 106 | float lambertian = max(dot(-cubePos, normal), 0.0); 107 | float shadowFactor = getPointShadow( 108 | cubePos, 109 | pointLightShadowMap[index], 110 | linearDepth, 111 | lambertian, 112 | 1.0 / pointLights[index].shadowMapSize, 113 | pointLights[index].shadowLevel, 114 | pointLights[index].softness 115 | ); 116 | lighting *= shadowFactor; 117 | #endif 118 | totalLighting += lighting; 119 | } 120 | #endif 121 | #if (spotLightsNum > 0) 122 | for (int index = 0; index < spotLightsNum; index++) { 123 | vec3 lighting = calculateSpotLight( 124 | spotLights[index], 125 | uMaterial, 126 | vPosition, 127 | normal, 128 | cameraPos 129 | ); 130 | #ifdef RECEIVE_SHADOW 131 | float lambertian = dot(-spotLights[index].spotDir, normal); 132 | float shadowFactor = getSpotDirectionShadow( 133 | spotShadowCoord[index].xy / spotShadowCoord[index].w, 134 | spotLightShadowMap[index], 135 | spotLightDepth[index], 136 | lambertian, 137 | 1.0 / spotLights[index].shadowMapSize, 138 | spotLights[index].shadowLevel, 139 | spotLights[index].softness 140 | ); 141 | lighting *= shadowFactor; 142 | #endif 143 | totalLighting += lighting; 144 | 145 | } 146 | #endif 147 | color += totalLighting; 148 | gl_FragColor *= vec4(color, 1.0); 149 | } 150 | -------------------------------------------------------------------------------- /src/renderer/ShadowPreProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "../cameras/Camera"; 2 | import { RectGeometry } from "../geometries/RectGeometry"; 3 | import { Light } from "../lights/Light"; 4 | import { ShadowLevel } from "../lights/ShadowLevel"; 5 | import { LinearDepthPackMaterial } from "../materials/ESM/DepthPackMaterial"; 6 | import { PCSSFilteringMaterial } from "../materials/ESM/LogBlurMaterial"; 7 | import { IMaterial } from "../materials/Material"; 8 | import { BlinnPhongMaterial } from "../materials/surface/BlinnPhongMaterial"; 9 | import { StandardMaterial } from "../materials/surface/StandardMaterial"; 10 | import { Mesh } from "../Mesh"; 11 | import { Scene } from "../Scene"; 12 | 13 | import { ISurfaceMaterial } from "../materials/surface/ISurfaceMaterial"; 14 | import { WebGLExtension } from "./IExtension"; 15 | import { IProcessor } from "./IProcessor"; 16 | 17 | export class ShadowPreProcess implements IProcessor { 18 | private gl: WebGLRenderingContext; 19 | 20 | private ext: WebGLExtension; 21 | 22 | private depthMaterial: LinearDepthPackMaterial; 23 | 24 | private blurMaterial: PCSSFilteringMaterial; 25 | 26 | private rectMesh: Mesh; 27 | 28 | constructor(gl: WebGLRenderingContext, ext: WebGLExtension, scene: Scene) { 29 | this.gl = gl; 30 | this.ext = ext; 31 | 32 | this.depthMaterial = new LinearDepthPackMaterial(gl); 33 | 34 | this.blurMaterial = new PCSSFilteringMaterial(gl); 35 | this.blurMaterial.shader.setViewPort({ 36 | x: 0, 37 | y: 0, 38 | width: 512, 39 | height: 512, 40 | }); 41 | 42 | this.rectMesh = new Mesh(new RectGeometry(gl).build(), []); 43 | this.rectMesh.geometry.resetLightShadows(gl); 44 | } 45 | 46 | public process(scene: Scene, camera: Camera, matriels: IMaterial[]) { 47 | for (const light of scene.lights) { 48 | if (light.shadowLevel > ShadowLevel.None) { 49 | this.depthMaterial.shader.setViewPort({ 50 | x: 0, 51 | y: 0, 52 | width: light.shadowSize, 53 | height: light.shadowSize, 54 | }); 55 | this.renderDepth(scene, light); 56 | } 57 | if (light.shadowLevel > ShadowLevel.Hard) { 58 | this.blurMaterial.shader.setViewPort({ 59 | x: 0, 60 | y: 0, 61 | width: light.shadowSize, 62 | height: light.shadowSize, 63 | }); 64 | this.prefiltDepth(scene, light); 65 | } 66 | } 67 | 68 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); 69 | } 70 | 71 | private renderDepth(scene: Scene, light: Light) { 72 | light.shadowFrameBuffers.forEach(() => { 73 | light.clearShadowFrameBuffer(); 74 | for (const object of scene.objects) { 75 | if (object instanceof Mesh) { 76 | let blockShadow = false; 77 | for (const material of object.materials) { 78 | if ( 79 | material instanceof StandardMaterial || 80 | material instanceof BlinnPhongMaterial 81 | ) { 82 | if ((material as ISurfaceMaterial).blockShadow) { 83 | blockShadow = true; 84 | break; 85 | } 86 | } 87 | } 88 | if (blockShadow) { 89 | this.gl.useProgram( 90 | this.depthMaterial.shader.webGlProgram, 91 | ); 92 | light.drawWithLightCamera({ 93 | mesh: object, 94 | material: this.depthMaterial, 95 | }); 96 | } 97 | } 98 | } 99 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); 100 | }); 101 | } 102 | 103 | private prefiltDepth(scene: Scene, light: Light) { 104 | light.shadowFrameBuffers.forEach((shadowFrameBuffer) => { 105 | this.blurMaterial.origin = 106 | shadowFrameBuffer.active.attachments.color.targetTexture; 107 | shadowFrameBuffer.swap(); 108 | light.clearShadowFrameBuffer(); 109 | this.blurMaterial.blurStep = 1.0 / light.shadowSize; 110 | this.gl.useProgram(this.blurMaterial.shader.webGlProgram); 111 | light.drawWithLightCamera({ 112 | mesh: this.rectMesh, 113 | material: this.blurMaterial, 114 | }); 115 | shadowFrameBuffer.swap(); 116 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/lights/SpotLight.ts: -------------------------------------------------------------------------------- 1 | import { mat4, quat, vec3 } from "gl-matrix"; 2 | import { Camera } from "../cameras/Camera"; 3 | import { PerspectiveCamera } from "../cameras/PerspectiveCamera"; 4 | import { DataType } from "../DataTypeEnum"; 5 | import { uniform } from "../Decorators"; 6 | import { BoundingBox2D } from "../Intersections/BoundingBox"; 7 | import { FrameBuffer } from "../renderer/FrameBuffer"; 8 | import { Renderer } from "../renderer/Renderer"; 9 | import { ProcessingFrameBuffer } from "../renderer/SwapFramebuffer"; 10 | import { Texture } from "../textures/Texture"; 11 | import { encodeNormal } from "../Util"; 12 | import { DampingLight } from "./DampingLight"; 13 | 14 | export class SpotLight extends DampingLight { 15 | protected _coneAngle: number; 16 | 17 | protected _shadowFrameBuffer: ProcessingFrameBuffer; 18 | 19 | constructor(renderer: Renderer) { 20 | super(renderer); 21 | this.setConeAngle(Math.PI / 8); 22 | this.setRadius(100); 23 | } 24 | 25 | public get shadowMap() { 26 | return this._shadowFrameBuffer.active.attachments.color.targetTexture; 27 | } 28 | 29 | public get shadowFrameBuffer(): ProcessingFrameBuffer { 30 | return this._shadowFrameBuffer; 31 | } 32 | 33 | public get shadowFrameBuffers() { 34 | return [this._shadowFrameBuffer]; 35 | } 36 | 37 | @uniform(DataType.vec3, "spotDir") 38 | public get spotDirection() { 39 | const dir = vec3.transformQuat( 40 | vec3.create(), 41 | vec3.fromValues(0, 0, -1), 42 | mat4.getRotation(quat.create(), this._matrix), 43 | ); 44 | vec3.normalize(dir, dir); 45 | return dir; 46 | } 47 | 48 | public get coneAngle() { 49 | return this._coneAngle; 50 | } 51 | 52 | public getDeferredInfo(layer: number, camera: Camera) { 53 | switch (layer) { 54 | case 0: 55 | super.getDeferredInfo(layer, camera); 56 | case 1: 57 | const dir = this.spotDirection; 58 | const codeDir = encodeNormal(dir); 59 | return [codeDir[0], codeDir[1], codeDir[2], this._coneAngle]; 60 | default: 61 | throw Error("deferred Info " + layer + " undifined"); 62 | } 63 | } 64 | 65 | @uniform(DataType.float) 66 | protected get coneAngleCos(): number { 67 | return Math.cos(this._coneAngle); 68 | } 69 | 70 | public setRadius(radius: number) { 71 | this._radius = radius; 72 | return this; 73 | } 74 | 75 | public setConeAngle(coneAngle: number) { 76 | console.assert(coneAngle > 0, "coneAngle should be greater than 0!"); 77 | this._coneAngle = coneAngle; 78 | (this._projectCamera as PerspectiveCamera).setFovy(coneAngle * 2); 79 | return this; 80 | } 81 | 82 | public setSpotDirection(spotDirection: vec3) { 83 | const lookPoint = vec3.add(vec3.create(), this.position, spotDirection); 84 | this.lookAt(lookPoint); 85 | return this; 86 | } 87 | 88 | public setShadowSize(_size: number) { 89 | super.setShadowSize(_size); 90 | if (this._shadowFrameBuffer !== null) { 91 | this._shadowFrameBuffer 92 | .setWidth(_size) 93 | .setHeight(_size) 94 | .attach(this.gl); 95 | } 96 | return this; 97 | } 98 | 99 | public getProjecttionBoundingBox2D(camera: Camera): BoundingBox2D { 100 | // TODO: implements bounding box for spot light. 101 | console.error("function getProjecttionBoundingBox2D has not been init"); 102 | return { 103 | left: -1, 104 | right: 1, 105 | top: 1, 106 | bottom: -1, 107 | }; 108 | } 109 | 110 | public init(render: Renderer) { 111 | if (!this._shadowFrameBuffer) { 112 | this._shadowFrameBuffer = new ProcessingFrameBuffer(this.gl).onInit( 113 | (frameBuffer: FrameBuffer) => { 114 | frameBuffer.setWidth(this._shadowSize).setHeight(this._shadowSize); 115 | frameBuffer.attachments.color.targetTexture 116 | .setType(this.gl.FLOAT) 117 | .setFormat(this.gl.RGBA) 118 | .setMinFilter(this.gl.NEAREST) 119 | .setMagFilter(this.gl.NEAREST) 120 | .setWrapS(this.gl.REPEAT) 121 | .setWrapT(this.gl.REPEAT) 122 | .apply(this.gl); 123 | frameBuffer.attach(this.gl); 124 | }, 125 | ); 126 | } 127 | this._projectCamera = new PerspectiveCamera() 128 | .setParent(this) 129 | .setLocalPosition(vec3.create()) 130 | .setAspectRadio(1); 131 | return this; 132 | } 133 | 134 | public clearShadowFrameBuffer() { 135 | this.gl.bindFramebuffer( 136 | this.gl.FRAMEBUFFER, 137 | this._shadowFrameBuffer.active.glFramebuffer, 138 | ); 139 | this.gl.enable(this.gl.DEPTH_TEST); 140 | this.gl.depthFunc(this.gl.LEQUAL); 141 | this.gl.clearColor(this.far, 0, 0, 0); 142 | this.gl.clear(this.gl.DEPTH_BUFFER_BIT | this.gl.COLOR_BUFFER_BIT); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Scene.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | import { DataType } from "./DataTypeEnum"; 3 | import { arrayOfStructures, textureArray, uniform } from "./Decorators"; 4 | import { IDirtyable } from "./Dirtyable"; 5 | import { DirectionalLight } from "./lights/DirectionalLight"; 6 | import { Light } from "./lights/Light"; 7 | import { PointLight } from "./lights/PointLight"; 8 | import { ShadowLevel } from "./lights/ShadowLevel"; 9 | import { SpotLight } from "./lights/SpotLight"; 10 | import { Object3d } from "./Object3d"; 11 | import { Texture } from "./textures/Texture"; 12 | 13 | export class Scene implements IDirtyable { 14 | // TODO: optimize objects storage management; 15 | public objects: Object3d[] = []; 16 | 17 | public lights: Light[] = []; 18 | 19 | @uniform(DataType.vec3, "ambient") 20 | public ambientLight: vec3 = vec3.fromValues(0.2, 0.2, 0.2); 21 | 22 | public openLight: boolean = false; 23 | 24 | public clearColor: number[] = [0, 0, 0, 0]; 25 | 26 | public programSetUp: boolean = false; 27 | 28 | @arrayOfStructures() 29 | public directLights: DirectionalLight[] = []; 30 | 31 | @arrayOfStructures() 32 | public pointLights: PointLight[] = []; 33 | 34 | @arrayOfStructures() 35 | public spotLights: SpotLight[] = []; 36 | 37 | private updateEvents: Array<(deltaTime?: number) => void> = []; 38 | 39 | private _directLightShadowMap: Texture[] = []; 40 | private _spotLightShadowMap: Texture[] = []; 41 | private _pointLightShadowMap: Texture[] = []; 42 | 43 | private _directShadowDirty = true; 44 | private _pointShadowDirty = true; 45 | private _spotShadowDirty = true; 46 | 47 | @textureArray() 48 | public get directLightShadowMap() { 49 | this.resetLightShadows(); 50 | return this._directLightShadowMap; 51 | } 52 | 53 | @textureArray() 54 | public get spotLightShadowMap() { 55 | this.resetLightShadows(); 56 | return this._spotLightShadowMap; 57 | } 58 | 59 | @textureArray() 60 | public get pointLightShadowMap() { 61 | this.resetLightShadows(); 62 | return this._pointLightShadowMap; 63 | } 64 | 65 | public resetLightShadows() { 66 | if (this._directShadowDirty) { 67 | this._directLightShadowMap = this.directLights 68 | .filter((light) => light.shadowLevel > ShadowLevel.None) 69 | .map((light) => light.shadowMap); 70 | 71 | this._directShadowDirty = false; 72 | } 73 | if (this._spotShadowDirty) { 74 | this._spotLightShadowMap = this.spotLights 75 | .filter((light) => light.shadowLevel > ShadowLevel.None) 76 | .map((light) => light.shadowMap); 77 | this._spotShadowDirty = false; 78 | } 79 | if (this._pointShadowDirty) { 80 | this._pointLightShadowMap = this.pointLights 81 | .filter((light) => light.shadowLevel > ShadowLevel.None) 82 | .map((light) => light.shadowMap); 83 | this._pointShadowDirty = false; 84 | } 85 | } 86 | 87 | public update(dt: number) { 88 | for (const event of this.updateEvents) { 89 | if (!!event) { 90 | event(dt); 91 | } 92 | } 93 | } 94 | 95 | public addOnUpdateListener(listener: (deltaTime: number) => void) { 96 | this.updateEvents.push(listener); 97 | return this; 98 | } 99 | 100 | public removeOnUpdateListener(listener: (deltaTime: number) => void) { 101 | const index = this.updateEvents.indexOf(listener); 102 | if (index !== -1) { 103 | // lazy delete 104 | this.updateEvents[index] = undefined; 105 | } 106 | return this; 107 | } 108 | 109 | public addObject(...objects: Object3d[]) { 110 | for (const object of objects) { 111 | if (this.objects.indexOf(object) === -1) { 112 | this.objects.push(object); 113 | object.scene = this; 114 | object.children.forEach((child) => { 115 | this.addObject(child); 116 | }); 117 | } 118 | } 119 | return this; 120 | } 121 | 122 | public removeObject(object: Object3d) { 123 | object.children.forEach((child) => { 124 | this.removeObject(child); 125 | }); 126 | this.objects.splice(this.objects.indexOf(object)); 127 | return this; 128 | } 129 | 130 | public addLight(...lights: Light[]) { 131 | this.openLight = true; 132 | const addonDirect = lights.filter( 133 | (light) => light instanceof DirectionalLight, 134 | ) as DirectionalLight[]; 135 | this.directLights = this.directLights.concat(addonDirect); 136 | this._directShadowDirty = addonDirect.length > 0; 137 | const addonPoint = lights.filter( 138 | (light) => 139 | light instanceof PointLight && !(light instanceof SpotLight), 140 | ) as PointLight[]; 141 | this.pointLights = this.pointLights.concat(addonPoint); 142 | this._pointShadowDirty = addonPoint.length > 0; 143 | const addonSpot = lights.filter( 144 | (light) => light instanceof SpotLight, 145 | ) as SpotLight[]; 146 | this.spotLights = this.spotLights.concat(addonSpot); 147 | this._spotShadowDirty = addonSpot.length > 0; 148 | this.lights = this.lights.concat(lights); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/renderer/FrameBuffer.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from "../textures/Texture"; 2 | 3 | import { Graphics } from "./GraphicsUtils"; 4 | 5 | export enum AttachmentType { 6 | Texture, 7 | RenderBuffer, 8 | } 9 | 10 | export class Attachment { 11 | public readonly frameBuffer: FrameBuffer; 12 | public glRenderBuffer: WebGLRenderbuffer; 13 | public targetTexture: Texture; 14 | public textureTargetCode: number; 15 | public readonly attachmentCode: (gl: WebGLRenderingContext | WebGLDrawBuffers) => number; 16 | 17 | private _innerFormatForBuffer: number = -1; 18 | private _type: AttachmentType; 19 | private _isAble = true; 20 | 21 | constructor( 22 | frameBuffer: FrameBuffer, 23 | attachmentCode: (gl: WebGLRenderingContext | WebGLDrawBuffers) => number, 24 | ) { 25 | this.frameBuffer = frameBuffer; 26 | this.attachmentCode = attachmentCode; 27 | } 28 | 29 | public get innerFormatForBuffer() { 30 | return this._innerFormatForBuffer; 31 | } 32 | 33 | public get type() { 34 | return this._type; 35 | } 36 | 37 | public get isAble() { 38 | return this._isAble; 39 | } 40 | 41 | public enable() { 42 | this._isAble = true; 43 | return this; 44 | } 45 | 46 | public disable() { 47 | this._isAble = false; 48 | return this; 49 | } 50 | 51 | public setInnerFormatForBuffer(innerFormatForBuffer: number) { 52 | this._innerFormatForBuffer = innerFormatForBuffer; 53 | return this; 54 | } 55 | 56 | public asRenderBuffer(gl: WebGLRenderingContext) { 57 | this._type = AttachmentType.RenderBuffer; 58 | this.glRenderBuffer = gl.createRenderbuffer(); 59 | this.targetTexture = null; 60 | return this; 61 | } 62 | 63 | public asTargetTexture(texture: Texture, targetcode) { 64 | this._type = AttachmentType.Texture; 65 | this.targetTexture = texture; 66 | this.textureTargetCode = targetcode; 67 | this.glRenderBuffer = null; 68 | return this; 69 | } 70 | } 71 | 72 | export class FrameBuffer { 73 | 74 | public glFramebuffer: WebGLFramebuffer; 75 | 76 | public attachments = { 77 | color: new Attachment(this, (gl: WebGLRenderingContext) => gl.COLOR_ATTACHMENT0), 78 | depth: new Attachment(this, (gl: WebGLRenderingContext) => gl.DEPTH_ATTACHMENT), 79 | stencil: new Attachment(this, (gl: WebGLRenderingContext) => gl.STENCIL_ATTACHMENT), 80 | }; 81 | 82 | public extras: Attachment[] = []; 83 | 84 | private _attached = false; 85 | 86 | private _width: number; 87 | private _height: number; 88 | 89 | constructor(gl: WebGLRenderingContext) { 90 | this.glFramebuffer = gl.createFramebuffer(); 91 | this._width = gl.canvas.width; 92 | this._height = gl.canvas.height; 93 | this.attachments.color.asTargetTexture(new Texture(gl), gl.TEXTURE_2D) 94 | .setInnerFormatForBuffer(gl.RGBA4); 95 | this.attachments.depth.asRenderBuffer(gl) 96 | .setInnerFormatForBuffer(gl.DEPTH_COMPONENT16); 97 | this.attachments.stencil.asRenderBuffer(gl) 98 | .setInnerFormatForBuffer(gl.STENCIL_INDEX8) 99 | .disable(); 100 | } 101 | 102 | public setWidth(_width: number) { 103 | this._width = _width; 104 | return this; 105 | } 106 | 107 | public setHeight(_height: number) { 108 | this._height = _height; 109 | return this; 110 | } 111 | 112 | public get attached(): boolean { 113 | return this._attached; 114 | } 115 | 116 | public get width() { 117 | return this._width; 118 | } 119 | 120 | public get height() { 121 | return this._height; 122 | } 123 | 124 | // make all attachment settings take effect. 125 | public attach(gl: WebGLRenderingContext, drawBuffer?: WebGLDrawBuffers) { 126 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.glFramebuffer); 127 | for (const index in this.attachments) { 128 | const attachment: Attachment = this.attachments[index]; 129 | if (attachment.isAble) { 130 | this.linkAttachment(attachment, gl, gl); 131 | } 132 | } 133 | if (!!drawBuffer) { 134 | let i = 0; 135 | const drawBuffers = []; 136 | for (const attachment of this.extras) { 137 | this.linkAttachment(attachment, gl, drawBuffer); 138 | drawBuffers.push(drawBuffer.COLOR_ATTACHMENT0_WEBGL + i); 139 | i++; 140 | } 141 | drawBuffer.drawBuffersWEBGL(drawBuffers); 142 | } 143 | this._attached = Graphics.logIfFrameBufferInvalid(gl, this.glFramebuffer); 144 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 145 | } 146 | 147 | private linkAttachment( 148 | attachment: Attachment, 149 | gl: WebGLRenderingContext, 150 | context: WebGLRenderingContext | WebGLDrawBuffers) { 151 | switch (attachment.type) { 152 | case AttachmentType.Texture: 153 | attachment.targetTexture.applyForRendering(gl, this.width, this.height); 154 | gl.framebufferTexture2D( 155 | gl.FRAMEBUFFER, 156 | attachment.attachmentCode(context), 157 | attachment.textureTargetCode, 158 | attachment.targetTexture.glTexture, 159 | 0); 160 | break; 161 | case AttachmentType.RenderBuffer: 162 | gl.bindRenderbuffer(gl.RENDERBUFFER, attachment.glRenderBuffer); 163 | gl.renderbufferStorage( 164 | gl.RENDERBUFFER, 165 | attachment.innerFormatForBuffer, 166 | this.width, 167 | this.height, 168 | ); 169 | gl.framebufferRenderbuffer( 170 | gl.FRAMEBUFFER, 171 | attachment.attachmentCode(gl), 172 | gl.RENDERBUFFER, 173 | attachment.glRenderBuffer, 174 | ); 175 | break; 176 | default: break; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/geometries/Geometry.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "../DataTypeEnum"; 2 | import { IDirtyable } from "../Dirtyable"; 3 | import { Graphics } from "../renderer/GraphicsUtils"; 4 | import { Attribute } from "../shader/Attibute"; 5 | 6 | export class Faces { 7 | public buffer: WebGLBuffer | null; 8 | public data: number[] = []; 9 | constructor(gl: WebGLRenderingContext, data: number[]) { 10 | this.data = data; 11 | this.buffer = gl.createBuffer(); 12 | } 13 | } 14 | 15 | export class Geometry implements IDirtyable { 16 | public attributes: { 17 | [index: string]: Attribute; 18 | }; 19 | 20 | public faces: Faces; 21 | 22 | protected _dirty = true; 23 | 24 | protected gl: WebGLRenderingContext; 25 | 26 | constructor(gl: WebGLRenderingContext) { 27 | this.gl = gl; 28 | this.attributes = { 29 | position: new Attribute(gl, { 30 | type: DataType.float, 31 | size: 3, 32 | data: [], 33 | }), 34 | aMainUV: new Attribute(gl, { 35 | type: DataType.float, 36 | size: 2, 37 | data: [], 38 | }), 39 | aNormal: new Attribute(gl, { 40 | type: DataType.float, 41 | size: 3, 42 | data: [], 43 | }), 44 | flatNormal: new Attribute(gl, { 45 | type: DataType.float, 46 | size: 3, 47 | data: [], 48 | }), 49 | }; 50 | this.faces = { data: [], buffer: gl.createBuffer() }; 51 | } 52 | 53 | public build() { 54 | // empty here 55 | return this; 56 | } 57 | 58 | public assertValid() { 59 | let maxIndex = 0; 60 | for (const index of this.faces.data) { 61 | maxIndex = Math.max(maxIndex, index); 62 | } 63 | for (const attributeName in this.attributes) { 64 | console.assert( 65 | this.attributes[attributeName].size <= 4 && 66 | this.attributes[attributeName].size >= 1, 67 | attributeName + 68 | "size error, now: " + 69 | this.attributes[attributeName].size + 70 | " should be 1-4", 71 | ); 72 | console.assert( 73 | (maxIndex + 1) * this.attributes[attributeName].stride <= 74 | this.attributes[attributeName].data.length, 75 | attributeName + 76 | " length error, now:" + 77 | this.attributes[attributeName].data.length + 78 | ", should bigger than:" + 79 | (maxIndex + 1) * this.attributes[attributeName].stride, 80 | ); 81 | } 82 | return this; 83 | } 84 | 85 | public setAttribute(name, attribute: Attribute) { 86 | this.attributes[name] = attribute; 87 | return this; 88 | } 89 | 90 | public addVertex(vertex: any) { 91 | for (const attributeName in this.attributes) { 92 | if (this.attributes[attributeName] !== undefined) { 93 | if (vertex[attributeName] === undefined) { 94 | continue; 95 | } 96 | if ( 97 | vertex[attributeName].length !== 98 | this.attributes[attributeName].size 99 | ) { 100 | console.error("length " + attributeName + "wrong"); 101 | continue; 102 | } 103 | for (const comp of vertex[attributeName]) { 104 | this.attributes[attributeName].data.push(comp); 105 | } 106 | } 107 | } 108 | return this; 109 | } 110 | 111 | public removeAttribute(name: string) { 112 | this.attributes[name] = undefined; 113 | return this; 114 | } 115 | 116 | public getVertexByIndex(index: number) { 117 | const vertex: any = {}; 118 | for (const attributeName in this.attributes) { 119 | vertex[attributeName] = []; 120 | for (let i = 0; i < this.attributes[attributeName].stride; ++i) { 121 | vertex[attributeName].push( 122 | this.attributes[attributeName].data[ 123 | this.attributes[attributeName].stride * index + i 124 | ], 125 | ); 126 | } 127 | } 128 | return vertex; 129 | } 130 | 131 | public getTriangleByIndex(triangleIndex: number) { 132 | return [ 133 | this.getVertexByIndex(triangleIndex * 3), 134 | this.getVertexByIndex(triangleIndex * 3 + 1), 135 | this.getVertexByIndex(triangleIndex * 3 + 2), 136 | ]; 137 | } 138 | 139 | public generateFlatNormal() { 140 | for (let i = 0; i < this.faces.data.length; i += 3) { 141 | const triangle = this.getTriangleByIndex(i / 3); 142 | const flatX = 143 | (triangle[0].normals[0] + 144 | triangle[0].normals[1] + 145 | triangle[0].normals[2]) / 146 | 3; 147 | const flatY = 148 | (triangle[1].normals[0] + 149 | triangle[1].normals[1] + 150 | triangle[1].normals[2]) / 151 | 3; 152 | const flatZ = 153 | (triangle[2].normals[0] + 154 | triangle[0].normals[1] + 155 | triangle[2].normals[2]) / 156 | 3; 157 | 158 | const flat = [ 159 | flatX, 160 | flatY, 161 | flatZ, 162 | flatX, 163 | flatY, 164 | flatZ, 165 | flatX, 166 | flatY, 167 | flatZ, 168 | ]; 169 | this.attributes.flatNormal.data = this.attributes.flatNormal.data.concat( 170 | flat, 171 | ); 172 | } 173 | return this; 174 | } 175 | 176 | public resetLightShadows(gl: WebGLRenderingContext) { 177 | if (this._dirty) { 178 | Graphics.copyDataToVertexBuffer(gl, this); 179 | } 180 | this._dirty = false; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/renderer/GraphicsUtils.ts: -------------------------------------------------------------------------------- 1 | import { RENDER_PARAM_HOLDER } from "../Decorators"; 2 | import { Geometry } from "../geometries/Geometry"; 3 | import { Attribute } from "../shader/Attibute"; 4 | import { IRenderParamHolder } from "../shader/Program"; 5 | 6 | export namespace Graphics { 7 | 8 | enum ShaderType { 9 | VertexShader, 10 | FragmentShader, 11 | } 12 | 13 | export function getRenderParamHost(obj: any, logError: boolean = false) { 14 | const holder: IRenderParamHolder = obj[RENDER_PARAM_HOLDER]; 15 | if (holder === undefined) { 16 | if (logError) { 17 | console.log(obj); 18 | throw new Error(`${obj} has no renderParam property`); 19 | } else { 20 | return undefined; 21 | } 22 | } 23 | holder.hostObject = obj; 24 | return holder; 25 | } 26 | 27 | export function copyDataToVertexBuffer(gl: WebGLRenderingContext, geometry: Geometry) { 28 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, geometry.faces.buffer); 29 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, 30 | new Uint16Array(geometry.faces.data), gl.STATIC_DRAW); 31 | for (const name in geometry.attributes) { 32 | const attribute: Attribute = geometry.attributes[name]; 33 | if (attribute !== undefined) { 34 | gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer); 35 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attribute.data), gl.STATIC_DRAW); 36 | } 37 | } 38 | } 39 | 40 | export function logEnabledAttribute(gl: WebGLRenderingContext, program: WebGLProgram) { 41 | for (let i = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES) - 1; i >= 0; i--) { 42 | console.dir(gl.getActiveAttrib(program, i)); 43 | } 44 | } 45 | 46 | export function logIfFrameBufferInvalid( 47 | gl: WebGLRenderingContext, 48 | frameBuffer: WebGLFramebuffer, 49 | ) { 50 | let valid = false; 51 | gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); 52 | const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 53 | switch (status) { 54 | case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 55 | console.error( 56 | `gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: The attachment types are mismatched 57 | or not all framebuffer attachment points are framebuffer attachment complete.`, 58 | ); 59 | break; 60 | case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 61 | console.error( 62 | `gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: There is no attachment.`, 63 | ); 64 | break; 65 | case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 66 | console.error( 67 | `gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: Height and width of the attachment are not the same.`, 68 | ); 69 | break; 70 | case gl.FRAMEBUFFER_UNSUPPORTED: 71 | console.error( 72 | `gl.FRAMEBUFFER_UNSUPPORTED: The format of the attachment is not supported, 73 | or if depth and stencil attachments are not the same renderbuffer.`, 74 | ); 75 | break; 76 | default: 77 | valid = true; 78 | break; 79 | } 80 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 81 | return valid; 82 | } 83 | 84 | export function initWebwebglContext(canvas, debug?: boolean): WebGLRenderingContext { 85 | let gl; 86 | try { 87 | gl = canvas.getContext("experimental-webgl"); 88 | } catch (e) { 89 | gl = canvas.getContext("webgl"); 90 | } 91 | if (!gl) { 92 | alert("Cannot init webgl, current browser may not support it."); 93 | } 94 | return gl; 95 | } 96 | 97 | function createSeparatedShader(gl: WebGLRenderingContext, source: string, type: ShaderType): WebGLShader { 98 | 99 | let shader: WebGLShader; 100 | 101 | let typeInfo; 102 | 103 | if (type === ShaderType.FragmentShader) { 104 | shader = gl.createShader(gl.FRAGMENT_SHADER); 105 | typeInfo = "fragment shader"; 106 | } else if (type === ShaderType.VertexShader) { 107 | shader = gl.createShader(gl.VERTEX_SHADER); 108 | typeInfo = "vertex shader"; 109 | } 110 | gl.shaderSource(shader, source); 111 | 112 | // Compile the shader program 113 | 114 | gl.compileShader(shader); 115 | 116 | // See if it compiled successfully 117 | 118 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 119 | console.error("error: " + typeInfo + "\n" + gl.getShaderInfoLog(shader)); 120 | console.error(source); 121 | return null; 122 | } 123 | 124 | return shader; 125 | } 126 | 127 | function linkShader( 128 | gl: WebGLRenderingContext, 129 | vertexShader: WebGLShader, 130 | fragmentShader: WebGLShader, 131 | vertexSource: string, 132 | fragmentSource: string, 133 | ): WebGLProgram { 134 | const shaderProgram = gl.createProgram(); 135 | gl.attachShader(shaderProgram, vertexShader); 136 | gl.attachShader(shaderProgram, fragmentShader); 137 | gl.linkProgram(shaderProgram); 138 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 139 | console.error("error: link shader program failed.\n" + gl.getProgramInfoLog(shaderProgram)); 140 | console.error("vertex:\n" + vertexSource); 141 | console.error("fragment:\n" + fragmentSource); 142 | } 143 | return shaderProgram; 144 | } 145 | 146 | export function createEntileShader( 147 | gl: WebGLRenderingContext, vertexShaderSource: string, 148 | fragmentShaderSource: string, 149 | ): WebGLProgram { 150 | const vertShader = createSeparatedShader(gl, vertexShaderSource, ShaderType.VertexShader); 151 | const fragShader = createSeparatedShader(gl, fragmentShaderSource, ShaderType.FragmentShader); 152 | return linkShader(gl, vertShader, fragShader, vertexShaderSource, fragmentShaderSource); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/materials/surface/ISurfaceMaterial.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { DataType } from "../../DataTypeEnum"; 4 | import { 5 | define, 6 | readyRequire, 7 | structure, 8 | texture, 9 | uniform, 10 | } from "../../Decorators"; 11 | import { Program, shaderPassLib } from "../../shader/Program"; 12 | import { ShaderBuilder } from "../../shader/ShaderBuilder"; 13 | import { ShaderSource } from "../../shader/shaders"; 14 | import { CubeTexture } from "../../textures/CubeTexture"; 15 | import { Texture } from "../../textures/Texture"; 16 | import { Texture2D } from "../../textures/Texture2D"; 17 | import { IMaterial } from "../Material"; 18 | 19 | @structure("uMaterial") 20 | export class ISurfaceMaterial extends IMaterial { 21 | protected _geometryShader: Program; 22 | 23 | @define("_DEBUG") 24 | protected _debug: boolean = false; 25 | 26 | protected _blockShadow: boolean = true; 27 | 28 | @define("RECEIVE_SHADOW", true) 29 | protected _receiveShadow: boolean = true; 30 | 31 | protected _ambient: vec3 = vec3.fromValues(0.1, 0.1, 0.1); 32 | 33 | @define("_MAIN_TEXTURE") 34 | @texture("uMainTexture") 35 | protected _mainTexture: Texture2D; 36 | 37 | protected _transparency: number = 0; 38 | 39 | // @texture("alphaTexture") 40 | protected _alphaMap: Texture; 41 | 42 | @readyRequire 43 | protected _bumpMap: Texture; 44 | 45 | @readyRequire 46 | protected _displamentMap: Texture; 47 | 48 | @readyRequire 49 | protected _stencilMap: Texture; 50 | 51 | @uniform(DataType.float, "reflectivity") 52 | protected _reflectivity: number = 1; 53 | 54 | @define("_ENVIRONMENT_MAP") 55 | @texture("uCubeTexture") 56 | protected _environmentMap: CubeTexture; 57 | 58 | public get geometryShader() { 59 | if (!this._geometryShader) { 60 | this._geometryShader = new ShaderBuilder() 61 | .resetShaderLib() 62 | .addDefinition(ShaderSource.definitions__material_pbs_glsl) 63 | .addShaderLib(ShaderSource.calculators__packFloat1x32_glsl) 64 | .setShadingVert( 65 | ShaderSource.interploters__deferred__geometry_vert, 66 | ) 67 | .setShadingFrag( 68 | ShaderSource.interploters__deferred__geometry_frag, 69 | ) 70 | .setExtraRenderParamHolder("mvp", { 71 | uniforms: { 72 | modelViewProjectionMatrix: 73 | shaderPassLib.uniforms.modelViewProjectionMatrix, 74 | normalViewMatrix: 75 | shaderPassLib.uniforms.normalViewMatrix, 76 | }, 77 | }) 78 | .build(this.gl); 79 | this._geometryShader.extensionStatements.push( 80 | "#extension GL_EXT_draw_buffers : require", 81 | ); 82 | } 83 | return this._geometryShader; 84 | } 85 | 86 | public get debugMode() { 87 | return this._debug; 88 | } 89 | 90 | public get blockShadow() { 91 | return this._blockShadow; 92 | } 93 | 94 | public get receiveShadow() { 95 | return this._receiveShadow; 96 | } 97 | 98 | public get mainTexture() { 99 | return this._mainTexture; 100 | } 101 | 102 | @uniform(DataType.vec3) 103 | public get ambient() { 104 | return this._ambient; 105 | } 106 | 107 | public get transparency() { 108 | return this._transparency; 109 | } 110 | 111 | // @texture("alphaTexture") 112 | public get alphaMap() { 113 | return this._alphaMap; 114 | } 115 | 116 | public get bumpMap() { 117 | return this._bumpMap; 118 | } 119 | 120 | public get displamentMap() { 121 | return this._displamentMap; 122 | } 123 | public get stencilMap() { 124 | return this.stencilMap; 125 | } 126 | 127 | public get environmentMap() { 128 | return this._environmentMap; 129 | } 130 | 131 | public setDebugMode(_debug: boolean) { 132 | this._debug = _debug; 133 | return this; 134 | } 135 | 136 | public setCastShadow(_castShadow: boolean) { 137 | this._blockShadow = _castShadow; 138 | return this; 139 | } 140 | 141 | public setRecieveShadow(_receiveShadow: boolean) { 142 | this._receiveShadow = _receiveShadow; 143 | return this; 144 | } 145 | 146 | public setMainTexture(_texture: Texture) { 147 | this._mainTexture = _texture; 148 | return this; 149 | } 150 | 151 | public setAmbient(_ambient: vec3) { 152 | this._ambient = _ambient; 153 | return this; 154 | } 155 | 156 | public setTransparency(_transparency: number) { 157 | console.assert(_transparency >= 0 && _transparency <= 1); 158 | this._transparency = _transparency; 159 | return this; 160 | } 161 | 162 | // @texture("alphaTexture") 163 | public setAlphaMap(_alphaMap) { 164 | this._alphaMap = _alphaMap; 165 | return this; 166 | } 167 | 168 | public setBumpMap(_bumpMap: Texture) { 169 | this._bumpMap = _bumpMap; 170 | return this; 171 | } 172 | 173 | public setDisplamentMap(_displamentMap: Texture) { 174 | this._displamentMap = _displamentMap; 175 | return this; 176 | } 177 | public setStencilMap(_stencilMap: Texture) { 178 | this._stencilMap = _stencilMap; 179 | return this; 180 | } 181 | 182 | public setReflectivity(_reflectivity: number) { 183 | this._reflectivity = _reflectivity; 184 | return this; 185 | } 186 | 187 | public setEnvironmentMap(_environmentMap: CubeTexture) { 188 | this._environmentMap = _environmentMap; 189 | return this; 190 | } 191 | 192 | protected initShader(gl: WebGLRenderingContext) { 193 | return new ShaderBuilder() 194 | .addDefinition(ShaderSource.definitions__material_pbs_glsl) 195 | .setLightModel(ShaderSource.light_model__pbs_ggx_glsl) 196 | .setExtraRenderParamHolder("mvp", { 197 | uniforms: { 198 | modelViewProjectionMatrix: 199 | shaderPassLib.uniforms.modelViewProjectionMatrix, 200 | }, 201 | }) 202 | .setExtraRenderParamHolder("pcss", { 203 | defines: shaderPassLib.defines, 204 | }) 205 | .build(gl); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/loader/obj_mtl/OBJLoader.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Geometry } from "../../geometries/Geometry"; 3 | import { StandardMaterial } from "../../materials/surface/StandardMaterial"; 4 | import { Mesh } from "../../Mesh"; 5 | import { Object3d } from "../../Object3d"; 6 | 7 | import { mixin } from "../../Util"; 8 | import { fetchRes } from "../ResourceFetcher"; 9 | import { patterns } from "./CommonPatterns"; 10 | import { MTLLoader } from "./MTLLoader"; 11 | 12 | export class OBJLoader { 13 | 14 | public static load(gl: WebGLRenderingContext, url: string): Object3d { 15 | const container: Object3d = new Object3d(); 16 | container.setAsyncFinished(fetchRes(url).then((content: string) => { 17 | // remove comment of .obj file 18 | content = content.replace(patterns.commentPattern, ""); 19 | 20 | const home = url.substr(0, url.lastIndexOf("/") + 1); 21 | 22 | const materialLibs: string[] = content.match(OBJLoader.mtlLibPattern); 23 | const materialsMixin = {}; 24 | const promises = []; 25 | 26 | if (materialLibs != null) { 27 | for (const mtlLib of materialLibs) { 28 | const mtlurl = home + mtlLib.match(OBJLoader.mtlLibSinglePattern)[1]; 29 | promises.push(MTLLoader.load(gl, mtlurl)); 30 | } 31 | } 32 | return Promise.all(promises).then((mtls) => { 33 | for (const materials of mtls) { 34 | mixin(materialsMixin, materials); 35 | } 36 | const positionlines: string[] = content.match(OBJLoader.vertexPattern); 37 | const uvlines: string[] = content.match(OBJLoader.uvPattern); 38 | const normallines: string[] = content.match(OBJLoader.normalPattern); 39 | const unIndexedPositions = OBJLoader.praiseAttibuteLines(positionlines); 40 | const unIndexedUVs = OBJLoader.praiseAttibuteLines(uvlines); 41 | const unIndexedNormals = OBJLoader.praiseAttibuteLines(normallines); 42 | OBJLoader.buildUpMeshes( 43 | gl, container, content, materialsMixin, unIndexedPositions, unIndexedUVs, unIndexedNormals); 44 | return Promise.resolve(container); 45 | }); 46 | })); 47 | return container; 48 | } 49 | 50 | protected static faceSplitVertPattern = /([0-9]|\/|\-)+/g; 51 | protected static facePerVertPattern = /([0-9]*)\/?([0-9]*)\/?([0-9]*)/; 52 | protected static objectSplitPattern = /[o|g]\s+.+/mg; 53 | protected static mtlLibPattern = /mtllib\s([^\s]+)/mg; 54 | protected static useMTLPattern = /usemtl\s([^\s]+)/mg; 55 | protected static mtlLibSinglePattern = /mtllib\s([^\s]+)/; 56 | protected static useMTLSinglePattern = /usemtl\s([^\s]+)/; 57 | protected static vertexPattern = /v\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? ?)+/mg; 58 | protected static uvPattern = /vt\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? ?)+/mg; 59 | protected static normalPattern = /vn\s+([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? ?)+/mg; 60 | protected static indexPattern = /f\s+([-+]?[0-9]*\.?[0-9]+ ?|\/)+/mg; 61 | 62 | protected static praiseAttibuteLines(lines) { 63 | const result: number[][] = []; 64 | if (lines === null) { 65 | return; 66 | } 67 | lines.forEach((expression: string) => { 68 | const data: number[] = []; 69 | expression.match(patterns.num).forEach( 70 | (floatNum) => { 71 | if (expression !== "") { 72 | data.push(parseFloat(floatNum)); 73 | } 74 | }, 75 | ); 76 | result.push(data); 77 | }); 78 | return result; 79 | } 80 | 81 | protected static parseAsTriangle(faces: string[], forEachFace: (face: string[]) => void) { 82 | for (let i = 0; i < faces.length - 2; ++i) { 83 | const triangleFace = [faces[0], faces[i + 1], faces[i + 2]]; 84 | forEachFace(triangleFace); 85 | } 86 | } 87 | 88 | protected static buildUpMeshes( 89 | gl: WebGLRenderingContext, 90 | container: Object3d, 91 | content: string, 92 | materials: any, 93 | unIndexedPositions: number[][], 94 | unIndexedUVs: number[][], 95 | unIndexedNormals: number[][], 96 | ) { 97 | const objects = content.split(OBJLoader.objectSplitPattern); 98 | objects.splice(0, 1); 99 | objects.forEach((objectContent) => { 100 | const geometry: Geometry = new Geometry(gl); 101 | const faces = objectContent.match(OBJLoader.indexPattern); 102 | if (faces !== null) { 103 | faces.forEach((faceStr) => { 104 | OBJLoader.parseAsTriangle(faceStr.match(OBJLoader.faceSplitVertPattern), (triangleFaces) => { 105 | triangleFaces.forEach((perVertData) => { 106 | const match = perVertData.match(OBJLoader.facePerVertPattern); 107 | console.assert(match !== null && match[1] !== null, "obj file error"); 108 | const positionIndex = parseInt(match[1], 0) - 1; 109 | geometry.faces.data.push(geometry.attributes.position.data.length / 3); 110 | geometry.addVertex({ 111 | position: unIndexedPositions[positionIndex], 112 | aMainUV: [unIndexedUVs[parseInt(match[2], 0) - 1][0], 113 | unIndexedUVs[parseInt(match[2], 0) - 1][1]], 114 | aNormal: unIndexedNormals[parseInt(match[3], 0) - 1], 115 | }); 116 | }); 117 | }); 118 | }); 119 | } 120 | const meshMaterials = []; 121 | const mtls = objectContent.match(OBJLoader.useMTLPattern); 122 | if (!!mtls) { 123 | mtls.forEach((useMTLLine) => { 124 | meshMaterials.push(materials[useMTLLine.match(OBJLoader.useMTLSinglePattern)[1]]); 125 | }); 126 | } else { 127 | meshMaterials.push(new StandardMaterial(gl)); 128 | } 129 | const mesh = new Mesh(geometry, meshMaterials); 130 | mesh.setParent(container); 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/loader/obj_mtl/MTLLoader.ts: -------------------------------------------------------------------------------- 1 | import { vec3 } from "gl-matrix"; 2 | 3 | import { BlinnPhongMaterial } from "../../materials/surface/BlinnPhongMaterial"; 4 | import { StandardMaterial } from "../../materials/surface/StandardMaterial"; 5 | import { Texture2D } from "../../textures/Texture2D"; 6 | import { fetchRes } from "../ResourceFetcher"; 7 | import { patterns } from "./CommonPatterns"; 8 | 9 | export class MTLLoader { 10 | public static load(gl: WebGLRenderingContext, baseurl: string) { 11 | const materials = {}; 12 | let currentMaterial: BlinnPhongMaterial | StandardMaterial = null; 13 | const home = baseurl.substr(0, baseurl.lastIndexOf("/") + 1); 14 | return fetchRes(baseurl).then((content: string) => { 15 | content = content.replace(patterns.commentPattern, ""); 16 | content 17 | .split("\n") 18 | .filter((line) => !!line) 19 | .forEach((line) => { 20 | currentMaterial = MTLLoader.handleSingleLine( 21 | gl, 22 | home, 23 | line, 24 | materials, 25 | currentMaterial, 26 | ); 27 | }); 28 | return Promise.resolve(materials); 29 | }); 30 | } 31 | 32 | protected static newmtlPattern = /newmtl\s(.+)/m; 33 | protected static ambientPattern = /Ka\s(.+)/m; 34 | protected static diffusePattern = /Kd\s(.+)/m; 35 | protected static specularePattern = /Ks\s(.+)/m; 36 | protected static specularExponentPattern = /Ns\s(.+)/m; 37 | 38 | protected static metallicPattern = /Pm\s(.+)/m; 39 | protected static roughnessPattern = /Pr\s(.+)/m; 40 | 41 | protected static mapPattern = /(map_[^\s]+|bump|disp|decal)\s.+/gm; 42 | protected static mapSinglePattern = /(map_[^\s]+|bump|disp|decal)\s([^\s]+)/m; 43 | 44 | private static handleSingleLine( 45 | gl: WebGLRenderingContext, 46 | home: string, 47 | line: string, 48 | materials: any, 49 | currentMaterial: BlinnPhongMaterial | StandardMaterial, 50 | ) { 51 | const matches = line.match(/([^\s]+)/g); 52 | if (!matches || matches.length === 0) { 53 | return; 54 | } 55 | const firstVar = matches[0]; 56 | switch (firstVar) { 57 | case "newmtl": 58 | const mtlName = line.match(MTLLoader.newmtlPattern)[1]; 59 | materials[mtlName] = new BlinnPhongMaterial(gl); 60 | materials[mtlName].name = mtlName; 61 | return materials[mtlName]; 62 | case "Ka": 63 | currentMaterial.setAmbient( 64 | MTLLoader.getVector(MTLLoader.ambientPattern, line), 65 | ); 66 | break; 67 | case "Kd": 68 | if (currentMaterial instanceof BlinnPhongMaterial) { 69 | currentMaterial.setDiffuse( 70 | MTLLoader.getVector(MTLLoader.diffusePattern, line), 71 | ); 72 | } else if (currentMaterial instanceof StandardMaterial) { 73 | currentMaterial.setAlbedo( 74 | MTLLoader.getVector(MTLLoader.diffusePattern, line), 75 | ); 76 | } 77 | break; 78 | case "Ks": 79 | if (currentMaterial instanceof BlinnPhongMaterial) { 80 | currentMaterial.setSpecular( 81 | MTLLoader.getVector(MTLLoader.specularePattern, line), 82 | ); 83 | } 84 | break; 85 | case "Ns": 86 | if (currentMaterial instanceof BlinnPhongMaterial) { 87 | currentMaterial.setSpecularExponent( 88 | MTLLoader.getNumber(MTLLoader.specularExponentPattern, line), 89 | ); 90 | } 91 | break; 92 | case "map_Ka": 93 | currentMaterial.setMainTexture( 94 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 95 | ); 96 | break; 97 | case "map_Kd": 98 | currentMaterial.setMainTexture( 99 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 100 | ); 101 | break; 102 | case "map_d": 103 | currentMaterial.setAlphaMap( 104 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 105 | ); 106 | break; 107 | case "map_bump": 108 | currentMaterial.setBumpMap( 109 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 110 | ); 111 | break; 112 | case "bump": 113 | currentMaterial.setBumpMap( 114 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 115 | ); 116 | break; 117 | case "disp": 118 | currentMaterial.setDisplamentMap( 119 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 120 | ); 121 | break; 122 | case "decal": 123 | currentMaterial.setStencilMap( 124 | new Texture2D(gl, home + MTLLoader.getImageUrl(line)), 125 | ); 126 | break; 127 | case "Pr": 128 | if (currentMaterial instanceof BlinnPhongMaterial) { 129 | currentMaterial = StandardMaterial.fromLaggard(gl, currentMaterial); 130 | materials[currentMaterial.name] = currentMaterial; 131 | } 132 | currentMaterial.setRoughness( 133 | MTLLoader.getNumber(MTLLoader.roughnessPattern, line), 134 | ); 135 | break; 136 | case "Pm": 137 | if (currentMaterial instanceof BlinnPhongMaterial) { 138 | currentMaterial = StandardMaterial.fromLaggard(gl, currentMaterial); 139 | materials[currentMaterial.name] = currentMaterial; 140 | } 141 | currentMaterial.setMetallic( 142 | MTLLoader.getNumber(MTLLoader.metallicPattern, line), 143 | ); 144 | break; 145 | default: 146 | break; 147 | } 148 | return currentMaterial; 149 | } 150 | 151 | private static getImageUrl(line) { 152 | const matches = line.match(MTLLoader.mapSinglePattern); 153 | return matches[2]; 154 | } 155 | 156 | private static getVector(pattern: RegExp, line: string) { 157 | const matches = line.match(pattern); 158 | const vector = []; 159 | if (matches.length > 0) { 160 | matches[1].match(patterns.num).forEach((numStr) => { 161 | if (numStr !== "") { 162 | vector.push(parseFloat(numStr)); 163 | } 164 | }); 165 | } 166 | return vec3.fromValues(vector[0], vector[1], vector[2]); 167 | } 168 | 169 | private static getNumber(pattern: RegExp, line: string) { 170 | const matches = line.match(pattern); 171 | if (matches.length > 0) { 172 | return parseFloat(matches[1].match(patterns.num)[0]); 173 | } 174 | return 0; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Decorators.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from "./DataTypeEnum"; 2 | import { IBuildinRenderParamMaps, IRenderParamHolder } from "./shader/Program"; 3 | 4 | export const RENDER_PARAM_HOLDER = "renderParams"; 5 | 6 | function tryAddParamHolder(proto) { 7 | if (proto.hasOwnProperty(RENDER_PARAM_HOLDER)) { 8 | return; 9 | } 10 | let params: IRenderParamHolder = proto[RENDER_PARAM_HOLDER]; 11 | if (params === undefined) { 12 | params = {}; 13 | } 14 | delete proto[RENDER_PARAM_HOLDER]; 15 | Object.defineProperty(proto, RENDER_PARAM_HOLDER, { 16 | enumerable: true, 17 | configurable: false, 18 | writable: false, 19 | value: params, 20 | }); 21 | } 22 | 23 | export function structure(name: string) { 24 | return (constructor) => { 25 | tryAddParamHolder(constructor.prototype); 26 | constructor.prototype[RENDER_PARAM_HOLDER].customPrefix = name + "."; 27 | }; 28 | } 29 | 30 | /** 31 | * A property decorator. Treat property as an uniform parameter for rendering. If not provided, 32 | * the name of the property will be uniform name to find in shader by default, 33 | * otherwise will use the given name 34 | */ 35 | export function uniform(type: DataType, name?: string) { 36 | return (proto, key) => { 37 | tryAddParamHolder(proto); 38 | const uniforms = proto[RENDER_PARAM_HOLDER].uniforms || {}; 39 | uniforms[key] = { name, type }; 40 | proto[RENDER_PARAM_HOLDER].uniforms = uniforms; 41 | }; 42 | } 43 | 44 | export function bindUniformGetter(name: string, type: DataType, getter: (p: IBuildinRenderParamMaps) => any) { 45 | return (constructor) => { 46 | const proto = constructor.prototype; 47 | tryAddParamHolder(proto); 48 | const uniforms = proto[RENDER_PARAM_HOLDER].uniforms || {}; 49 | uniforms[name] = { type, updator: getter }; 50 | proto[RENDER_PARAM_HOLDER].uniforms = uniforms; 51 | }; 52 | } 53 | 54 | /** 55 | * A property decorator. Treat property as an array of uniform for rendering. 56 | * The array in shader must be declared as ${name}[${name}Num], if not provided, 57 | * the name of the property will be array name to find in shader by default, otherwise will use the given name 58 | */ 59 | export function uniformArray(type: DataType, name?: string) { 60 | return (proto, key) => { 61 | tryAddParamHolder(proto); 62 | const uArray = proto[RENDER_PARAM_HOLDER].uArray || {}; 63 | uArray[key] = { name, type }; 64 | proto[RENDER_PARAM_HOLDER].uArray = uArray; 65 | }; 66 | } 67 | 68 | /** 69 | * A property decorator. Treat property as a texture parameter for rendering. If not provided, 70 | * the name of the property will be the uniform sampler name to find in shader by default, 71 | * otherwise will use the given name 72 | */ 73 | export function texture(name?: string) { 74 | return (proto, key) => { 75 | readyRequire(proto, key); 76 | tryAddParamHolder(proto); 77 | const textures = proto[RENDER_PARAM_HOLDER].textures || {}; 78 | textures[key] = { name }; 79 | proto[RENDER_PARAM_HOLDER].textures = textures; 80 | }; 81 | } 82 | 83 | /** 84 | * A property decorator. Treat property as an array of texture for rendering. The array in shader must 85 | * be declared as ${name}[${name}Num], if not provided, the name of the property will be sampler 86 | * array name to find in shader by default, otherwise will use the given name 87 | */ 88 | export function textureArray(name?: string) { 89 | return (proto, key) => { 90 | tryAddParamHolder(proto); 91 | const textureArrays = proto[RENDER_PARAM_HOLDER].textureArrays || {}; 92 | textureArrays[key] = { name }; 93 | proto[RENDER_PARAM_HOLDER].textureArrays = textureArrays; 94 | }; 95 | } 96 | 97 | /** 98 | * A property decorator. Treat property as an array of structures(AoS) for rendering. 99 | * The array in shader must be declared as ${name}[${name}Num] 100 | * the name of the property will be array name to find in shader by default, otherwise will use the given name 101 | */ 102 | export function arrayOfStructures(name?: string) { 103 | return (proto, key) => { 104 | tryAddParamHolder(proto); 105 | const structArrays = proto[RENDER_PARAM_HOLDER].structArrays || {}; 106 | structArrays[key] = { name }; 107 | proto[RENDER_PARAM_HOLDER].structArrays = structArrays; 108 | }; 109 | } 110 | 111 | /** 112 | * A property decorator to control if add define statement at the start of the shader 113 | * @param defineName name after #define 114 | * @param useValue Whether use property value as the define value after name 115 | */ 116 | export function define(defineName: string, useValue = false) { 117 | return (proto, key) => { 118 | tryAddParamHolder(proto); 119 | const defines = proto[RENDER_PARAM_HOLDER].defines || {}; 120 | defines[key] = { defineName, useValue }; 121 | proto[RENDER_PARAM_HOLDER].defines = defines; 122 | }; 123 | } 124 | 125 | /** 126 | * A property decorator to control if pass property value to shader or not, 127 | * by auto detect if the given name defined inside shader source 128 | */ 129 | export function ifdefine(defineName: string) { 130 | return (proto, key) => { 131 | tryAddParamHolder(proto); 132 | const paramFilters = (proto[RENDER_PARAM_HOLDER] as IRenderParamHolder).paramFilters || {}; 133 | paramFilters[key] = { name: defineName, filter: () => true }; 134 | proto[RENDER_PARAM_HOLDER].paramFilters = paramFilters; 135 | }; 136 | } 137 | 138 | /** 139 | * A property decorator to control if pass property value to shader or not, 140 | * by auto detect if the define value equal to the given value 141 | */ 142 | export function ifequal(defineName: string, defineValue: string) { 143 | return (proto, key) => { 144 | tryAddParamHolder(proto); 145 | const paramFilters = (proto[RENDER_PARAM_HOLDER] as IRenderParamHolder).paramFilters || {}; 146 | paramFilters[key] = { name: defineName, filter: (value) => value === defineValue }; 147 | proto[RENDER_PARAM_HOLDER].paramFilters = paramFilters; 148 | }; 149 | } 150 | 151 | /** 152 | * A property decorator to control if pass property value to shader or not, 153 | * by auto detect if the define value less to the given value 154 | */ 155 | export function ifgreat(defineName: string, defineValue: string) { 156 | return (proto, key) => { 157 | tryAddParamHolder(proto); 158 | const paramFilters = (proto[RENDER_PARAM_HOLDER] as IRenderParamHolder).paramFilters || {}; 159 | paramFilters[key] = { name: defineName, filter: (value) => value > defineValue }; 160 | proto[RENDER_PARAM_HOLDER].paramFilters = paramFilters; 161 | }; 162 | } 163 | 164 | /** 165 | * A property decorator. Mark the property as an async resource 166 | * A async resource is only available when the promise it's method asyncFinished returned resolved 167 | */ 168 | export function readyRequire(proto, key) { 169 | const asyncResources = proto.asyncResources || []; 170 | if (!proto.hasOwnProperty("asyncResources")) { 171 | delete proto.asyncResources; 172 | Object.defineProperty(proto, "asyncResources", { 173 | enumerable: true, 174 | configurable: false, 175 | writable: false, 176 | value: asyncResources, 177 | }); 178 | } 179 | asyncResources.push((obj) => { 180 | const resources = obj[key]; 181 | if (!!obj[key]) { 182 | return obj[key].asyncFinished(); 183 | } 184 | return undefined; 185 | }); 186 | proto.asyncResources = asyncResources; 187 | } 188 | --------------------------------------------------------------------------------