1import { ExpoWebGLRenderingContext, GLView } from 'expo-gl';
2import { Renderer, TextureLoader } from 'expo-three';
3import * as React from 'react';
4import { LayoutChangeEvent, PixelRatio, StyleSheet, View } from 'react-native';
5import { BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera, Scene } from 'three';
6import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
7import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass';
8import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
9
10import TitledSwitch from '../../components/TitledSwitch';
11
12export default function GLThreeComposer() {
13  const [isComposerEnabled, setComposerEnabled] = React.useState(false);
14  const [hasGL, setHasGL] = React.useState(false);
15
16  const animationFrameId = React.useRef(-1);
17  const gl = React.useRef<null | ExpoWebGLRenderingContext>(null);
18  const camera = React.useRef<null | PerspectiveCamera>(null);
19  const glitchPass = React.useRef<null | GlitchPass>(null);
20  const scene = React.useRef<null | Scene>(null);
21  const renderer = React.useRef<null | Renderer>(null);
22  const composer = React.useRef<null | EffectComposer>(null);
23  const cubes = React.useRef<{ angularVelocity: { x: number; y: number }; mesh: Mesh }[]>([]);
24
25  React.useEffect(() => {
26    return () => {
27      cancelAnimationFrame(animationFrameId.current);
28      animationFrameId.current = 0;
29    };
30  }, []);
31
32  React.useEffect(() => {
33    if (glitchPass.current) glitchPass.current.enabled = isComposerEnabled;
34  }, [isComposerEnabled, glitchPass.current]);
35
36  React.useEffect(() => {
37    if (!gl.current || scene.current) {
38      return;
39    }
40
41    scene.current = new Scene();
42    camera.current = new PerspectiveCamera(
43      75,
44      gl.current.drawingBufferWidth / gl.current.drawingBufferHeight,
45      0.1,
46      1000
47    );
48
49    renderer.current = new Renderer({ gl: gl.current });
50    renderer.current.setSize(gl.current.drawingBufferWidth, gl.current.drawingBufferHeight);
51    renderer.current.setClearColor(0xffffff);
52
53    composer.current = new EffectComposer(renderer.current);
54    composer.current.addPass(new RenderPass(scene.current, camera.current));
55    glitchPass.current = new GlitchPass();
56    glitchPass.current.enabled = false;
57    composer.current.addPass(glitchPass.current);
58
59    const geometry = new BoxGeometry(1, 1, 1);
60    const material = new MeshBasicMaterial({
61      transparent: true,
62      map: new TextureLoader().load(require('../../../assets/images/nikki.png')),
63    });
64
65    cubes.current = Array(24)
66      .fill(0)
67      .map(() => {
68        const mesh = new Mesh(geometry, material);
69        scene.current!.add(mesh);
70        mesh.position.x = 3 - 6 * Math.random();
71        mesh.position.y = 3 - 6 * Math.random();
72        mesh.position.z = -5 * Math.random();
73        const angularVelocity = {
74          x: 0.1 * Math.random(),
75          y: 0.1 * Math.random(),
76        };
77        return { mesh, angularVelocity };
78      });
79
80    camera.current.position.z = 3;
81
82    const animate = () => {
83      animationFrameId.current = requestAnimationFrame(animate);
84      cubes.current.forEach(({ mesh, angularVelocity }) => {
85        mesh.rotation.x += angularVelocity.x;
86        mesh.rotation.y += angularVelocity.y;
87      });
88
89      if (composer.current) composer.current.render();
90      if (gl.current) {
91        gl.current.endFrameEXP();
92      }
93    };
94
95    animate();
96    renderer.current.render(scene.current, camera.current);
97    gl.current.endFrameEXP();
98  }, [gl.current, hasGL]);
99
100  const onLayout = React.useCallback(({ nativeEvent: { layout } }: LayoutChangeEvent) => {
101    if (camera.current) {
102      camera.current.aspect = layout.width / layout.height;
103      camera.current.updateProjectionMatrix();
104    }
105    if (renderer.current) {
106      const scale = PixelRatio.get();
107      renderer.current.setSize(layout.width * scale, layout.height * scale);
108    }
109    if (composer.current) {
110      composer.current.setSize(layout.width, layout.height);
111    }
112  }, []);
113
114  return (
115    <View style={styles.flex}>
116      <GLView
117        style={styles.flex}
118        onLayout={onLayout}
119        onContextCreate={(context: ExpoWebGLRenderingContext) => {
120          gl.current = context;
121          setHasGL(true);
122        }}
123      />
124      <TitledSwitch
125        style={{ position: 'absolute', justifyContent: 'flex-end', top: 0, left: 8, right: 8 }}
126        title="Use Composer"
127        value={isComposerEnabled}
128        setValue={setComposerEnabled}
129      />
130    </View>
131  );
132}
133
134GLThreeComposer.title = 'three.js glitch and film effects';
135
136const styles = StyleSheet.create({
137  flex: {
138    flex: 1,
139  },
140});
141