1import Slider from '@react-native-community/slider';
2import { Asset } from 'expo-asset';
3import * as FileSystem from 'expo-file-system';
4import * as GL from 'expo-gl';
5import { GLView } from 'expo-gl';
6import React from 'react';
7import { Image, StyleSheet, Text, View } from 'react-native';
8
9import exampleImage from '../../../assets/images/example3.jpg';
10
11const vertShaderSource = `#version 300 es
12precision mediump float;
13
14in vec2 position;
15out vec2 textureCoord;
16
17void main() {
18  vec2 clipSpace = (1.0 - 2.0 * position) * vec2(-1.0, -1.0);
19
20  textureCoord = position;
21  gl_Position = vec4(clipSpace, 0.0, 1.0);
22}`;
23
24const fragShaderSource = `#version 300 es
25precision mediump float;
26
27uniform sampler2D inputImageTexture;
28uniform float contrast;
29
30in vec2 textureCoord;
31out vec4 fragColor;
32
33void main() {
34  vec4 textureColor = texture(inputImageTexture, textureCoord);
35  vec3 rgb = (textureColor.rgb - vec3(0.5)) * contrast + vec3(0.5);
36
37  fragColor = vec4(rgb, textureColor.a);
38}`;
39
40// location to contrast uniform
41let contrastLocation: WebGLUniformLocation | null;
42
43// create and prepare GL context
44const glPromise = GLView.createContextAsync().then(async gl => {
45  // Compile vertex and fragment shaders
46  const vertShader = gl.createShader(gl.VERTEX_SHADER)!;
47  gl.shaderSource(vertShader, vertShaderSource);
48  gl.compileShader(vertShader);
49
50  const fragShader = gl.createShader(gl.FRAGMENT_SHADER)!;
51  gl.shaderSource(fragShader, fragShaderSource);
52  gl.compileShader(fragShader);
53
54  // Link, use program, save and enable attributes
55  const program = gl.createProgram()!;
56  gl.attachShader(program, vertShader);
57  gl.attachShader(program, fragShader);
58  gl.linkProgram(program);
59  gl.validateProgram(program);
60
61  gl.useProgram(program);
62
63  contrastLocation = gl.getUniformLocation(program, 'contrast');
64
65  // create framebuffer
66  const framebuffer = gl.createFramebuffer();
67  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
68
69  // Create, bind, fill buffer
70  const buffer = gl.createBuffer();
71  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
72
73  const verts = new Float32Array([0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]);
74  gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
75
76  // Bind 'position' attribute
77  const positionAttrib = gl.getAttribLocation(program, 'position');
78  gl.enableVertexAttribArray(positionAttrib);
79  gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 0, 0);
80
81  // download an asset
82  const asset = Asset.fromModule(exampleImage);
83  await asset.downloadAsync();
84
85  // set viewport
86  gl.viewport(0, 0, asset.width!, asset.height!);
87
88  // create output texture
89  const outputTexture = gl.createTexture();
90  gl.activeTexture(gl.TEXTURE0);
91  gl.bindTexture(gl.TEXTURE_2D, outputTexture);
92  gl.texImage2D(
93    gl.TEXTURE_2D,
94    0,
95    gl.RGBA,
96    asset.width!,
97    asset.height!,
98    0,
99    gl.RGBA,
100    gl.UNSIGNED_BYTE,
101    null
102  );
103
104  // create input texture
105  const inputTexture = gl.createTexture();
106  gl.activeTexture(gl.TEXTURE1);
107  gl.bindTexture(gl.TEXTURE_2D, inputTexture);
108
109  // attach texture to framebuffer
110  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, outputTexture, 0);
111
112  // set texture parameters
113  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
114  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
115  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
116  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
117
118  // fill up input texture
119  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, asset as any);
120  gl.uniform1i(gl.getUniformLocation(program, 'inputImageTexture'), 1);
121
122  gl.clearColor(0, 0, 0, 1);
123  // tslint:disable-next-line: no-bitwise
124  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
125
126  return gl;
127});
128
129interface State {
130  contrast: number;
131  snapshot?: GL.GLSnapshot;
132}
133
134// See: https://github.com/expo/expo/pull/10229#discussion_r490961694
135// eslint-disable-next-line @typescript-eslint/ban-types
136export default class GLHeadlessRenderingScreen extends React.PureComponent<{}, State> {
137  static title = 'Headless rendering';
138
139  isDrawing = false;
140
141  readonly state: State = {
142    contrast: 1.0,
143  };
144
145  componentDidMount() {
146    this.draw();
147  }
148
149  async draw() {
150    if (this.isDrawing) {
151      // if another draw call is already running, we need to skip this one
152      return;
153    }
154
155    this.isDrawing = true;
156
157    const gl = await glPromise;
158
159    gl.uniform1f(contrastLocation, this.state.contrast);
160    gl.drawArrays(gl.TRIANGLES, 0, 6);
161    gl.endFrameEXP();
162
163    // we need to use flip option because framebuffer contents are flipped vertically
164    const snapshot = await GLView.takeSnapshotAsync(gl, {
165      flip: true,
166    });
167
168    // delete previous snapshot
169    if (this.state.snapshot) {
170      FileSystem.deleteAsync(this.state.snapshot.uri as string, { idempotent: true });
171    }
172
173    this.setState({ snapshot });
174    this.isDrawing = false;
175  }
176
177  onContrastChange = (contrast: number) => {
178    this.setState({ contrast }, () => {
179      this.draw();
180    });
181  };
182
183  render() {
184    const { contrast, snapshot } = this.state;
185
186    return (
187      <View style={styles.container}>
188        <Text>
189          This example shows how to use headless rendering context with Expo.GLView. Instead of the
190          GLView, it renders snapshots taken from headless context that sits on top of the app and
191          can be reused in multiple components without redundant shader recompilations.
192        </Text>
193        <View style={styles.flex}>
194          {snapshot && (
195            <Image
196              style={styles.flex}
197              fadeDuration={0}
198              source={{ uri: snapshot.uri as string }}
199              resizeMode="contain"
200            />
201          )}
202        </View>
203        <Text style={styles.sliderHeader}>{`Contrast: ${parseInt(
204          String(contrast * 100),
205          10
206        )}%`}</Text>
207        <Slider
208          value={contrast}
209          step={0.01}
210          minimumValue={0}
211          maximumValue={2}
212          onValueChange={this.onContrastChange}
213        />
214      </View>
215    );
216  }
217}
218
219const styles = StyleSheet.create({
220  container: {
221    flex: 1,
222    padding: 10,
223  },
224  flex: {
225    flex: 1,
226    width: '100%',
227  },
228  sliderHeader: {
229    paddingHorizontal: 10,
230  },
231});
232