Plane
24 |25 |
Sandbox.Plane(width, height, widthSegments, heightSegments)
26 |├── img
└── jellyfish.jpg
├── documentation
└── assets
│ └── img
│ └── icon.png
├── src
├── shaders
│ ├── picking
│ │ ├── fragment.glsl
│ │ └── vertex.glsl
│ └── primitive
│ │ ├── fragment.glsl
│ │ └── vertex.glsl
├── js
│ ├── modules
│ │ ├── Volume.js
│ │ ├── Vector.js
│ │ ├── Geometry.js
│ │ ├── Program.js
│ │ ├── Circle.js
│ │ ├── FrameBuffer.js
│ │ ├── Tetrahedron.js
│ │ ├── Sandbox.js
│ │ ├── Plane.js
│ │ ├── Collection.js
│ │ ├── Texture.js
│ │ ├── Sphere.js
│ │ ├── Cylinder.js
│ │ ├── Orthographic.js
│ │ ├── Light.js
│ │ ├── Perspective.js
│ │ ├── ColorPicker.js
│ │ ├── Cube.js
│ │ ├── Renderer.js
│ │ ├── Mesh.js
│ │ └── Matrix.js
│ └── demo
│ │ └── main.js
└── css
│ └── main.css
├── LICENSE.md
├── README.md
├── index.html
└── dist
└── js
├── sandbox.min.js
└── demo.js
/img/jellyfish.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cherupil/Sandbox/HEAD/img/jellyfish.jpg
--------------------------------------------------------------------------------
/documentation/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cherupil/Sandbox/HEAD/documentation/assets/img/icon.png
--------------------------------------------------------------------------------
/src/shaders/picking/fragment.glsl:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | uniform sampler2D uTexture;
4 | uniform vec3 uPlaneColor;
5 | uniform float uPickingColor;
6 |
7 | varying vec2 vUV;
8 |
9 | void main() {
10 | vec4 uvs = vec4(vUV, 0.0, 1.0);
11 | gl_FragColor = vec4(1.0, 0.0, 0.0, uPickingColor);
12 | }
--------------------------------------------------------------------------------
/src/shaders/primitive/fragment.glsl:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | varying vec3 vNormal;
4 | varying vec2 vUV;
5 | varying vec3 vPos;
6 |
7 | void main() {
8 | float distance = 0.25 + (1.0 - 0.25) * (vPos.z - (-1.0)) / (1.0 - (-1.0));
9 | vec3 green = vec3(0.420, 0.831, 0.565) * distance;
10 |
11 | gl_FragColor = vec4(green, 1.0);
12 | }
--------------------------------------------------------------------------------
/src/js/modules/Volume.js:
--------------------------------------------------------------------------------
1 | export default class Volume {
2 | constructor() {
3 | this.objects = []
4 | }
5 |
6 | add(object) {
7 | this.objects.push(object)
8 | this.objects.sort((a, b) => {
9 | const bufferDiff = a.geometryID - b.geometryID
10 | if (bufferDiff) {
11 | return bufferDiff
12 | }
13 | return a.shader.id - b.shader.id
14 | })
15 | }
16 | }
--------------------------------------------------------------------------------
/src/shaders/picking/vertex.glsl:
--------------------------------------------------------------------------------
1 | attribute vec4 aPosition;
2 | attribute vec3 aNormal;
3 | attribute vec2 aUV;
4 |
5 | uniform mat4 uViewProjectionMatrix;
6 | uniform mat4 uNormalMatrix;
7 | uniform mat4 uLocalMatrix;
8 | uniform float uTime;
9 |
10 | varying vec3 vNormal;
11 | varying vec2 vUV;
12 |
13 | void main() {
14 | vec4 position = uViewProjectionMatrix * uLocalMatrix * aPosition;
15 | gl_Position = position;
16 | vNormal = aNormal;
17 | vUV = aUV;
18 | }
--------------------------------------------------------------------------------
/src/shaders/primitive/vertex.glsl:
--------------------------------------------------------------------------------
1 | attribute vec4 aPosition;
2 | attribute vec3 aNormal;
3 | attribute vec2 aUV;
4 |
5 | uniform mat4 uViewProjectionMatrix;
6 | uniform mat4 uNormalMatrix;
7 | uniform mat4 uLocalMatrix;
8 |
9 | varying vec3 vNormal;
10 | varying vec2 vUV;
11 | varying vec3 vPos;
12 |
13 | void main() {
14 | vec4 position = uViewProjectionMatrix * uLocalMatrix * aPosition;
15 | gl_Position = position;
16 | vNormal = aNormal + 0.5;
17 | vUV = aUV;
18 | vPos = aPosition.xyz;
19 | }
--------------------------------------------------------------------------------
/src/js/modules/Vector.js:
--------------------------------------------------------------------------------
1 | export default class Vector {
2 | static cross(a, b) {
3 | return [
4 | a[1] * b[2] - a[2] * b[1],
5 | a[2] * b[0] - a[0] * b[2],
6 | a[0] * b[1] - a[1] * b[0]
7 | ]
8 | }
9 |
10 | static subtract(a, b) {
11 | return [
12 | a[0] - b[0],
13 | a[1] - b[1],
14 | a[2] - b[2]
15 | ]
16 | }
17 |
18 | static normalize(v) {
19 | const magnitude = Math.sqrt((v[0]**2) + (v[1]**2) + (v[2]**2))
20 |
21 | if (magnitude > 0.00001) {
22 | return [
23 | v[0] / magnitude,
24 | v[1] / magnitude,
25 | v[2] / magnitude
26 | ]
27 | } else {
28 | return [
29 | 0,
30 | 0,
31 | 0
32 | ]
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/js/modules/Geometry.js:
--------------------------------------------------------------------------------
1 | let geometryId = 0
2 |
3 | export default class Geometry {
4 | constructor(positions) {
5 | this.id = geometryId++
6 | this.attributes = {}
7 | this.setAttribute('aPosition', new Float32Array(positions), 3)
8 | this._generateNormals(positions)
9 | }
10 |
11 | setAttribute(name, data, size) {
12 | this.attributes[name] = {
13 | name,
14 | data,
15 | size,
16 | count: data.length / size
17 | }
18 | }
19 |
20 | _generateNormals(positions) {
21 | const normals = []
22 | for (var i = 0; i < positions.length; i+=3) {
23 | const x = positions[i]
24 | const y = positions[i + 1]
25 | const z = positions[i + 2]
26 | const magnitude = Math.sqrt((x**2) + (y**2) + (z**2))
27 | normals.push((x/magnitude), (y/magnitude), (z/magnitude))
28 | }
29 | this.setAttribute('aNormal', new Float32Array(normals), 3)
30 | }
31 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Christopher Cherupil
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/js/modules/Program.js:
--------------------------------------------------------------------------------
1 |
2 | let programId = 0
3 |
4 | export default class Program {
5 | constructor(gl, vertex, fragment) {
6 | const vertexShader = this._createShader(gl, gl.VERTEX_SHADER, vertex)
7 | const fragmentShader = this._createShader(gl, gl.FRAGMENT_SHADER, fragment)
8 | this.gl = gl
9 | this.id = programId++
10 | this.program = this._createProgram(gl, vertexShader, fragmentShader)
11 | }
12 |
13 | _createShader(gl, type, source) {
14 | const shader = gl.createShader(type)
15 | gl.shaderSource(shader, source)
16 | gl.compileShader(shader)
17 | const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
18 | if (success) {
19 | return shader
20 | }
21 |
22 | console.log(gl.getShaderInfoLog(shader))
23 | gl.deleteShader(shader)
24 | }
25 |
26 | _createProgram(gl, vertexShader, fragmentShader) {
27 | const program = gl.createProgram()
28 | gl.attachShader(program, vertexShader)
29 | gl.attachShader(program, fragmentShader)
30 | gl.linkProgram(program)
31 | const success = gl.getProgramParameter(program, gl.LINK_STATUS)
32 | if (success) {
33 | return program
34 | }
35 |
36 | console.log(gl.getProgramInfoLog(program))
37 | gl.deleteProgram(program)
38 | }
39 | }
--------------------------------------------------------------------------------
/src/js/modules/Circle.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Circle extends Geometry {
4 | constructor(radius, segments) {
5 | const positions = []
6 |
7 | positions.push(0, 0, 0)
8 |
9 | for (let i = 0; i < segments; i++) {
10 | const x = Math.cos(i * Math.PI / (segments / 2)) * radius
11 | const y = Math.sin(i * Math.PI / (segments / 2)) * radius
12 | const z = 0
13 | positions.push(x, y, z)
14 | }
15 |
16 | positions.push(Math.cos(0) * radius, Math.sin(0) * radius, 0)
17 |
18 | super(positions)
19 |
20 | const normals = []
21 | for (var i = 0; i < positions.length; i+=3) {
22 | const x = positions[i]
23 | const y = positions[i + 1]
24 | const z = 1
25 | const magnitude = Math.sqrt((x**2) + (y**2) + (z**2))
26 | normals.push((x/magnitude), (y/magnitude), (z/magnitude))
27 | }
28 | this.attributes.aNormal.data = new Float32Array(normals)
29 | this.attributes.aNormal.count = normals.length / 3
30 |
31 | const uvs = []
32 | for (let i = 0; i < positions.length; i+=3) {
33 | const x = (positions[i] + radius) / (radius * 2)
34 | const y = (positions[i+1] + radius) / (radius * 2)
35 | uvs.push(x, y)
36 | }
37 | this.setAttribute('aUV', new Float32Array(uvs), 2)
38 |
39 | this.type = 'TRIANGLE_FAN'
40 | }
41 | }
--------------------------------------------------------------------------------
/src/js/modules/FrameBuffer.js:
--------------------------------------------------------------------------------
1 | export default class FrameBuffer {
2 | constructor(gl, target) {
3 | this.gl = gl
4 | this.target = target
5 |
6 | this.buffer = this.gl.createFramebuffer()
7 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.buffer)
8 |
9 | this.depthBuffer = this.gl.createRenderbuffer()
10 | this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, this.depthBuffer)
11 | this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, this.target.width, this.target.height)
12 |
13 | this.attachTexture(this.target)
14 | this.attachRenderBuffer()
15 | }
16 |
17 | resize(width, height) {
18 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.target.texture)
19 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.target.format, width, height, 0, this.target.format, this.gl.UNSIGNED_BYTE,
20 | null)
21 | this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, this.depthBuffer)
22 | this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, width, height)
23 | }
24 |
25 | attachTexture(target) {
26 | this.target = target
27 | this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, this.target.texture, 0)
28 | }
29 |
30 | attachRenderBuffer() {
31 | this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, this.depthBuffer)
32 | }
33 | }
--------------------------------------------------------------------------------
/src/js/modules/Tetrahedron.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Tetra extends Geometry {
4 | constructor(size) {
5 | const positions = []
6 |
7 | const height = (Math.sqrt(3) / 2) * size
8 |
9 | positions.push(
10 | -size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6,
11 | size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6,
12 | 0, (size * Math.sqrt(3)) / 3, 0,
13 |
14 | size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6,
15 | 0, -(size * Math.sqrt(3)) / 6, -(size * Math.sqrt(3)) / 3,
16 | 0, (size * Math.sqrt(3)) / 3, 0,
17 |
18 | 0, -(size * Math.sqrt(3)) / 6, -(size * Math.sqrt(3)) / 3,
19 | -size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6,
20 | 0, (size * Math.sqrt(3)) / 3, 0,
21 |
22 | -size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6,
23 | 0, -(size * Math.sqrt(3)) / 6, -(size * Math.sqrt(3)) / 3,
24 | size/2, -(size * Math.sqrt(3)) / 6, (size * Math.sqrt(3)) / 6
25 | )
26 |
27 | super(positions)
28 |
29 | const uvs = []
30 |
31 | for (let i = 0; i < positions.length; i+=9) {
32 | if (i === 27) {
33 | uvs.push(1, (1 - Math.sqrt(0.75)) / 2, 0.5, ((1 - Math.sqrt(0.75)) / 2) + Math.sqrt(0.75), 0, (1 - Math.sqrt(0.75)) / 2)
34 | } else {
35 | uvs.push(0, (1 - Math.sqrt(0.75)) / 2, 1, (1 - Math.sqrt(0.75)) / 2, 0.5, ((1 - Math.sqrt(0.75)) / 2) + Math.sqrt(0.75))
36 | }
37 | }
38 |
39 | this.setAttribute('aUV', new Float32Array(uvs), 2)
40 | }
41 | }
--------------------------------------------------------------------------------
/src/js/modules/Sandbox.js:
--------------------------------------------------------------------------------
1 | import Renderer from './Renderer.js'
2 | import Orthographic from './Orthographic.js'
3 | import Perspective from './Perspective.js'
4 | import Volume from './Volume.js'
5 | import Collection from './Collection.js'
6 | import Mesh from './Mesh.js'
7 | import Geometry from './Geometry.js'
8 | import Plane from './Plane.js'
9 | import Circle from './Circle.js'
10 | import Tetrahedron from './Tetrahedron.js'
11 | import Cube from './Cube.js'
12 | import Sphere from './Sphere.js'
13 | import Cylinder from './Cylinder.js'
14 | import Program from './Program.js'
15 | import { ImageTexture, DataTexture } from './Texture.js'
16 | import Light from './Light.js'
17 | import FrameBuffer from './FrameBuffer.js'
18 | import ColorPicker from './ColorPicker.js'
19 |
20 | export default class Sandbox {
21 | static createColor(r, g, b) {
22 | return {
23 | r: (r / 255),
24 | g: (g / 255),
25 | b: (b / 255)
26 | }
27 | }
28 | }
29 |
30 | Sandbox.Renderer = Renderer
31 | Sandbox.Orthographic = Orthographic
32 | Sandbox.Perspective = Perspective
33 | Sandbox.Volume = Volume
34 | Sandbox.Collection = Collection
35 | Sandbox.Mesh = Mesh
36 | Sandbox.Geometry = Geometry
37 | Sandbox.Plane = Plane
38 | Sandbox.Circle = Circle
39 | Sandbox.Tetrahedron = Tetrahedron
40 | Sandbox.Cube = Cube
41 | Sandbox.Sphere = Sphere
42 | Sandbox.Cylinder = Cylinder
43 | Sandbox.Program = Program
44 | Sandbox.ImageTexture = ImageTexture
45 | Sandbox.DataTexture = DataTexture
46 | Sandbox.Light = Light
47 | Sandbox.FrameBuffer = FrameBuffer
48 | Sandbox.ColorPicker = ColorPicker
49 |
--------------------------------------------------------------------------------
/src/js/modules/Plane.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Plane extends Geometry {
4 | constructor(width, height, widthSegments, heightSegments) {
5 | const positions = []
6 |
7 | const segmentWidth = width / widthSegments
8 | const segmentHeight = height / heightSegments
9 |
10 | for (let i = 0; i < heightSegments; i++) {
11 | for (let j = 0; j < widthSegments; j++) {
12 | const x1 = (j * segmentWidth) - width / 2
13 | const y1 = (i * segmentHeight) - height / 2
14 | const z = 0
15 |
16 | const x2 = ((j + 1) * segmentWidth) - width / 2
17 | const y2 = y1
18 |
19 | const x3 = x1
20 | const y3 = ((i + 1) * segmentHeight) - height / 2
21 |
22 | const x4 = x1
23 | const y4 = y3
24 |
25 | const x5 = x2
26 | const y5 = y2
27 |
28 | const x6 = x2
29 | const y6 = y3
30 |
31 | positions.push(x1, y1, z, x2, y2, z, x3, y3, z, x4, y4, z, x5, y5, z, x6, y6, z)
32 | }
33 | }
34 | super(positions)
35 |
36 | const normals = []
37 | for (var i = 0; i < positions.length; i+=3) {
38 | const x = positions[i]
39 | const y = positions[i + 1]
40 | const z = 1
41 | const magnitude = Math.sqrt((x**2) + (y**2) + (z**2))
42 | normals.push((x/magnitude), (y/magnitude), 1)
43 | }
44 | this.attributes.aNormal.data = new Float32Array(normals)
45 | this.attributes.aNormal.count = normals.length / 3
46 |
47 | const uvs = []
48 | for (let i = 0; i < positions.length; i+=3) {
49 | const x = (positions[i] + width/2) / width
50 | const y = (positions[i+1] + height/2) / height
51 | uvs.push(x, y)
52 | }
53 | this.setAttribute('aUV', new Float32Array(uvs), 2)
54 | }
55 | }
--------------------------------------------------------------------------------
/src/js/modules/Collection.js:
--------------------------------------------------------------------------------
1 | import Matrix from './Matrix.js'
2 |
3 | export default class Collection {
4 | constructor() {
5 | this.items = []
6 | this.position = {
7 | x: 0,
8 | y: 0,
9 | z: 0
10 | }
11 | this.rotation = {
12 | x: 0,
13 | y: 0,
14 | z: 0
15 | }
16 | this.scale = {
17 | x: 1,
18 | y: 1,
19 | z: 1
20 | }
21 | this.localMatrix = Matrix.identity()
22 | }
23 |
24 | _recalculateModelMatrix() {
25 | const identity = Matrix.identity()
26 | const translation = Matrix.translate(this.position.x, this.position.y, this.position.z)
27 | const rotationX = Matrix.rotateX(this.rotation.x)
28 | const rotationY = Matrix.rotateY(this.rotation.y)
29 | const rotationZ = Matrix.rotateZ(this.rotation.z)
30 | const scale = Matrix.scale(this.scale.x, this.scale.y, this.scale.z)
31 | let matrix = Matrix.multiply(identity, translation)
32 | matrix = Matrix.multiply(matrix, rotationX)
33 | matrix = Matrix.multiply(matrix, rotationY)
34 | matrix = Matrix.multiply(matrix, rotationZ)
35 | matrix = Matrix.multiply(matrix, scale)
36 | this.localMatrix = matrix
37 | }
38 |
39 | setProjectionMatrix(matrix) {
40 | this._recalculateModelMatrix()
41 | this.projectionMatrix = matrix
42 | }
43 |
44 | setPosition(x, y, z) {
45 | this.position = { x, y, z }
46 | this._recalculateModelMatrix()
47 | }
48 |
49 | setRotationX(angle) {
50 | this.rotation.x = angle
51 | this._recalculateModelMatrix()
52 | }
53 |
54 | setRotationY(angle) {
55 | this.rotation.y = angle
56 | this._recalculateModelMatrix()
57 | }
58 |
59 | setRotationZ(angle) {
60 | this.rotation.z = angle
61 | this._recalculateModelMatrix()
62 | }
63 |
64 | setScale(x, y, z) {
65 | this.scale = { x, y, z }
66 | this._recalculateModelMatrix()
67 | }
68 | }
--------------------------------------------------------------------------------
/src/js/modules/Texture.js:
--------------------------------------------------------------------------------
1 | let textureId = 0
2 |
3 | export class ImageTexture {
4 | constructor(gl, path) {
5 | this.gl = gl
6 | this.texture = this.gl.createTexture()
7 | this.id = textureId++
8 |
9 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
10 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE,
11 | new Uint8Array([0, 0, 0, 255]))
12 |
13 | this.image = new Image()
14 | this.image.addEventListener('load', this.attachImage.bind(this))
15 | this.image.src = path
16 | }
17 |
18 | attachImage() {
19 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
20 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.image)
21 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
22 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
23 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR)
24 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR)
25 | }
26 | }
27 |
28 | export class DataTexture {
29 | constructor(gl, format, width, height, data, filter) {
30 | this.gl = gl
31 | this.texture = this.gl.createTexture()
32 | this.id = textureId++
33 | this.width = width
34 | this.height = height
35 | this.data = data ? new Uint8Array(data) : null
36 |
37 | switch (format) {
38 | case 'rgba':
39 | this.format = this.gl.RGBA
40 | break
41 | case 'rgb':
42 | this.format = this.gl.RGB
43 | break
44 | case 'luminance_alpha':
45 | this.format = this.gl.LUMINANCE_ALPHA
46 | break
47 | case 'luminance':
48 | this.format = this.gl.LUMINANCE
49 | break
50 | default:
51 | this.format = this.gl.RGBA
52 | break
53 | }
54 |
55 | switch (filter) {
56 | case 'linear':
57 | this.filter = this.gl.LINEAR
58 | break
59 | case 'nearest':
60 | this.filter = this.gl.NEAREST
61 | break
62 | default:
63 | this.filter = this.gl.NEAREST
64 | break
65 | }
66 |
67 | this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture)
68 | this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, this.gl.UNSIGNED_BYTE,
69 | this.data)
70 |
71 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE)
72 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE)
73 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.filter)
74 | this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.filter)
75 | }
76 | }
--------------------------------------------------------------------------------
/src/js/modules/Sphere.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Sphere extends Geometry {
4 | constructor(radius, segments) {
5 | const positions = []
6 | const uvs = []
7 |
8 | const segmentSize = (Math.PI * 2) / segments
9 |
10 | for (let i = 0; i < segments; i++) {
11 | for (let j = 0; j < segments; j++) {
12 | const x1 = radius * Math.cos(j * segmentSize) * Math.sin(i * segmentSize)
13 | const y1 = radius * Math.cos(i * segmentSize)
14 | const z1 = radius * Math.sin(j * segmentSize) * Math.sin(i * segmentSize)
15 |
16 | const x2 = radius * Math.cos(j * segmentSize) * Math.sin((i + 1) * segmentSize)
17 | const y2 = radius * Math.cos((i + 1) * segmentSize)
18 | const z2 = radius * Math.sin(j * segmentSize) * Math.sin((i + 1) * segmentSize)
19 |
20 | const x3 = radius * Math.cos((j + 1) * segmentSize) * Math.sin((i + 1) * segmentSize)
21 | const y3 = radius * Math.cos((i + 1) * segmentSize)
22 | const z3 = radius * Math.sin((j + 1) * segmentSize) * Math.sin((i + 1) * segmentSize)
23 |
24 | const x4 = x1
25 | const y4 = y1
26 | const z4 = z1
27 |
28 | const x5 = x3
29 | const y5 = y3
30 | const z5 = z3
31 |
32 | const x6 = radius * Math.cos((j + 1) * segmentSize) * Math.sin(i * segmentSize)
33 | const y6 = radius * Math.cos(i * segmentSize)
34 | const z6 = radius * Math.sin((j + 1) * segmentSize) * Math.sin(i * segmentSize)
35 |
36 | positions.push(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, x5, y5, z5, x6, y6, z6)
37 | }
38 | }
39 |
40 | super(positions)
41 |
42 | for (let i = 0; i < this.attributes.aNormal.data.length; i+=3) {
43 | let offset = 0
44 | if (
45 | (this.attributes.aNormal.data[i+1] == -1) &&
46 | (this.attributes.aNormal.data[i] >= 0)
47 | ) {
48 | offset = -0.5
49 | } else if (
50 | (this.attributes.aNormal.data[i+1] == -1) &&
51 | (this.attributes.aNormal.data[i] < 0)
52 | ) {
53 | offset = 0.5
54 | }
55 | const u = 0.5 + (Math.atan2(this.attributes.aNormal.data[i], this.attributes.aNormal.data[i+2]) / (Math.PI * 2))
56 | const v = 0.5 - (Math.asin(this.attributes.aNormal.data[i+1]) / Math.PI)
57 | uvs.push(u + offset, 1 - v)
58 | }
59 |
60 | const pointsPerRow = 6 * 2 * segments
61 |
62 | const quarterCircle = 3 * (pointsPerRow / 4)
63 |
64 | for (let i = 0; i < uvs.length; i+=pointsPerRow) {
65 | if (i !== 0) {
66 | uvs[i-(quarterCircle)] = 1
67 | uvs[i-(quarterCircle-2)] = 1
68 | uvs[i-(quarterCircle-6)] = 1
69 | }
70 | }
71 |
72 | uvs[uvs.length - (quarterCircle)] = 1
73 | uvs[uvs.length - (quarterCircle - 2)] = 1
74 | uvs[uvs.length - (quarterCircle - 6)] = 1
75 |
76 | this.setAttribute('aUV', new Float32Array(uvs), 2)
77 | }
78 | }
--------------------------------------------------------------------------------
/src/js/modules/Cylinder.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Cylinder extends Geometry {
4 | constructor(radius, height, segments) {
5 | const positions = []
6 |
7 |
8 | //Front
9 | for (let i = 0; i < segments; i++) {
10 | positions.push(0, 0, height / 2)
11 |
12 | const x1 = Math.cos(i * Math.PI / (segments / 2)) * radius
13 | const y1 = Math.sin(i * Math.PI / (segments / 2)) * radius
14 | const z1 = height / 2
15 | positions.push(x1, y1, z1)
16 |
17 | const x2 = Math.cos((i + 1) * Math.PI / (segments / 2)) * radius
18 | const y2 = Math.sin((i + 1) * Math.PI / (segments / 2)) * radius
19 | const z2 = height / 2
20 | positions.push(x2, y2, z2)
21 | }
22 |
23 | //Back
24 | for (let i = 0; i < segments; i++) {
25 | positions.push(0, 0, -height / 2)
26 |
27 | const x1 = Math.cos(i * Math.PI / (segments / 2)) * radius
28 | const y1 = Math.sin(i * Math.PI / (segments / 2)) * radius
29 | const z1 = -height / 2
30 | positions.push(x1, y1, z1)
31 |
32 | const x2 = Math.cos((i + 1) * Math.PI / (segments / 2)) * radius
33 | const y2 = Math.sin((i + 1) * Math.PI / (segments / 2)) * radius
34 | const z2 = -height / 2
35 | positions.push(x2, y2, z2)
36 | }
37 |
38 | //Sides
39 | for (let i = 0; i < segments; i++) {
40 | const x1 = Math.cos(i * Math.PI / (segments / 2)) * radius
41 | const y1 = Math.sin(i * Math.PI / (segments / 2)) * radius
42 | const z1 = height / 2
43 | positions.push(x1, y1, z1)
44 |
45 | const x2 = Math.cos((i + 1) * Math.PI / (segments / 2)) * radius
46 | const y2 = Math.sin((i + 1) * Math.PI / (segments / 2)) * radius
47 | positions.push(x2, y2, z1)
48 |
49 | const z2 = -height / 2
50 | positions.push(x1, y1, z2)
51 |
52 | positions.push(x2, y2, z2)
53 | positions.push(x1, y1, z2)
54 | positions.push(x2, y2, z1)
55 | }
56 |
57 | //positions.push(Math.cos(0) * radius, Math.sin(0) * radius, 0)
58 |
59 | super(positions)
60 |
61 | const normals = []
62 | for (var i = 0; i < positions.length; i+=3) {
63 | const x = positions[i]
64 | const y = positions[i + 1]
65 | const z = positions[i + 2] > 0 ? 1 : -1
66 | const magnitude = Math.sqrt((x**2) + (y**2) + (z**2))
67 | normals.push((x/magnitude), (y/magnitude), (z/magnitude))
68 | }
69 | this.attributes.aNormal.data = new Float32Array(normals)
70 | this.attributes.aNormal.count = normals.length / 3
71 |
72 | const uvs = []
73 | for (let i = 0; i < positions.length; i+=3) {
74 | const x = (positions[i] + radius) / (radius * 2)
75 | const y = (positions[i + 1] + radius) / (radius * 2)
76 | uvs.push(x, y)
77 | }
78 | this.setAttribute('aUV', new Float32Array(uvs), 2)
79 |
80 | //this.type = 'TRIANGLE_FAN'
81 | }
82 | }
--------------------------------------------------------------------------------
/src/js/modules/Orthographic.js:
--------------------------------------------------------------------------------
1 | import Matrix from './Matrix.js'
2 |
3 | export default class Orthographic {
4 | constructor(left, right, bottom, top, near, far) {
5 | this.left = left
6 | this.right = right
7 | this.bottom = bottom
8 | this.top = top
9 | this.near = near
10 | this.far = far
11 | this.type = 'orthographic'
12 | this.position = {
13 | x: 0,
14 | y: 0,
15 | z: 0
16 | }
17 | this.rotation = {
18 | x: 0,
19 | y: 0,
20 | z: 0
21 | }
22 | this.viewMatrix = Matrix.identity()
23 | this.createMatrix()
24 | }
25 |
26 | createMatrix() {
27 | this.matrix = [
28 | 2 / (this.right - this.left), 0, 0, 0,
29 | 0, 2 / (this.top - this.bottom), 0, 0,
30 | 0, 0, -2 / (this.far - this.near), 0,
31 | -(this.right + this.left) / (this.right - this.left), -(this.top + this.bottom) / (this.top - this.bottom), -(this.far + this.near) / (this.far - this.near), 1
32 | ]
33 | }
34 |
35 | _recalculateViewMatrix() {
36 | const identity = Matrix.identity()
37 | const translation = Matrix.translate(this.position.x, this.position.y, this.position.z)
38 | const rotationX = Matrix.rotateX(this.rotation.x)
39 | const rotationY = Matrix.rotateY(this.rotation.y)
40 | const rotationZ = Matrix.rotateZ(this.rotation.z)
41 | let matrix = Matrix.multiply(identity, translation)
42 | matrix = Matrix.multiply(matrix, rotationX)
43 | matrix = Matrix.multiply(matrix, rotationY)
44 | matrix = Matrix.multiply(matrix, rotationZ)
45 | if (this.lookAtEnabled) {
46 | matrix = Matrix.lookAt(
47 | [matrix[12], matrix[13], matrix[14]],
48 | [this.lookAtTarget.localMatrix[12], this.lookAtTarget.localMatrix[13], this.lookAtTarget.localMatrix[14]]
49 | )
50 | }
51 | this.viewMatrix = Matrix.inverse(matrix)
52 | }
53 |
54 | setViewProjectionMatrix() {
55 | this._recalculateViewMatrix()
56 | this.viewProjectionMatrix = Matrix.multiply(this.matrix, this.viewMatrix)
57 | }
58 |
59 | setLeft(left) {
60 | this.left = left
61 | this.createMatrix()
62 | }
63 |
64 | setRight(right) {
65 | this.right = right
66 | this.createMatrix()
67 | }
68 |
69 | setBottom(bottom) {
70 | this.bottom = bottom
71 | this.createMatrix()
72 | }
73 |
74 | setTop(top) {
75 | this.top = top
76 | this.createMatrix()
77 | }
78 |
79 | setNear(near) {
80 | this.near = near
81 | this.createMatrix()
82 | }
83 |
84 | setFar(far) {
85 | this.far = far
86 | this.createMatrix()
87 | }
88 |
89 | setPosition(x, y, z) {
90 | this.position = { x, y, z }
91 | }
92 |
93 | setRotationX(angle) {
94 | this.rotation.x = angle
95 | }
96 |
97 | setRotationY(angle) {
98 | this.rotation.y = angle
99 | }
100 |
101 | setRotationZ(angle) {
102 | this.rotation.z = angle
103 | }
104 |
105 | lookAt(target) {
106 | this.lookAtEnabled = true
107 | this.lookAtTarget = target
108 | }
109 | }
--------------------------------------------------------------------------------
/src/js/modules/Light.js:
--------------------------------------------------------------------------------
1 | import Vector from './Vector.js'
2 |
3 | export default class Light {
4 | constructor(type, vector) {
5 | this.type = type
6 | if (this.type === 'directional') {
7 | this.direction = Vector.normalize([vector[0], vector[1], vector[2]])
8 | } else if (this.type === 'point') {
9 | this.position = [vector[0], vector[1], vector[2]]
10 | }
11 | }
12 |
13 | /*
14 | VERTEX SHADER REQUIREMENTS
15 | Using a directional light requires calculating the normals relative to rotation and scale
16 | to pass to the fragment shader:
17 |
18 | vNormal = mat3(uNormalMatrix) * aNormal;
19 |
20 | FRAGMENT SHADER REQUIREMENTS
21 | Normals must be normalized:
22 |
23 | vec3 normal = normalize(vNormal);
24 |
25 | The dot product of the directional light vector and the normals will determine the lighting:
26 |
27 | float light = dot(normal, uDirectionalLight);
28 |
29 | Light is computed by multiplying the existing fragment color by the light value:
30 |
31 | gl_FragColor.rgb += gl_FragColor.rgb * ((light + 1.0) * 0.5);
32 |
33 | */
34 | setDirection(x, y, z) {
35 | this.direction = Vector.normalize([x, y, z])
36 | }
37 |
38 | /*
39 | VERTEX SHADER REQUIREMENTS
40 | Using a point light requires calculating the surface position as follows:
41 |
42 | vec3 surfacePosition = (uLocalMatrix * aPosition).xyz;
43 |
44 | Then one must calculate the normals relative to rotation and scale to pass to the fragment shader:
45 |
46 | vNormal = mat3(uNormalMatrix) * aNormal;
47 |
48 | Lastly, vectors representing the surface position to the light position and the surface position
49 | to the camera position should be passed to the fragment shader:
50 |
51 | vSurfaceToLight = uPointLight - surfacePosition;
52 | vSurfaceToCamera = uCameraPosition - surfacePosition;
53 |
54 | FRAGMENT SHADER REQUIREMENTS
55 | Normals, surface to light, and surface to camera vectors must be normalized:
56 |
57 | vec3 normal = normalize(vNormal);
58 | vec3 surfaceToLight = normalize(vSurfaceToLight);
59 | vec3 surfaceToCamera = normalize(vSurfaceToCamera);
60 |
61 | The half vector must be computed in order to get specular lighting:
62 |
63 | vec3 halfVector = normalize(vSurfaceToLight + vSurfaceToCamera);
64 |
65 | The dot product of the surface to light vector and the normals will determine the lighting:
66 |
67 | float light = dot(normal, surfaceToLight);
68 |
69 | The specular highlight requires the dot product of the normals and half vector raised to some power
70 | if the light value is greater than zero:
71 |
72 | float specular = 0.0;
73 | if (light > 0.0) {
74 | specular = pow(dot(normal, halfVector), 16.0);
75 | }
76 |
77 | Light is computed by multiplying the existing fragment color by the light value, while the specular
78 | is added to the final color value:
79 |
80 | gl_FragColor.rgb += gl_FragColor.rgb * ((light + 1.0) * 0.5);
81 | gl_FragColor.rgb += specular;
82 |
83 | */
84 | setPosition(x, y, z) {
85 | this.position = [x, y, z]
86 | }
87 | }
--------------------------------------------------------------------------------
/src/js/modules/Perspective.js:
--------------------------------------------------------------------------------
1 | import Matrix from './Matrix.js'
2 |
3 | export default class Perspective {
4 | constructor(fieldOfView, aspectRatio, near, far) {
5 | this.fieldOfView = fieldOfView * Math.PI / 180
6 | this.aspectRatio = aspectRatio
7 | this.near = near
8 | this.far = far
9 | this.type = 'perspective'
10 | this.position = {
11 | x: 0,
12 | y: 0,
13 | z: 0
14 | }
15 | this.rotation = {
16 | x: 0,
17 | y: 0,
18 | z: 0
19 | }
20 | this.viewMatrix = Matrix.identity()
21 | this.lookAtEnabled = false
22 | this.createMatrix()
23 | }
24 |
25 | createMatrix() {
26 | this.top = this.near * Math.tan(this.fieldOfView / 2)
27 | this.bottom = -this.top
28 | this.right = this.top * this.aspectRatio
29 | this.left = -this.right
30 |
31 | this.matrix = [
32 | 2 * this.near / (this.right - this.left), 0, 0, 0,
33 | 0, 2 * this.near / (this.top - this.bottom), 0, 0,
34 | 0, 0, -(this.far + this.near) / (this.far - this.near), -1,
35 | -this.near * (this.right + this.left) / (this.right - this.left), -this.near * (this.top + this.bottom) / (this.top - this.bottom), (2 * this.far * this.near) / (this.near - this.far), 0
36 | ]
37 | }
38 |
39 | _recalculateViewMatrix() {
40 | const identity = Matrix.identity()
41 | const translation = Matrix.translate(this.position.x, this.position.y, this.position.z)
42 | const rotationX = Matrix.rotateX(this.rotation.x)
43 | const rotationY = Matrix.rotateY(this.rotation.y)
44 | const rotationZ = Matrix.rotateZ(this.rotation.z)
45 | let matrix = Matrix.multiply(identity, translation)
46 | matrix = Matrix.multiply(matrix, rotationX)
47 | matrix = Matrix.multiply(matrix, rotationY)
48 | matrix = Matrix.multiply(matrix, rotationZ)
49 | if (this.lookAtEnabled) {
50 | matrix = Matrix.lookAt(
51 | [matrix[12], matrix[13], matrix[14]],
52 | [this.lookAtTarget.localMatrix[12], this.lookAtTarget.localMatrix[13], this.lookAtTarget.localMatrix[14]]
53 | )
54 | }
55 | this.viewMatrix = Matrix.inverse(matrix)
56 | }
57 |
58 | setViewProjectionMatrix() {
59 | this._recalculateViewMatrix()
60 | this.viewProjectionMatrix = Matrix.multiply(this.matrix, this.viewMatrix)
61 | }
62 |
63 | setFieldOfView(fieldOfView) {
64 | this.fieldOfView = fieldOfView * Math.PI / 180
65 | this.createMatrix()
66 | }
67 |
68 | setAspectRatio(aspectRatio) {
69 | this.aspectRatio = aspectRatio
70 | this.createMatrix()
71 | }
72 |
73 | setNear(near) {
74 | this.near = near
75 | this.createMatrix()
76 | }
77 |
78 | setFar(far) {
79 | this.far = far
80 | this.createMatrix()
81 | }
82 |
83 | setPosition(x, y, z) {
84 | this.position = { x, y, z }
85 | }
86 |
87 | setRotationX(angle) {
88 | this.rotation.x = angle
89 | }
90 |
91 | setRotationY(angle) {
92 | this.rotation.y = angle
93 | }
94 |
95 | setRotationZ(angle) {
96 | this.rotation.z = angle
97 | }
98 |
99 | lookAt(target) {
100 | this.lookAtEnabled = true
101 | this.lookAtTarget = target
102 | }
103 | }
--------------------------------------------------------------------------------
/src/js/modules/ColorPicker.js:
--------------------------------------------------------------------------------
1 | export default class ColorPicker {
2 | constructor(gl, mouse, camera) {
3 | this.gl = gl
4 | this.mouse = mouse
5 | this.camera = camera
6 | this.color = new Uint8Array(4)
7 | this.selectedIndex = -1
8 | this.objectCount = 0
9 | this.viewport = {
10 | width: window.innerWidth,
11 | height: window.innerHeight,
12 | aspectRatio: window.innerWidth / window.innerHeight
13 | }
14 | }
15 |
16 | resize(viewport) {
17 | this.viewport.width = viewport.width
18 | this.viewport.height = viewport.height
19 | this.viewport.aspectRatio = viewport.width / viewport.height
20 | }
21 |
22 | _getPixel() {
23 | this.pixel = {
24 | x: this.mouse.x * this.viewport.width / this.viewport.width,
25 | y: this.viewport.height - this.mouse.y * this.viewport.height / this.viewport.height - 1
26 | }
27 | }
28 |
29 | _getColor() {
30 | this.gl.readPixels(0, 0, 1, 1, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.color)
31 | }
32 |
33 | getMatrix() {
34 | this._getPixel()
35 | if (this.camera.type === 'perspective') {
36 | this.top = this.camera.near * Math.tan(this.camera.fieldOfView / 2)
37 | this.bottom = -this.top
38 | this.right = this.top * this.camera.aspectRatio
39 | this.left = -this.right
40 | } else if (this.camera.type === 'orthographic') {
41 | this.top = this.camera.top
42 | this.bottom = this.camera.bottom
43 | this.right = this.camera.right
44 | this.left = this.camera.left
45 | }
46 | this.width = Math.abs(this.right - this.left)
47 | this.height = Math.abs(this.top - this.bottom)
48 |
49 | this.pixelLeft = this.left + this.pixel.x * this.width / this.viewport.width
50 | this.pixelRight = this.pixelLeft + (1 / this.viewport.width)
51 | this.pixelTop = this.bottom + this.pixel.y * this.height / this.viewport.height
52 | this.pixelBottom = this.pixelTop + (1 / this.viewport.height)
53 | this.near = this.camera.near
54 | this.far = this.camera.far
55 |
56 | if (this.camera.type === 'perspective') {
57 | this.matrix = [
58 | 2 * this.near / (this.pixelRight - this.pixelLeft), 0, 0, 0,
59 | 0, 2 * this.near / (this.pixelTop - this.pixelBottom), 0, 0,
60 | (this.pixelRight + this.pixelLeft) / (this.pixelRight - this.pixelLeft), (this.pixelTop + this.pixelBottom) / (this.pixelTop - this.pixelBottom), -(this.far + this.near) / (this.far - this.near), -1,
61 | 0, 0, (2 * this.far * this.near) / (this.near - this.far), 0
62 | ]
63 | } else if (this.camera.type === 'orthographic') {
64 | this.matrix = [
65 | 2 / (this.pixelRight - this.pixelLeft), 0, 0, 0,
66 | 0, 2 / (this.pixelTop - this.pixelBottom), 0, 0,
67 | 0, 0, -2 / (this.far - this.near), 0,
68 | -((this.pixelRight + this.pixelLeft) / (this.pixelRight - this.pixelLeft)), -((this.pixelTop + this.pixelBottom) / (this.pixelTop - this.pixelBottom)), -((this.far + this.near) / (this.far - this.near)), 1
69 | ]
70 | }
71 |
72 | return this.matrix
73 | }
74 |
75 | getObjectIndex() {
76 | this._getColor()
77 | this.selectedIndex = ((this.color[3] / 255) * this.objectCount) - 1
78 | return this.selectedIndex
79 | }
80 | }
--------------------------------------------------------------------------------
/src/js/modules/Cube.js:
--------------------------------------------------------------------------------
1 | import Geometry from './Geometry'
2 |
3 | export default class Cube extends Geometry {
4 | constructor(width, height, depth, widthSegments, heightSegments, depthSegments) {
5 | const positions = []
6 | const uvs = []
7 |
8 | //Front
9 | createSide('x', 'y', 'z', width, height, depth, widthSegments, heightSegments, 'front', false, false)
10 |
11 | //Back
12 | createSide('x', 'y', 'z', width, height, -depth, widthSegments, heightSegments, 'back', true, false)
13 |
14 | //Top
15 | createSide('x', 'z', 'y', width, depth, height, widthSegments, depthSegments, 'back', false, true)
16 |
17 | //Bottom
18 | createSide('x', 'z', 'y', width, depth, -height, widthSegments, depthSegments, 'front', false, false)
19 |
20 | //Right
21 | createSide('z', 'y', 'x', depth, height, width, depthSegments, heightSegments, 'back', true, false)
22 |
23 | //Left
24 | createSide('z', 'y', 'x', depth, height, -width, depthSegments, heightSegments, 'front', false, false)
25 |
26 | function createSide(x, y, z, xLength, yLength, depth, xSegments, ySegments, direction, uvFlipX, uvFlipY) {
27 | const segmentX = xLength / xSegments
28 | const segmentY = yLength / ySegments
29 |
30 | const z1 = depth / 2
31 |
32 | for (let i = 0; i < ySegments; i++) {
33 | for (let j = 0; j < xSegments; j++) {
34 | const point = {}
35 | point[x] = []
36 | point[y] = []
37 | point[z] = []
38 |
39 | const x1 = (j * segmentX) - xLength / 2
40 | const y1 = (i * segmentY) - yLength / 2
41 | const x2 = ((j + 1) * segmentX) - xLength / 2
42 | const y2 = ((i + 1) * segmentY) - yLength / 2
43 | if (direction === 'front') {
44 | point[x].push(x1)
45 | point[y].push(y1)
46 | point[z].push(z1)
47 |
48 | point[x].push(x2)
49 | point[y].push(y1)
50 | point[z].push(z1)
51 |
52 | point[x].push(x1)
53 | point[y].push(y2)
54 | point[z].push(z1)
55 |
56 | point[x].push(x1)
57 | point[y].push(y2)
58 | point[z].push(z1)
59 |
60 | point[x].push(x2)
61 | point[y].push(y1)
62 | point[z].push(z1)
63 |
64 | point[x].push(x2)
65 | point[y].push(y2)
66 | point[z].push(z1)
67 | } else if (direction === 'back') {
68 | point[x].push(x2)
69 | point[y].push(y1)
70 | point[z].push(z1)
71 |
72 | point[x].push(x1)
73 | point[y].push(y1)
74 | point[z].push(z1)
75 |
76 | point[x].push(x2)
77 | point[y].push(y2)
78 | point[z].push(z1)
79 |
80 | point[x].push(x2)
81 | point[y].push(y2)
82 | point[z].push(z1)
83 |
84 | point[x].push(x1)
85 | point[y].push(y1)
86 | point[z].push(z1)
87 |
88 | point[x].push(x1)
89 | point[y].push(y2)
90 | point[z].push(z1)
91 | }
92 |
93 | positions.push(
94 | point.x[0], point.y[0], point.z[0],
95 | point.x[1], point.y[1], point.z[1],
96 | point.x[2], point.y[2], point.z[2],
97 | point.x[3], point.y[3], point.z[3],
98 | point.x[4], point.y[4], point.z[4],
99 | point.x[5], point.y[5], point.z[5]
100 | )
101 |
102 | for (let k = 0; k < 6; k++) {
103 | let uvX
104 | let uvY
105 | if (uvFlipX) {
106 | uvX = 1 - (point[x][k] + xLength/2) / xLength
107 | } else {
108 | uvX = (point[x][k] + xLength/2) / xLength
109 | }
110 | if (uvFlipY) {
111 | uvY = 1 - (point[y][k] + yLength/2) / yLength
112 | } else {
113 | uvY = (point[y][k] + yLength/2) / yLength
114 | }
115 | uvs.push(uvX, uvY)
116 | }
117 | }
118 | }
119 |
120 |
121 | }
122 |
123 | super(positions)
124 | this.setAttribute('aUV', new Float32Array(uvs), 2)
125 | }
126 | }
--------------------------------------------------------------------------------
/src/js/modules/Renderer.js:
--------------------------------------------------------------------------------
1 | export default class Renderer {
2 | constructor(element) {
3 | this.gl = element.getContext('webgl', {
4 | powerPreference: 'high-performance'
5 | })
6 |
7 | this.resize = this.resize.bind(this)
8 | this.render = this.render.bind(this)
9 | this.depthTest = true
10 | this.faceCulling = true
11 | this.pixelRatio = Math.min(window.devicePixelRatio, 2.0)
12 | this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true)
13 | this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, 1)
14 | this.framebuffer = null
15 | }
16 |
17 | setPixelRatio(ratio) {
18 | this.pixelRatio = ratio
19 | }
20 |
21 | setFrameBuffer(framebuffer) {
22 | if (framebuffer !== null) {
23 | this.framebuffer = framebuffer.buffer
24 | } else {
25 | this.framebuffer = null
26 | }
27 | }
28 |
29 | resize() {
30 | const displayWidth = this.gl.canvas.clientWidth * this.pixelRatio
31 | const displayHeight = this.gl.canvas.clientHeight * this.pixelRatio
32 |
33 | const needsResize = this.gl.canvas.width * this.pixelRatio !== displayWidth || this.gl.canvas.height * this.pixelRatio !== displayHeight
34 |
35 | if (needsResize) {
36 | this.gl.canvas.width = displayWidth
37 | this.gl.canvas.height = displayHeight
38 |
39 | this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
40 |
41 | return true
42 | }
43 |
44 | return false
45 | }
46 |
47 | render(volume, camera) {
48 | this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.framebuffer)
49 | this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT)
50 |
51 | if (this.faceCulling) {
52 | this.gl.enable(this.gl.CULL_FACE)
53 | }
54 | if (this.depthTest) {
55 | this.gl.enable(this.gl.DEPTH_TEST)
56 | }
57 | this.gl.enable(this.gl.BLEND)
58 | this.gl.blendEquation( this.gl.FUNC_ADD )
59 | this.gl.blendFunc( this.gl.ONE_MINUS_CONSTANT_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA )
60 |
61 | let lastShader = null
62 | let lastBuffer = null
63 |
64 | camera.setViewProjectionMatrix()
65 |
66 | for (const object of volume.objects) {
67 | object.setProjectionMatrix(camera.viewProjectionMatrix)
68 | let bindBuffers = false
69 |
70 | if (object.shader.program !== lastShader) {
71 | this.gl.useProgram(object.shader.program)
72 | lastShader = object.shader.program
73 | bindBuffers = true
74 | }
75 |
76 | if (bindBuffers || object.attributes != lastBuffer) {
77 | for (const attribute in object.attributes) {
78 | this.gl.enableVertexAttribArray(object.attributes[attribute].location)
79 | this.gl.bindBuffer(this.gl.ARRAY_BUFFER, object.attributes[attribute].buffer)
80 | const size = object.attributes[attribute].size
81 | const type = this.gl.FLOAT
82 | const normalize = false
83 | const stride = 0
84 | const offset = 0
85 | this.gl.vertexAttribPointer(object.attributes[attribute].location, size, type, normalize, stride, offset)
86 | }
87 | lastBuffer = object.attributes
88 | }
89 |
90 | for (const uniform in object.uniforms) {
91 | if (uniform === 'uViewProjectionMatrix') {
92 | this.gl.uniformMatrix4fv(object.uniforms[uniform].location, false, object.projectionMatrix)
93 | } else if (uniform === 'uNormalMatrix') {
94 | this.gl.uniformMatrix4fv(object.uniforms[uniform].location, false, object.normalMatrix)
95 | } else if (uniform === 'uLocalMatrix') {
96 | this.gl.uniformMatrix4fv(object.uniforms[uniform].location, false, object.localMatrix)
97 | } else {
98 | switch (object.uniforms[uniform].type) {
99 | case '1f':
100 | this.gl.uniform1f(object.uniforms[uniform].location, object.uniforms[uniform].value)
101 | break
102 | case '2f':
103 | this.gl.uniform2f(object.uniforms[uniform].location, object.uniforms[uniform].value[0], object.uniforms[uniform].value[1])
104 | break
105 | case '3f':
106 | this.gl.uniform3f(object.uniforms[uniform].location, object.uniforms[uniform].value[0], object.uniforms[uniform].value[1], object.uniforms[uniform].value[2])
107 | break
108 | case '4f':
109 | this.gl.uniform4f(object.uniforms[uniform].location, object.uniforms[uniform].value[0], object.uniforms[uniform].value[1], object.uniforms[uniform].value[2], object.uniforms[uniform].value[3])
110 | break
111 | case 'mat3':
112 | this.gl.uniformMatrix3fv(object.uniforms[uniform].location, false, object.uniforms[uniform].value)
113 | break
114 | case 'mat4':
115 | this.gl.uniformMatrix4fv(object.uniforms[uniform].location, false, object.uniforms[uniform].value)
116 | break
117 | case 'tex':
118 | this.gl.uniform1i(object.uniforms[uniform].location, object.uniforms[uniform].value.id)
119 | this.gl.activeTexture(this.gl.TEXTURE0 + object.uniforms[uniform].value.id)
120 | this.gl.bindTexture(this.gl.TEXTURE_2D, object.uniforms[uniform].value.texture)
121 | default:
122 | break
123 | }
124 | }
125 | }
126 |
127 | const primitiveType = this.gl[object.drawMode]
128 | const vertexOffset = 0
129 | const count = object.attributes.aPosition.count
130 | this.gl.drawArrays(primitiveType, vertexOffset, count)
131 | }
132 | }
133 | }
--------------------------------------------------------------------------------
/src/js/modules/Mesh.js:
--------------------------------------------------------------------------------
1 | import Matrix from './Matrix.js'
2 | import Vector from './Vector.js'
3 |
4 | export default class Mesh {
5 | constructor(geometry, shader) {
6 | this.geometryID = geometry.id
7 | this.geometryType = geometry.type
8 | this.shader = shader
9 | this.position = {
10 | x: 0,
11 | y: 0,
12 | z: 0
13 | }
14 | this.rotation = {
15 | x: 0,
16 | y: 0,
17 | z: 0
18 | }
19 | this.scale = {
20 | x: 1,
21 | y: 1,
22 | z: 1
23 | }
24 | this.attributes = geometry.attributes
25 | this.uniforms = {
26 | uViewProjectionMatrix: {
27 | name: 'uViewProjectionMatrix',
28 | value: null,
29 | type: 'mat4'
30 | },
31 | uNormalMatrix: {
32 | name: 'uNormalMatrix',
33 | value: null,
34 | type: 'mat4'
35 | },
36 | uLocalMatrix: {
37 | name: 'uLocalMatrix',
38 | value: null,
39 | type: 'mat4'
40 | }
41 | }
42 | this.surfaceNormals = false
43 | this.localMatrix = Matrix.identity()
44 | this._setAttributeData()
45 | this._setUniformData()
46 | this._setDrawMode()
47 | this._setSurfaceNormals()
48 | }
49 |
50 | _setAttributeData() {
51 | for (const attribute in this.attributes) {
52 | this.attributes[attribute].location = this.shader.gl.getAttribLocation(this.shader.program, this.attributes[attribute].name)
53 | this.attributes[attribute].buffer = this.shader.gl.createBuffer()
54 | this.shader.gl.bindBuffer(this.shader.gl.ARRAY_BUFFER, this.attributes[attribute].buffer)
55 | this.shader.gl.bufferData(this.shader.gl.ARRAY_BUFFER, this.attributes[attribute].data, this.shader.gl.STATIC_DRAW)
56 | }
57 | }
58 |
59 | _setUniformData() {
60 | for (const uniform in this.uniforms) {
61 | this.uniforms[uniform].location = this.shader.gl.getUniformLocation(this.shader.program, this.uniforms[uniform].name)
62 | }
63 | }
64 |
65 | _setDrawMode() {
66 | if (this.geometryType) {
67 | this.drawMode = this.geometryType
68 | } else {
69 | this.drawMode = 'TRIANGLES'
70 | }
71 | }
72 |
73 | _setSurfaceNormals() {
74 | if (this.surfaceNormals) {
75 | const surfaceNormals = []
76 | for (let i = 0; i < this.geometry.attributes.aNormal.data.length; i+=9) {
77 | const p1 = [this.geometry.attributes.aNormal.data[i], this.geometry.attributes.aNormal.data[i+1], this.geometry.attributes.aNormal.data[i+2]]
78 | const p2 = [this.geometry.attributes.aNormal.data[i+3], this.geometry.attributes.aNormal.data[i+4], this.geometry.attributes.aNormal.data[i+5]]
79 | const p3 = [this.geometry.attributes.aNormal.data[i+6], this.geometry.attributes.aNormal.data[i+7], this.geometry.attributes.aNormal.data[i+8]]
80 |
81 | const u = Vector.subtract(p2, p1)
82 | const v = Vector.subtract(p3, p1)
83 |
84 | const x = (u[1] * v[2]) - (u[2] * v[1])
85 | const y = (u[2] * v[0]) - (u[0] * v[2])
86 | const z = (u[0] * v[1]) - (u[1] * v[0])
87 |
88 | const normals = Vector.normalize([x, y, z])
89 |
90 | surfaceNormals.push(normals[0], normals[1], normals[2])
91 | surfaceNormals.push(normals[0], normals[1], normals[2])
92 | surfaceNormals.push(normals[0], normals[1], normals[2])
93 | }
94 | this.shader.gl.bindBuffer(this.shader.gl.ARRAY_BUFFER, this.geometry.attributes.aNormal.buffer)
95 | this.shader.gl.bufferData(this.shader.gl.ARRAY_BUFFER, new Float32Array(surfaceNormals), this.shader.gl.STATIC_DRAW)
96 | }
97 | }
98 |
99 | _recalculateModelMatrix() {
100 | const identity = Matrix.identity()
101 | const translation = Matrix.translate(this.position.x, this.position.y, this.position.z)
102 | const rotationX = Matrix.rotateX(this.rotation.x)
103 | const rotationY = Matrix.rotateY(this.rotation.y)
104 | const rotationZ = Matrix.rotateZ(this.rotation.z)
105 | const scale = Matrix.scale(this.scale.x, this.scale.y, this.scale.z)
106 | let matrix = Matrix.multiply(identity, translation)
107 | matrix = Matrix.multiply(matrix, rotationX)
108 | matrix = Matrix.multiply(matrix, rotationY)
109 | matrix = Matrix.multiply(matrix, rotationZ)
110 | matrix = Matrix.multiply(matrix, scale)
111 | if (this.parentCollection) {
112 | this.localMatrix = Matrix.multiply(this.parentCollection.localMatrix, matrix)
113 | } else {
114 | this.localMatrix = matrix
115 | }
116 | }
117 |
118 | _recalculateNormalMatrix() {
119 | this.normalMatrix = Matrix.transpose(Matrix.inverse(this.localMatrix))
120 | }
121 |
122 | setProjectionMatrix(matrix) {
123 | this._recalculateModelMatrix()
124 | this._recalculateNormalMatrix()
125 | this.projectionMatrix = matrix
126 | }
127 |
128 | setPosition(x, y, z) {
129 | this.position = { x, y, z }
130 | this._recalculateModelMatrix()
131 | }
132 |
133 | setRotationX(angle) {
134 | this.rotation.x = angle
135 | this._recalculateModelMatrix()
136 | }
137 |
138 | setRotationY(angle) {
139 | this.rotation.y = angle
140 | this._recalculateModelMatrix()
141 | }
142 |
143 | setRotationZ(angle) {
144 | this.rotation.z = angle
145 | this._recalculateModelMatrix()
146 | }
147 |
148 | setScale(x, y, z) {
149 | this.scale = { x, y, z }
150 | this._recalculateModelMatrix()
151 | }
152 |
153 | setAttribute(name, data, size) {
154 | this.attributes[name] = {
155 | name,
156 | data,
157 | size,
158 | count: data.length / size
159 | }
160 | this._setAttributeData()
161 | }
162 |
163 | setUniform(name, value, type) {
164 | this.uniforms[name] = {
165 | name,
166 | value,
167 | type
168 | }
169 | this._setUniformData()
170 | }
171 |
172 | setShader(shader) {
173 | this.shader = shader
174 | this._setAttributeData()
175 | this._setUniformData()
176 | }
177 |
178 | setParent(collection) {
179 | if (this.parentCollection) {
180 | let index = this.parentCollection.items.indexOf(this)
181 | if (index >= 0) {
182 | this.parentCollection.items.splice(index, 1)
183 | }
184 | }
185 |
186 | if (collection) {
187 | collection.items.push(this)
188 | }
189 | this.parentCollection = collection
190 | }
191 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Sandbox
4 | A simple WebGL library for creating 3D scenes.
5 |
6 | ## Demo
7 | Open the index.html file to view a demo of the essential features.
8 |
9 | ## Getting Started
10 | To create a 3D scene in Sandbox you will need a few things:
11 | 1. A canvas element
12 | 2. A Renderer object
13 | 3. A Camera object
14 | 4. At least one Mesh consisting of both geometry data and a shader program
15 | 5. A Volume to contain meshes
16 |
17 | ### Creating a Renderer object
18 | A Renderer's constructor requires the canvas element as an argument to be the render target. The renderer has a 'resize' method that can be called whenever the canvas dimensions have changed. The 'pixelRatio' attribute can be set to account for high-density displays; by default the pixel ratio is set to either the display's pixel ratio or a value of 2 depending on which is smaller. The 'depthTest' and 'faceCulling' attributes are exposed from the OpenGL implementation for additional customization.
19 | ```javascript
20 | const canvas = document.getElementById('webgl-canvas')
21 | const renderer = new Sandbox.Renderer(canvas)
22 |
23 | //Methods and attributes
24 | renderer.resize()
25 | renderer.pixelRatio = 2.0
26 | renderer.depthTest = true
27 | renderer.faceCulling = true
28 | ```
29 |
30 | ### Creating a Camera object
31 | Sandbox includes two types of Camera objects, a Perspective Camera and an Orthographic Camera. Common to both Camera types are a few methods and attributes:
32 | - A 'position' object containing 'x', 'y', and 'z' attributes
33 | - A 'rotation' object containing 'x', 'y', and 'z' attributes
34 | - A 'lookAtEnabled' flag to determine if the current 'lookAt' target should be used
35 | - A 'createMatrix' method that regenerates the Camera's local matrix based on current viewing frustum attributes
36 | - A 'setViewProjectionMatrix' method that regenerates the viewProjection matrix based on the current local matrix
37 | - A 'setNear' method used to set the near plane of the viewing frustum
38 | - A 'setFar' method used to set the far plane of the viewing frustum
39 | - A 'setPosition' method that takes 'x', 'y', and 'z' arguments
40 | - A 'setRotationX' method to set the Camera's X rotation
41 | - A 'setRotationY' method to set the Camera's Y rotation
42 | - A 'setRotationZ' method to set the Camera's Z rotation
43 | - A 'lookAt' method which takes a Mesh target to force the Camera to center the target in its field of view regardless of position
44 |
45 | #### The Perspective Camera
46 | A Perspective Camera requires four arguments:
47 | 1. A 'fieldOfView'
48 | 2. An 'aspectRatio'
49 | 3. A 'near' plane location
50 | 4. A 'far' plane location
51 | ```javascript
52 | const camera = new Sandbox.Perspective(35, 1.85, 0.1, 100)
53 | ```
54 |
55 | #### The Orthographic Camera
56 | A Perspective Camera requires six arguments:
57 | 1. A 'left' plane location
58 | 2. A 'right' plane location
59 | 3. A 'bottom' plane location
60 | 4. A 'top' plane location
61 | 5. A 'near' plane location
62 | 6. A 'far' plane location
63 | ```javascript
64 | const camera = new Sandbox.Orthographic(-1, 1, -1, 1, -1, 1)
65 | ```
66 |
67 | ### Creating a Mesh
68 | A Mesh consists of both geometry data and a shader program.
69 |
70 | #### The base Geometry class
71 | To create a custom Geometry object, pass in an array of position data as 3D positions.
72 | ```javascript
73 | const positions = [
74 | 1, 0, 0, //x, y, z
75 | 0, 1, 0, //x, y, z
76 | 0, 0, 1 //x, y, z
77 | ]
78 | const geometry = new Sandbox.Geometry(positions)
79 | ```
80 |
81 | #### The Primitives
82 | While geometry data can be created manually with the Geometry class, various primitives are included as part of Sandbox including:
83 | - Plane
84 | - Circle
85 | - Tetrahedron
86 | - Cube
87 | - Sphere
88 | - Cylinder
89 |
90 | ##### Plane
91 | ```javascript
92 | new Sandbox.Plane(width, height, widthSegments, heightSegments)
93 | ```
94 |
95 | ##### Circle
96 | ```javascript
97 | new Sandbox.Circle(radius, segments)
98 | ```
99 |
100 | ##### Tetrahedron
101 | ```javascript
102 | new Sandbox.Tetrahedron(size)
103 | ```
104 |
105 | ##### Cube
106 | ```javascript
107 | new Sandbox.Cube(width, height, depth, widthSegments, heightSegments, depthSegments)
108 | ```
109 |
110 | ##### Sphere
111 | ```javascript
112 | new Sandbox.Sphere(radius, segments)
113 | ```
114 |
115 | ##### Cylinder
116 | ```javascript
117 | new Sandbox.Cylinder(radius, height, segments)
118 | ```
119 |
120 | #### Shader Programs
121 | A shader program requires that you pass in a reference the Renderer object's 'gl' attribute, a vertex shader string, and a fragment shader string. Sandbox accepts GLSL strings.
122 | ```javascript
123 | const shader = new Sandbox.Program(renderer.gl, vertex, fragment)
124 | ```
125 |
126 | #### The Mesh Object
127 | ```javascript
128 | const mesh = new Sandbox.Mesh(geometry, shader)
129 | ```
130 |
131 | The Mesh object has the same 'position' and 'rotation' attributes, as well as the 'setPosition', 'setRotationX', 'setRotationY', and 'setRotationZ' methods that the Camera objects have as well as an additional attribute 'scale' and a 'setScale' method which takes an 'x', 'y', and 'z' value.
132 |
133 | ##### Mesh Attributes
134 | Attributes in addition to the 'aPosition' attributed generated by the geometry data can be added through the 'setAttribute' method. Attributes must exist in the vertex shader in order for attribute data to properly work. The 'setAttribute' method takes three arguments:
135 | 1. A 'name' for the attribute that will be referenced in the vertex shader
136 | 2. A Float32Array of attribute data
137 | 3. A size of the data indicating how many array values exist per vertex.
138 | ```javascript
139 | mesh.setAttribute('aColor', new Float32Array(colorData), 3)
140 | ```
141 |
142 | ##### Mesh Uniforms
143 | Uniforms can be supplied to Mesh objects through the 'setUniform' method. Uniforms must exist in the vertex shader in order for uniform data to properly work. The 'setUniform' method takes three arguments:
144 | 1. A 'name' for the uniform that will be referenced in the vertex shader
145 | 2. A current value for the uniform. Sandbox supports floats, JavaScript arrays containing 2-4 floats, 3D and 4D matricies, and textures.
146 | 3. A data type for the uniform, either '1f', '2f', '3f', '4f', 'mat3', 'mat4', or 'tex'
147 | ```javascript
148 | mesh.setUniform('uTime', 0, '1f')
149 | ```
150 |
151 | ##### Swapping Shaders
152 | The shader initially set on creation of the Mesh can be swapped for an alternate using the 'setShader' method.
153 | ```javascript
154 | mesh.setShader(shader)
155 | ```
156 |
157 | ### Adding a Mesh to a Volume
158 | Create a volume then add a Mesh using the 'add' method.
159 | ```javascript
160 | const volume = new Sandbox.Volume()
161 | volume.add(mesh)
162 | ```
163 |
164 | ### Rendering the Volume
165 | Establish a clear color for the Renderer and then render the Volume with a Camera
166 | ```javascript
167 | renderer.gl.clearColor(0, 0, 0, 0)
168 | renderer.render(volume, camera)
169 | ```
170 |
171 | ## Todos
172 | Documentation and demos for Collections, Color Picking, Lights, Texturing, and Framebuffer Rendering are still in progress.
173 |
174 | ## License
175 | Licensed under [the MIT license](LICENSE.md).
176 |
177 |
178 |
--------------------------------------------------------------------------------
/src/js/modules/Matrix.js:
--------------------------------------------------------------------------------
1 | import Vector from './Vector.js'
2 |
3 | export default class Matrix {
4 | static multiply(a, b) {
5 | const b00 = b[0 * 4 + 0]
6 | const b01 = b[0 * 4 + 1]
7 | const b02 = b[0 * 4 + 2]
8 | const b03 = b[0 * 4 + 3]
9 | const b10 = b[1 * 4 + 0]
10 | const b11 = b[1 * 4 + 1]
11 | const b12 = b[1 * 4 + 2]
12 | const b13 = b[1 * 4 + 3]
13 | const b20 = b[2 * 4 + 0]
14 | const b21 = b[2 * 4 + 1]
15 | const b22 = b[2 * 4 + 2]
16 | const b23 = b[2 * 4 + 3]
17 | const b30 = b[3 * 4 + 0]
18 | const b31 = b[3 * 4 + 1]
19 | const b32 = b[3 * 4 + 2]
20 | const b33 = b[3 * 4 + 3]
21 | const a00 = a[0 * 4 + 0]
22 | const a01 = a[0 * 4 + 1]
23 | const a02 = a[0 * 4 + 2]
24 | const a03 = a[0 * 4 + 3]
25 | const a10 = a[1 * 4 + 0]
26 | const a11 = a[1 * 4 + 1]
27 | const a12 = a[1 * 4 + 2]
28 | const a13 = a[1 * 4 + 3]
29 | const a20 = a[2 * 4 + 0]
30 | const a21 = a[2 * 4 + 1]
31 | const a22 = a[2 * 4 + 2]
32 | const a23 = a[2 * 4 + 3]
33 | const a30 = a[3 * 4 + 0]
34 | const a31 = a[3 * 4 + 1]
35 | const a32 = a[3 * 4 + 2]
36 | const a33 = a[3 * 4 + 3]
37 |
38 | return [
39 | b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
40 | b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
41 | b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
42 | b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
43 | b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
44 | b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
45 | b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
46 | b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
47 | b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
48 | b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
49 | b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
50 | b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
51 | b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
52 | b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
53 | b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
54 | b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
55 | ]
56 | }
57 |
58 | static identity() {
59 | return [
60 | 1, 0, 0, 0,
61 | 0, 1, 0, 0,
62 | 0, 0, 1, 0,
63 | 0, 0, 0, 1
64 | ]
65 | }
66 |
67 | static inverse(m) {
68 | const result = new Float32Array(16)
69 | const m00 = m[0 * 4 + 0]
70 | const m01 = m[0 * 4 + 1]
71 | const m02 = m[0 * 4 + 2]
72 | const m03 = m[0 * 4 + 3]
73 | const m10 = m[1 * 4 + 0]
74 | const m11 = m[1 * 4 + 1]
75 | const m12 = m[1 * 4 + 2]
76 | const m13 = m[1 * 4 + 3]
77 | const m20 = m[2 * 4 + 0]
78 | const m21 = m[2 * 4 + 1]
79 | const m22 = m[2 * 4 + 2]
80 | const m23 = m[2 * 4 + 3]
81 | const m30 = m[3 * 4 + 0]
82 | const m31 = m[3 * 4 + 1]
83 | const m32 = m[3 * 4 + 2]
84 | const m33 = m[3 * 4 + 3]
85 | const tmp_0 = m22 * m33
86 | const tmp_1 = m32 * m23
87 | const tmp_2 = m12 * m33
88 | const tmp_3 = m32 * m13
89 | const tmp_4 = m12 * m23
90 | const tmp_5 = m22 * m13
91 | const tmp_6 = m02 * m33
92 | const tmp_7 = m32 * m03
93 | const tmp_8 = m02 * m23
94 | const tmp_9 = m22 * m03
95 | const tmp_10 = m02 * m13
96 | const tmp_11 = m12 * m03
97 | const tmp_12 = m20 * m31
98 | const tmp_13 = m30 * m21
99 | const tmp_14 = m10 * m31
100 | const tmp_15 = m30 * m11
101 | const tmp_16 = m10 * m21
102 | const tmp_17 = m20 * m11
103 | const tmp_18 = m00 * m31
104 | const tmp_19 = m30 * m01
105 | const tmp_20 = m00 * m21
106 | const tmp_21 = m20 * m01
107 | const tmp_22 = m00 * m11
108 | const tmp_23 = m10 * m01
109 |
110 | const t0 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) -
111 | (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31)
112 | const t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) -
113 | (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31)
114 | const t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) -
115 | (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31)
116 | const t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) -
117 | (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21)
118 |
119 | const d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3)
120 |
121 | result[0] = d * t0
122 | result[1] = d * t1
123 | result[2] = d * t2
124 | result[3] = d * t3
125 | result[4] = d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) -
126 | (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30))
127 | result[5] = d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) -
128 | (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30))
129 | result[6] = d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) -
130 | (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30))
131 | result[7] = d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) -
132 | (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20))
133 | result[8] = d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) -
134 | (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33))
135 | result[9] = d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) -
136 | (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33))
137 | result[10] = d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) -
138 | (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33))
139 | result[11] = d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) -
140 | (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23))
141 | result[12] = d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) -
142 | (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22))
143 | result[13] = d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) -
144 | (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02))
145 | result[14] = d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) -
146 | (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12))
147 | result[15] = d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) -
148 | (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02))
149 |
150 | return result
151 | }
152 |
153 | static transpose(m) {
154 | return [
155 | m[0], m[4], m[8], m[12],
156 | m[1], m[5], m[9], m[13],
157 | m[2], m[6], m[10], m[14],
158 | m[3], m[7], m[11], m[15],
159 | ]
160 | }
161 |
162 | static translate(tx, ty, tz) {
163 | return [
164 | 1, 0, 0, 0,
165 | 0, 1, 0, 0,
166 | 0, 0, 1, 0,
167 | tx, ty, tz, 1
168 | ]
169 | }
170 |
171 | static rotateX(angle) {
172 | const radians = angle * Math.PI / 180
173 | const cos = Math.cos(radians)
174 | const sin = Math.sin(radians)
175 | return [
176 | 1, 0, 0, 0,
177 | 0, cos, sin, 0,
178 | 0, -sin, cos, 0,
179 | 0, 0, 0, 1
180 | ]
181 | }
182 |
183 | static rotateY(angle) {
184 | const radians = angle * Math.PI / 180
185 | const cos = Math.cos(radians)
186 | const sin = Math.sin(radians)
187 | return [
188 | cos, 0, -sin, 0,
189 | 0, 1, 0, 0,
190 | sin, 0, cos, 0,
191 | 0, 0, 0, 1
192 | ]
193 | }
194 |
195 | static rotateZ(angle) {
196 | const radians = angle * Math.PI / 180
197 | const cos = Math.cos(radians)
198 | const sin = Math.sin(radians)
199 | return [
200 | cos, sin, 0, 0,
201 | -sin, cos, 0, 0,
202 | 0, 0, 1, 0,
203 | 0, 0, 0, 1
204 | ]
205 | }
206 |
207 | static scale(sx, sy, sz) {
208 | return [
209 | sx, 0, 0, 0,
210 | 0, sy, 0, 0,
211 | 0, 0, sz, 0,
212 | 0, 0, 0, 1
213 | ]
214 | }
215 |
216 | static lookAt(viewer, target) {
217 | const z = Vector.normalize(Vector.subtract(viewer, target))
218 | const x = Vector.normalize(Vector.cross([0, 1, 0], z))
219 | const y = Vector.normalize(Vector.cross(z, x))
220 |
221 | return [
222 | x[0], x[1], x[2], 0,
223 | y[0], y[1], y[2], 0,
224 | z[0], z[1], z[2], 0,
225 | viewer[0], viewer[1], viewer[2], 1
226 | ]
227 | }
228 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
A simple WebGL library for creating 3D scenes.
16 |Sandbox.Plane(width, height, widthSegments, heightSegments)
26 |Sandbox.Circle(radius, segments)
34 |Sandbox.Tetrahedron(size)
42 |Sandbox.Cube(width, height, depth, widthSegments, heightSegments, depthSegments)
50 |Sandbox.Sphere(radius, segments)
58 |Sandbox.Cylinder(radius, height, segments)
66 |Sandbox.Perspective(fieldOfView, aspectRatio, near, far)
78 |Sandbox.Orthographic(left, right, bottom, top, near, far)
102 |