├── .gitignore
├── screenshot.png
├── src
├── resources
│ └── cubemap
│ │ ├── nx.png
│ │ ├── ny.png
│ │ ├── nz.png
│ │ ├── px.png
│ │ ├── py.png
│ │ └── pz.png
├── index.html
├── gui.ts
└── index.ts
├── tsconfig.json
├── tslint.json
├── webpack
├── common.config.js
├── prod.config.js
└── dev.config.js
├── .github
└── workflows
│ ├── nodejs.yml
│ └── ghpages.yml
├── README.md
├── LICENSE
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | *.DS_Store
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/screenshot.png
--------------------------------------------------------------------------------
/src/resources/cubemap/nx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/nx.png
--------------------------------------------------------------------------------
/src/resources/cubemap/ny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/ny.png
--------------------------------------------------------------------------------
/src/resources/cubemap/nz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/nz.png
--------------------------------------------------------------------------------
/src/resources/cubemap/px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/px.png
--------------------------------------------------------------------------------
/src/resources/cubemap/py.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/py.png
--------------------------------------------------------------------------------
/src/resources/cubemap/pz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amsXYZ/three-multifaceted-refraction/HEAD/src/resources/cubemap/pz.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "noImplicitAny": true,
6 | "module": "commonjs",
7 | "target": "es2017",
8 | "esModuleInterop": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint:recommended",
4 | "tslint-plugin-prettier",
5 | "tslint-config-prettier"
6 | ],
7 | "rules": {
8 | "prettier": true,
9 | "object-literal-sort-keys": false
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/webpack/common.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | target: "web",
5 | mode: "production",
6 | output: {
7 | filename: "[name].bundle.js",
8 | path: path.resolve(__dirname, "../dist")
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.ts(x?)$/,
14 | exclude: /node_modules/,
15 | loader: "ts-loader"
16 | }
17 | ]
18 | },
19 | resolve: {
20 | extensions: [".ts", ".tsx", ".js", ".jsx"]
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 |
11 | - name: Use Node.js ${{ matrix.node-version }}
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: ${{ matrix.node-version }}
15 |
16 | - name: yarn install, build, and test
17 | run: |
18 | yarn install
19 | yarn build
20 | yarn test
21 | env:
22 | CI: true
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three-multifaceted-refraction
2 |
3 | Three.js project which explores multifaceted refraction using convex geometries, and the physical phenomena involved in it.
4 |
5 | 
6 |
7 | ## References
8 |
9 | - [Jesper Vos' Multiside Refraction Tutorial](https://tympanus.net/codrops/2019/10/29/real-time-multiside-refraction-in-three-steps/)
10 | - [Wikipedia's Spectral Color Article](https://en.wikipedia.org/wiki/Color#Spectral_colors)
11 | - [Learn OpenGL's PBR Theory Article](https://learnopengl.com/PBR/Theory)
12 |
13 | ## License
14 |
15 | The code is available under the [MIT license](LICENSE)
16 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AMS - Multifaceted Refraction
6 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/webpack/prod.config.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const path = require("path");
3 |
4 | const CopyWebpackPlugin = require("copy-webpack-plugin");
5 | const HtmlWebpackPlugin = require("html-webpack-plugin");
6 |
7 | const commonConfig = require("./common.config");
8 |
9 | const config = merge(commonConfig, {
10 | plugins: [
11 | new CopyWebpackPlugin([
12 | {
13 | from: path.join(__dirname, "../src/resources"),
14 | to: "resources",
15 | toType: "dir"
16 | }
17 | ]),
18 | new HtmlWebpackPlugin({
19 | template: "./src/index.html"
20 | })
21 | ]
22 | });
23 |
24 | module.exports = config;
25 |
--------------------------------------------------------------------------------
/.github/workflows/ghpages.yml:
--------------------------------------------------------------------------------
1 | name: Github Pages Deployment
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v1
13 |
14 | - name: Use Node.js ${{ matrix.node-version }}
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: ${{ matrix.node-version }}
18 |
19 | - name: yarn install and build
20 | run: |
21 | yarn install
22 | yarn build
23 | env:
24 | CI: true
25 |
26 | - name: Deploy
27 | if: success()
28 | uses: crazy-max/ghaction-github-pages@v1
29 | with:
30 | target_branch: gh-pages
31 | build_dir: dist
32 | env:
33 | GITHUB_PAT: ${{ secrets.GITHUB_PAT }}
34 |
--------------------------------------------------------------------------------
/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const path = require("path");
3 |
4 | const CopyWebpackPlugin = require("copy-webpack-plugin");
5 | const HtmlWebpackPlugin = require("html-webpack-plugin");
6 |
7 | const commonConfig = require("./common.config");
8 |
9 | const config = merge(commonConfig, {
10 | mode: "development",
11 | module: {
12 | rules: [
13 | {
14 | enforce: "pre",
15 | test: /\.js$/,
16 | loader: "source-map-loader"
17 | }
18 | ]
19 | },
20 | devtool: "source-map",
21 | devServer: {
22 | contentBase: "./dist"
23 | },
24 | plugins: [
25 | new CopyWebpackPlugin([
26 | {
27 | from: path.join(__dirname, "../src/resources"),
28 | to: "resources",
29 | toType: "dir"
30 | }
31 | ]),
32 | new HtmlWebpackPlugin({
33 | template: "./src/index.html"
34 | })
35 | ]
36 | });
37 |
38 | module.exports = config;
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Andrés Valencia Téllez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "three-multifaceted-refraction",
3 | "version": "1.0.0",
4 | "description": "Three.js project which explores multifaceted refraction using convex geometries, and the physical phenomena involved in it.",
5 | "main": "index.js",
6 | "author": "Andrés Valencia Téllez",
7 | "license": "MIT",
8 | "scripts": {
9 | "tslint": "tslint --project tsconfig.json",
10 | "tslint:fix": "tslint --fix --project tsconfig.json",
11 | "prettier": "prettier -l \"**/*.ts\" \"**/*.tsx\" \"**/*.json\"",
12 | "prettier:fix": "prettier --write -l \"**/*.ts\" \"**/*.tsx\" \"**/*.json\"",
13 | "test": "yarn run tslint && yarn run prettier",
14 | "fix": "yarn tslint:fix && yarn prettier:fix",
15 | "build": "webpack --config webpack/prod.config.js",
16 | "start": "webpack-dev-server --config webpack/dev.config.js",
17 | "start:local": "yarn start --host 0.0.0.0"
18 | },
19 | "dependencies": {
20 | "@types/animejs": "^3.1.0",
21 | "animejs": "^3.1.0",
22 | "copy-webpack-plugin": "^5.1.1",
23 | "guify": "^0.12.0",
24 | "html-webpack-plugin": "^3.2.0",
25 | "path": "^0.12.7",
26 | "prettier": "^1.19.1",
27 | "source-map-loader": "^0.2.4",
28 | "stats.js": "^0.17.0",
29 | "three": "^0.112.1",
30 | "ts-loader": "^6.2.1",
31 | "tslint": "^5.20.1",
32 | "tslint-config-prettier": "^1.18.0",
33 | "tslint-plugin-prettier": "^2.1.0",
34 | "typescript": "^3.7.4",
35 | "webpack-dev-server": "^3.10.1",
36 | "webpack-merge": "^4.2.2"
37 | },
38 | "devDependencies": {
39 | "webpack": "^4.41.5",
40 | "webpack-cli": "^3.3.10"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/gui.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BoxBufferGeometry,
3 | Color,
4 | DodecahedronBufferGeometry,
5 | IcosahedronBufferGeometry,
6 | OctahedronBufferGeometry,
7 | PlaneBufferGeometry,
8 | SphereBufferGeometry,
9 | TetrahedronBufferGeometry,
10 | TorusBufferGeometry,
11 | TorusKnotBufferGeometry
12 | } from "three";
13 |
14 | // tslint:disable:no-var-requires
15 | const guify = require("guify");
16 | // tslint:enable:no-var-requires
17 |
18 | export function createGUI(options: any, target: any) {
19 | const gui = new guify({
20 | title: "AMS - Multifaceted Refraction",
21 | align: "right",
22 | barMode: "above"
23 | });
24 | gui.Register({
25 | type: "folder",
26 | label: "Properties"
27 | });
28 | gui.Register({
29 | type: "color",
30 | label: "Color",
31 | folder: "Properties",
32 | format: "hex",
33 | object: options,
34 | property: "color",
35 | onChange: (value: string) => {
36 | (target.material.uniforms.color.value as Color).setHex(
37 | Number("0x" + value.substr(1))
38 | );
39 | }
40 | });
41 | gui.Register({
42 | type: "range",
43 | label: "Refraction Index",
44 | folder: "Properties",
45 | min: 1.0,
46 | max: 4.0,
47 | object: options,
48 | property: "refractionIndex",
49 | onChange: (value: number) => {
50 | target.material.uniforms.refractionIndex.value = value;
51 | }
52 | });
53 | gui.Register({
54 | type: "range",
55 | label: "Dispersion",
56 | folder: "Properties",
57 | min: 0.0,
58 | max: 1.0,
59 | object: options,
60 | property: "dispersion",
61 | onChange: (value: string) => {
62 | target.material.uniforms.dispersion.value = value;
63 | }
64 | });
65 | gui.Register({
66 | type: "range",
67 | label: "Roughness",
68 | folder: "Properties",
69 | min: 0.0,
70 | max: 1.0,
71 | object: options,
72 | property: "roughness",
73 | onChange: (value: string) => {
74 | target.material.uniforms.roughness.value = value;
75 | }
76 | });
77 | gui.Register({
78 | type: "select",
79 | label: "Geometry",
80 | options: [
81 | "plane",
82 | "tetrahedron",
83 | "cube",
84 | "octahedron",
85 | "dodecahedron",
86 | "icosahedron",
87 | "sphere",
88 | "torus",
89 | "knot"
90 | ],
91 | object: options,
92 | property: "geometry",
93 | onChange: (value: string) => {
94 | target.mesh.geometry.dispose();
95 | target.backMesh.geometry.dispose();
96 | let geom;
97 | switch (value) {
98 | case "plane":
99 | geom = new PlaneBufferGeometry();
100 | break;
101 | case "tetrahedron":
102 | geom = new TetrahedronBufferGeometry();
103 | break;
104 | case "cube":
105 | geom = new BoxBufferGeometry();
106 | break;
107 | case "octahedron":
108 | geom = new OctahedronBufferGeometry();
109 | break;
110 | case "dodecahedron":
111 | geom = new DodecahedronBufferGeometry();
112 | break;
113 | case "icosahedron":
114 | geom = new IcosahedronBufferGeometry();
115 | break;
116 | case "sphere":
117 | geom = new SphereBufferGeometry(1, 16, 16);
118 | break;
119 | case "torus":
120 | geom = new TorusBufferGeometry(1, 0.5, 16, 32);
121 | break;
122 | case "knot":
123 | geom = new TorusKnotBufferGeometry(1, 0.33, 64, 32);
124 | break;
125 | }
126 | target.mesh.geometry = geom;
127 | target.backMesh.geometry = geom;
128 | // value ? target.animation.play() : target.animation.pause();
129 | }
130 | });
131 | gui.Register({
132 | type: "checkbox",
133 | label: "Animation",
134 | object: options,
135 | property: "animation",
136 | onChange: (value: boolean) => {
137 | value ? target.animation.play() : target.animation.pause();
138 | }
139 | });
140 | }
141 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import anime from "animejs";
2 | import {
3 | BackSide,
4 | Color,
5 | CubeTexture,
6 | CubeTextureLoader,
7 | HalfFloatType,
8 | IcosahedronBufferGeometry,
9 | Mesh,
10 | PerspectiveCamera,
11 | Scene,
12 | ShaderMaterial,
13 | Uniform,
14 | Vector2,
15 | WebGLRenderer,
16 | WebGLRenderTarget
17 | } from "three";
18 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
19 | import { createGUI } from "./gui";
20 | // tslint:disable:no-var-requires
21 | const Stats = require("stats.js");
22 | // tslint:enable:no-var-requires
23 |
24 | const guiOptions = {
25 | refractionIndex: 1.5,
26 | color: "#FFFFFF",
27 | dispersion: 0.1,
28 | roughness: 0.9,
29 | animation: true,
30 | geometry: "icosahedron"
31 | };
32 |
33 | const canvas = document.getElementById("canvas") as HTMLCanvasElement;
34 | const renderer = new WebGLRenderer({ canvas });
35 | renderer.setSize(canvas.offsetWidth, canvas.offsetHeight, false);
36 | renderer.setPixelRatio(window.devicePixelRatio);
37 | renderer.autoClear = false;
38 |
39 | const backScene = new Scene();
40 | const scene = new Scene();
41 | const camera = new PerspectiveCamera(
42 | 60,
43 | window.innerWidth / window.innerHeight,
44 | 0.1,
45 | 1000
46 | );
47 | camera.position.z = 5;
48 | const controls = new OrbitControls(camera, canvas);
49 | controls.enableDamping = true;
50 | controls.autoRotate = true;
51 |
52 | if (
53 | !(
54 | renderer.getContext().getExtension("OES_texture_half_float") &&
55 | renderer.getContext().getExtension("OES_texture_half_float_linear")
56 | )
57 | ) {
58 | alert("This demo is not supported on your device.");
59 | }
60 | const renderTarget = new WebGLRenderTarget(
61 | canvas.offsetWidth,
62 | canvas.offsetHeight,
63 | {
64 | type: HalfFloatType
65 | }
66 | );
67 |
68 | const geometry = new IcosahedronBufferGeometry();
69 | const backMaterial = new ShaderMaterial({
70 | vertexShader: `
71 | varying vec3 vWorldNormal;
72 |
73 | void main() {
74 | vWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
75 | vWorldNormal = -normalize(vec3(-vWorldNormal.x, vWorldNormal.yz));
76 |
77 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
78 | }`,
79 | fragmentShader: `
80 | varying vec3 vWorldNormal;
81 |
82 | void main() {
83 | gl_FragColor.rgb = vWorldNormal;
84 | }`,
85 | side: BackSide
86 | });
87 | const material = new ShaderMaterial({
88 | uniforms: {
89 | resolution: new Uniform(
90 | new Vector2(canvas.offsetWidth, canvas.offsetHeight).multiplyScalar(
91 | window.devicePixelRatio
92 | )
93 | ),
94 | backNormals: new Uniform(renderTarget.texture),
95 | envMap: new Uniform(CubeTexture.DEFAULT_IMAGE),
96 | refractionIndex: new Uniform(guiOptions.refractionIndex),
97 | color: new Uniform(new Color(guiOptions.color)),
98 | dispersion: new Uniform(guiOptions.dispersion),
99 | roughness: new Uniform(guiOptions.roughness)
100 | },
101 | vertexShader: `
102 | varying vec3 vWorldCameraDir;
103 | varying vec3 vWorldNormal;
104 | varying vec3 vViewNormal;
105 |
106 | void main() {
107 | vec4 worldPosition = modelMatrix * vec4( position, 1.0);
108 |
109 | vWorldCameraDir = worldPosition.xyz - cameraPosition;
110 | vWorldCameraDir = normalize(vec3(-vWorldCameraDir.x, vWorldCameraDir.yz));
111 |
112 | vWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz;
113 | vWorldNormal = normalize(vec3(-vWorldNormal.x, vWorldNormal.yz));
114 |
115 | vViewNormal = normalize( modelViewMatrix * vec4(normal, 0.0)).xyz;
116 |
117 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
118 | }`,
119 | fragmentShader: `
120 | #define REF_WAVELENGTH 579.0
121 | #define RED_WAVELENGTH 650.0
122 | #define GREEN_WAVELENGTH 525.0
123 | #define BLUE_WAVELENGTH 440.0
124 |
125 | uniform vec2 resolution;
126 | uniform sampler2D backNormals;
127 | uniform samplerCube envMap;
128 | uniform float refractionIndex;
129 | uniform vec3 color;
130 | uniform float dispersion;
131 | uniform float roughness;
132 | varying vec3 vWorldCameraDir;
133 | varying vec3 vWorldNormal;
134 | varying vec3 vViewNormal;
135 |
136 | vec4 refractLight(float wavelength, vec3 backFaceNormal) {
137 | float index = 1.0 / mix(refractionIndex, refractionIndex * REF_WAVELENGTH / wavelength, dispersion);
138 | vec3 dir = vWorldCameraDir;
139 | dir = refract(dir, vWorldNormal, index);
140 | dir = refract(dir, backFaceNormal, index);
141 | return textureCube(envMap, dir);
142 | }
143 |
144 | vec3 fresnelSchlick(float cosTheta, vec3 F0)
145 | {
146 | return F0 + (1.0 - F0) * pow(1.0 + cosTheta, 5.0);
147 | }
148 |
149 | void main() {
150 | vec3 backFaceNormal = texture2D(backNormals, gl_FragCoord.xy / resolution).rgb;
151 |
152 | float r = refractLight(RED_WAVELENGTH, backFaceNormal).r;
153 | float g = refractLight(GREEN_WAVELENGTH, backFaceNormal).g;
154 | float b = refractLight(BLUE_WAVELENGTH, backFaceNormal).b;
155 |
156 | vec3 fresnel = fresnelSchlick(dot(vec3(0.0,0.0,-1.0), vViewNormal), vec3(0.04));
157 | vec3 reflectedColor = textureCube(envMap, reflect(vWorldCameraDir, vWorldNormal)).rgb * saturate((1.0 - roughness) + fresnel);
158 |
159 | gl_FragColor.rgb = vec3(r,g,b) * color + reflectedColor;
160 | }`
161 | });
162 |
163 | const backMesh = new Mesh(geometry, backMaterial);
164 | backScene.add(backMesh);
165 | const mesh = new Mesh(geometry, material);
166 | scene.add(mesh);
167 | scene.background = new CubeTextureLoader()
168 | .setPath("./resources/cubemap/")
169 | .load(
170 | ["px.png", "nx.png", "py.png", "ny.png", "pz.png", "nz.png"],
171 | (texture: CubeTexture) => {
172 | material.uniforms.envMap.value = texture;
173 | }
174 | );
175 |
176 | const animation = anime({
177 | targets: mesh.rotation,
178 | x: 2 * Math.PI,
179 | y: 2 * Math.PI,
180 | z: 2 * Math.PI,
181 | duration: 15000,
182 | easing: "easeOutBounce",
183 | loop: true,
184 | autoplay: guiOptions.animation,
185 | update: () => {
186 | backMesh.rotation.copy(mesh.rotation);
187 | },
188 | complete: () => {
189 | mesh.rotation.set(0, 0, 0);
190 | backMesh.rotation.set(0, 0, 0);
191 | }
192 | });
193 |
194 | const stats = new Stats();
195 | stats.dom.style.cssText =
196 | "position:fixed;bottom:0;left:0;cursor:pointer;opacity:0.9;z-index:10000";
197 | canvas.parentElement.appendChild(stats.dom);
198 | createGUI(guiOptions, { material, animation, mesh, backMesh });
199 |
200 | window.addEventListener("resize", (event: UIEvent) => {
201 | camera.aspect = window.innerWidth / window.innerHeight;
202 | camera.updateProjectionMatrix();
203 |
204 | renderer.setSize(canvas.offsetWidth, canvas.offsetHeight, false);
205 | renderTarget.setSize(canvas.offsetWidth, canvas.offsetHeight);
206 | material.uniforms.resolution.value.set(
207 | window.devicePixelRatio * canvas.offsetWidth,
208 | window.devicePixelRatio * canvas.offsetHeight
209 | );
210 | });
211 |
212 | function render() {
213 | renderer.setRenderTarget(renderTarget);
214 | renderer.clear(true, true);
215 | renderer.render(backScene, camera);
216 | renderer.setRenderTarget(null);
217 | renderer.clear(true, true);
218 | renderer.render(scene, camera);
219 | }
220 |
221 | function animate() {
222 | requestAnimationFrame(animate);
223 | stats.begin();
224 | controls.update();
225 | render();
226 | stats.end();
227 | }
228 | animate();
229 |
--------------------------------------------------------------------------------