├── .editorconfig
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── demo
├── index.html
├── src
│ ├── base.js
│ ├── index.js
│ ├── instance.js
│ └── utils.js
└── style.css
├── package.json
└── src
└── index.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_size = 2
5 | indent_style = space
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .DS_Store
4 | package-lock.json
5 | .rts*
6 | three.min.js
7 | uot.umd.js
8 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Colin van Eenige
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # THREE.Phenomenon
3 |
4 | [](https://www.npmjs.com/package/three.phenomenon)
5 | [](https://unpkg.com/three.phenomenon)
6 | [](https://github.com/vaneenige/three.phenomenon/blob/master/LICENSE)
7 | [](https://github.com/mrdoob/three.js/)
8 | [](https://www.typescriptlang.org/)
9 |
10 | THREE.Phenomenon is a tiny wrapper around three.js built for high-performance WebGL experiences.
11 |
12 | With it's simple API a mesh can be created that contains multiple instances of a geometry combined with a material. With access to the vertex shader, attributes per instance and uniforms this mesh can be transformed in any way possible (and on the GPU).
13 |
14 | #### Features:
15 | - Below 1kb in size (gzip)
16 | - Custom instanced geometries
17 | - Attributes for every instance
18 | - Support for default materials
19 | - Compatible with three.js r104
20 |
21 | ## Install
22 | ```
23 | $ npm install --save three.phenomenon
24 | ```
25 |
26 | ## Usage
27 | ```js
28 | // Import the library
29 | import Phenomenon from 'three.phenomenon';
30 |
31 | // Create an instance
32 | Phenomenon({ ... });
33 | ```
34 |
35 | > The wrapper is also available through THREE.Phenomenon.
36 |
37 | ## API
38 | ### Phenomenon(options)
39 |
40 | Returns an instance of Phenomenon.
41 |
42 | > The instance provides access to the mesh (with the compiled vertex and fragment shader) and uniforms.
43 |
44 | #### options.attributes
45 | Type: `Array`
46 |
47 | Values used in the program that are stored once, directly on the GPU. Every item in this array needs to have a:
48 | - `name` for referencing data in the vertex shader.
49 | - `data` function to create the data for each instance.
50 | - `size` so it's clear what comes back from the data.
51 |
52 | > The data function receives the index of the current instance and the total number of instances so calculations can be done based on these values.
53 |
54 | #### options.uniforms
55 | Type: `Object`
56 |
57 | Variables used in the program that can be adjusted on the fly. These are accessible through the instance variable and can be updated directly.
58 |
59 | #### options.vertex
60 | Type: `String`
61 |
62 | The vertex shader of the program which will calculate the position of every instance. This will automatically get merged with the shaders that's created based on the provided geometry.
63 |
64 | #### options.fragment
65 | Type: `Array`
66 |
67 | The fragment parameter is optional and can be used to modify specific parts of the provided material's fragment shader. For example: Give every instance a unique color or manually use its position for calculations.
68 |
69 | #### options.geometry
70 | Type: `THREE.Geometry`
71 |
72 | The geometry that will be multiplied. See Geometry for more information.
73 |
74 | #### options.material
75 | Type: `THREE.Material`
76 |
77 | The material that is used for the geometry. See Material for more information.
78 |
79 | #### options.multiplier
80 | Type: `Number`
81 | The amount of instances that will be created.
82 |
83 | #### options.castShadow
84 | Type: `Boolean`
85 | Should the mesh cast a shadow?
86 |
87 | ## Contribute
88 | Are you excited about this library and have interesting ideas on how to improve it? Please tell me or contribute directly!
89 |
90 | ```
91 | npm install > npm start > http://localhost:8080
92 | ```
93 |
94 | ## License
95 | MIT © Colin van Eenige
96 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | THREE.Phenomenon
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/src/base.js:
--------------------------------------------------------------------------------
1 | function base() {
2 | const renderer = new THREE.WebGLRenderer({
3 | antialias: true,
4 | });
5 |
6 | renderer.shadowMap.enabled = true;
7 | renderer.shadowMap.type = THREE.PCFSoftShadowMap;
8 |
9 | renderer.setClearColor(0x212121, 0);
10 | renderer.setSize(window.innerWidth, window.innerHeight);
11 | renderer.setPixelRatio(1);
12 |
13 | document.querySelector('body').appendChild(renderer.domElement);
14 |
15 | const scene = new THREE.Scene();
16 |
17 | const camera = new THREE.PerspectiveCamera(
18 | 40,
19 | window.innerWidth / window.innerHeight,
20 | 0.1,
21 | 10000
22 | );
23 | camera.position.set(0, 20 * 1, 35 * 1);
24 | camera.lookAt(scene.position);
25 | scene.add(camera);
26 |
27 | const ambientLight = new THREE.AmbientLight('#ffffff', 0.1);
28 | scene.add(ambientLight);
29 |
30 | const plane = new THREE.Mesh(
31 | new THREE.PlaneGeometry(1000, 1000),
32 | new THREE.MeshPhongMaterial({
33 | emissive: '#F694C1',
34 | })
35 | );
36 | plane.receiveShadow = true;
37 | plane.position.y = -15;
38 | plane.rotation.x = Math.PI * -0.5;
39 | scene.add(plane);
40 |
41 | const light = new THREE.SpotLight(0xffffff, 2, 80, Math.PI * 0.25, 1, 2);
42 | light.position.set(0, 40, 0);
43 | light.castShadow = true;
44 | light.shadow.mapSize.width = 1024;
45 | light.shadow.mapSize.height = 1024;
46 | light.shadow.camera.near = 0.5;
47 | light.shadow.camera.far = 31;
48 |
49 | scene.add(light);
50 |
51 | return { renderer, scene, camera };
52 | }
53 |
54 | export default base;
55 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import './../../dist/three.phenomenon';
2 |
3 | import base from './base';
4 | import instance from './instance';
5 |
6 | const { renderer, scene, camera } = base();
7 | const { mesh, uniforms } = instance();
8 |
9 | scene.add(mesh);
10 |
11 | let progress = 0;
12 |
13 | uot(
14 | p => {
15 | progress = p;
16 | },
17 | 3000,
18 | Infinity
19 | );
20 |
21 | function animate() {
22 | requestAnimationFrame(animate);
23 | uniforms.uProgress.value = progress;
24 | renderer.render(scene, camera);
25 | }
26 |
27 | animate();
28 |
--------------------------------------------------------------------------------
/demo/src/instance.js:
--------------------------------------------------------------------------------
1 | import { getArrayWithNoise } from './utils';
2 |
3 | function createInstance() {
4 | const duration = 0.7;
5 |
6 | const geometry = new THREE.TorusGeometry(2, 0.5, 32, 32);
7 |
8 | const multiplier = 100;
9 |
10 | const material = new THREE.MeshPhongMaterial({
11 | color: '#ff6e40',
12 | emissive: '#ff6e40',
13 | flatShading: false,
14 | shininess: 100,
15 | });
16 |
17 | const castShadow = true;
18 |
19 | const attributes = [
20 | {
21 | name: 'aPositionStart',
22 | data: () => getArrayWithNoise([0, 0, 0], 20),
23 | size: 3,
24 | },
25 | {
26 | name: 'aControlPointOne',
27 | data: () => getArrayWithNoise([0, 0, 0], 20),
28 | size: 3,
29 | },
30 | {
31 | name: 'aControlPointTwo',
32 | data: () => getArrayWithNoise([0, 0, 0], 20),
33 | size: 3,
34 | },
35 | {
36 | name: 'aPositionEnd',
37 | data: () => getArrayWithNoise([0, 0, 0], 20),
38 | size: 3,
39 | },
40 | {
41 | name: 'aOffset',
42 | data: i => [i * ((1 - duration) / (multiplier - 1))],
43 | size: 1,
44 | },
45 | {
46 | name: 'aColor',
47 | data: (i, total) => {
48 | const color = new THREE.Color();
49 | color.setHSL(
50 | // (i % 2 === 0) ?
51 | // ((i / total) * 0.2) + THREE.Math.randFloat(0.6, 0.8) :
52 | i / total,
53 | 0.6,
54 | 0.7
55 | );
56 | return [color.r, color.g, color.b];
57 | },
58 | size: 3,
59 | },
60 | ];
61 |
62 | const uniforms = {
63 | uProgress: {
64 | value: 0,
65 | },
66 | };
67 |
68 | const vertex = `
69 | attribute vec3 aPositionStart;
70 | attribute vec3 aControlPointOne;
71 | attribute vec3 aControlPointTwo;
72 | attribute vec3 aPositionEnd;
73 | attribute vec3 aColor;
74 | attribute float aOffset;
75 | uniform float uProgress;
76 |
77 | varying vec3 vColor;
78 |
79 | float easeInOutSin(float t){
80 | return (1.0 + sin(${Math.PI} * t - ${Math.PI} / 2.0)) / 2.0;
81 | }
82 |
83 | vec4 quatFromAxisAngle(vec3 axis, float angle) {
84 | float halfAngle = angle * 0.5;
85 | return vec4(axis.xyz * sin(halfAngle), cos(halfAngle));
86 | }
87 |
88 | vec3 rotateVector(vec4 q, vec3 v) {
89 | return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
90 | }
91 |
92 | vec3 bezier4(vec3 a, vec3 b, vec3 c, vec3 d, float t) {
93 | return mix(mix(mix(a, b, t), mix(b, c, t), t), mix(mix(b, c, t), mix(c, d, t), t), t);
94 | }
95 |
96 | void main(){
97 | float tProgress = easeInOutSin(min(1.0, max(0.0, (uProgress - aOffset)) / ${duration}));
98 | vec4 quatX = quatFromAxisAngle(vec3(1.0, 0.0, 0.0), -5.0 * tProgress);
99 | vec4 quatY = quatFromAxisAngle(vec3(0.0, 0.0, 0.0), -5.0 * tProgress);
100 | vec3 basePosition = rotateVector(quatX, rotateVector(quatY, position));
101 | vec3 newPosition = bezier4(aPositionStart, aControlPointOne, aControlPointTwo, aPositionEnd, tProgress);
102 | float scale = tProgress * 2.0 - 1.0;
103 | scale = 1.0 - scale * scale;
104 | basePosition *= scale;
105 | vNormal = rotateVector(quatX, vNormal);
106 | gl_Position = basePosition + newPosition;
107 | vColor = aColor;
108 | }
109 | `;
110 |
111 | const fragment = [
112 | ['#define PHONG', 'varying vec3 vColor;'],
113 | ['vec4( diffuse, opacity )', 'vec4( vColor, opacity )'],
114 | ['vec3 totalEmissiveRadiance = emissive;', 'vec3 totalEmissiveRadiance = vColor;'],
115 | ];
116 |
117 | const instance = new THREE.Phenomenon(
118 | geometry,
119 | material,
120 | multiplier,
121 | attributes,
122 | uniforms,
123 | vertex,
124 | castShadow,
125 | fragment
126 | );
127 |
128 | return instance;
129 | }
130 |
131 | export default createInstance;
132 |
--------------------------------------------------------------------------------
/demo/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Get a random value between positive and negative.
3 | * @param {number} value
4 | */
5 | export function getRandomBetween(value) {
6 | const floor = -value;
7 | return floor + Math.random() * value * 2;
8 | }
9 |
10 | /**
11 | * Get a random value from an array.
12 | * @param {array} array
13 | */
14 | export function getRandomFromArray(array) {
15 | console.log(array);
16 | return array[Math.floor(Math.random() * array.length)];
17 | }
18 |
19 | /**
20 | * Get an array with noise added to values.
21 | * @param {array} array
22 | * @param {number} noise
23 | */
24 | export function getArrayWithNoise(array, noise) {
25 | return array.map(item => item + getRandomBetween(noise));
26 | }
27 |
--------------------------------------------------------------------------------
/demo/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | canvas {
4 | margin: 0;
5 | width: 100%;
6 | height: 100%;
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | overflow: hidden;
11 | pointer-events: none;
12 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "three.phenomenon",
3 | "version": "1.2.0",
4 | "description": "A tiny wrapper around three.js built for high-performance WebGL experiences.",
5 | "source": "src/index.ts",
6 | "main": "dist/three.phenomenon.mjs",
7 | "unpkg": "dist/three.phenomenon.umd.js",
8 | "types": "dist/index.d.ts",
9 | "scripts": {
10 | "start": "npm run copy && http-server demo --silent & $npm_execpath run watch",
11 | "copy": "npm run copy:three && npm run copy:uot",
12 | "copy:three": "cp node_modules/three/build/three.min.js demo/three.min.js",
13 | "copy:uot": "cp node_modules/uot/dist/uot.umd.js demo/uot.umd.js",
14 | "watch": "microbundle watch --format umd --entry demo/src/index.js --output demo/dist/bundle.js",
15 | "build": "rm -rf dist && microbundle --name Phenomenon --format es,umd --sourcemap false",
16 | "prepare": "$npm_execpath run build"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/vaneenige/three.phenomenon.git"
21 | },
22 | "author": {
23 | "name": "Colin van Eenige",
24 | "email": "cvaneenige@gmail.com",
25 | "url": "https://use-the-platform.com"
26 | },
27 | "files": [
28 | "src",
29 | "dist"
30 | ],
31 | "keywords": [
32 | "webgl",
33 | "instances",
34 | "particles"
35 | ],
36 | "prettier": {
37 | "printWidth": 100,
38 | "singleQuote": true,
39 | "trailingComma": "es5"
40 | },
41 | "devDependencies": {
42 | "http-server": "^0.11.1",
43 | "microbundle": "^0.11.0"
44 | },
45 | "dependencies": {
46 | "three": "^0.104.0",
47 | "uot": "^1.3.0"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as three from 'three';
2 |
3 | declare global {
4 | var THREE: typeof three;
5 | }
6 |
7 | interface Attribute {
8 | name: string;
9 | data: (i: number, total: number) => void;
10 | size: number;
11 | }
12 |
13 | class Geometry extends THREE.BufferGeometry {
14 | constructor(geometry: three.Geometry, multiplier: number, attributes: Array) {
15 | super();
16 |
17 | // Assign settings to variables
18 | const { faces, vertices } = geometry;
19 |
20 | const vertexCount = vertices.length;
21 | const indexes = faces.length * 3;
22 |
23 | // Create array to put face coordinates in
24 | const bufferIndexes = [];
25 | for (let i = 0; i < faces.length; i += 1) {
26 | bufferIndexes.push(faces[i].a, faces[i].b, faces[i].c);
27 | }
28 |
29 | // Create array with length of multiplier times indexes
30 | const indexBuffer = new Uint32Array(multiplier * indexes);
31 |
32 | // Loop over the multiplier
33 | for (let i = 0; i < multiplier; i += 1) {
34 | // Loop over the indexes of the baseGeometry
35 | for (let j = 0; j < indexes; j += 1) {
36 | // Repeat over the indexes and add them to the buffer
37 | indexBuffer[i * indexes + j] = bufferIndexes[j] + i * vertexCount;
38 | }
39 | }
40 |
41 | // Set the index with the data
42 | this.setIndex(new THREE.BufferAttribute(indexBuffer, 1));
43 |
44 | // Create a new attribute to store data in
45 | const attributeData = new Float32Array(multiplier * vertexCount * 3);
46 |
47 | // Value to hold position used in the loop for the array positions
48 | let offset = 0;
49 | // Loop over the multiplier
50 | for (let i = 0; i < multiplier; i += 1) {
51 | // Loop over the vertexCount of the baseGeometry
52 | for (let j = 0; j < vertexCount; j += 1, offset += 3) {
53 | // Repeat over the vertices and add them to the buffer
54 | const vertex = vertices[j];
55 | attributeData[offset] = vertex.x;
56 | attributeData[offset + 1] = vertex.y;
57 | attributeData[offset + 2] = vertex.z;
58 | }
59 | }
60 |
61 | const attribute = new THREE.BufferAttribute(attributeData, 3);
62 | this.addAttribute('position', attribute);
63 |
64 | // Loop over the attributes
65 | for (let i = 0; i < attributes.length; i += 1) {
66 | // Create array with length of multiplier times vertexCount times attribute size
67 | const bufferArray = new Float32Array(multiplier * vertexCount * attributes[i].size);
68 | // Create a buffer attribute where the data will be stored in
69 | const bufferAttribute = new THREE.BufferAttribute(bufferArray, attributes[i].size);
70 | // Add the attribute by it's name
71 | this.addAttribute(attributes[i].name, bufferAttribute);
72 | // Loop over the multiplier
73 | for (let j = 0; j < multiplier; j += 1) {
74 | // Get data from the attribute function for every instance
75 | const data = attributes[i].data(j, multiplier);
76 | // Calculate offset based on vertexCount and attribute size
77 | offset = j * vertexCount * bufferAttribute.itemSize;
78 | // Loop over the vertexCount of the instance
79 | for (let k = 0; k < vertexCount; k += 1) {
80 | // Loop over the item size of the attribute
81 | for (let l = 0; l < bufferAttribute.itemSize; l += 1) {
82 | // Assign the buffer data to the right position
83 | bufferArray[offset] = data[l];
84 | offset += 1;
85 | }
86 | }
87 | }
88 | }
89 |
90 | return this;
91 | }
92 | }
93 |
94 | class Phenomenon {
95 | constructor(
96 | geometry: three.Geometry,
97 | material: three.Material,
98 | multiplier: number,
99 | attributes: Array,
100 | uniforms: object,
101 | vertex: string,
102 | castShadow?: boolean,
103 | fragment?: Array>
104 | ) {
105 | // Create the custom geometry
106 | const customGeometry = new Geometry(geometry, multiplier, attributes);
107 |
108 | // Create a combined mesh
109 | const mesh = new THREE.Mesh(customGeometry, material);
110 |
111 | // Compute vertex normals
112 | mesh.geometry.computeVertexNormals();
113 |
114 | // Set callback to modify our shaders
115 | material.onBeforeCompile = shader => {
116 | // @ts-ignore - Reference shader for debugging
117 | mesh.shader = shader;
118 |
119 | // Combine the uniforms
120 | Object.assign(shader.uniforms, uniforms);
121 |
122 | // Trim the provided vertex shader
123 | const vertexShader = vertex.replace(/(\r\n|\n|\r)/gm, '');
124 |
125 | // Get shader attributes
126 | const attributes = vertexShader.match(/.+?(?=void)/)[0];
127 |
128 | // Get shader main function
129 | const main = vertexShader.match(/main\(\){(.*?)}/)[1];
130 |
131 | // Construct the final vertex shader
132 | shader.vertexShader = `${attributes} \n ${shader.vertexShader}`;
133 | shader.vertexShader = shader.vertexShader.replace(
134 | '#include ',
135 | main.replace('gl_Position =', 'vec3 transformed =')
136 | );
137 |
138 | for (let i = 0; i < fragment.length; i += 1) {
139 | shader.fragmentShader = shader.fragmentShader.replace(fragment[i][0], fragment[i][1]);
140 | }
141 |
142 | // @ts-ignore - Hack to randomize function
143 | material.onBeforeCompile = `${material.onBeforeCompile
144 | .toString()
145 | .slice(0, -1)}/* ${Math.random()} */}`;
146 |
147 | if (castShadow) {
148 | // Create custom material for shadows
149 | const customMaterial = new THREE.ShaderMaterial({
150 | vertexShader: shader.vertexShader,
151 | fragmentShader: THREE.ShaderLib.shadow.fragmentShader,
152 | uniforms,
153 | });
154 | // Turn on shadows
155 | mesh.castShadow = true;
156 | // @ts-ignore - Set custom depth material
157 | mesh.customDepthMaterial = customMaterial;
158 | // @ts-ignore - Set custom distance material
159 | mesh.customDistanceMaterial = customMaterial;
160 | }
161 | };
162 |
163 | return { mesh, uniforms };
164 | }
165 | }
166 |
167 | // @ts-ignore - Make it available through THREE
168 | THREE.Phenomenon = Phenomenon;
169 |
170 | export default Phenomenon;
171 |
--------------------------------------------------------------------------------