398 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			398 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <GL/glew.h>
 | 
						|
#include <climits>
 | 
						|
#include <cstdio>
 | 
						|
#include <vector>
 | 
						|
#include <cmath>
 | 
						|
#include "bettola/noise/fast_noise_lite.h"
 | 
						|
#include "game/player.h"
 | 
						|
#include "game/world.h"
 | 
						|
#include "graphics/camera.h"
 | 
						|
#include <SDL3/SDL_timer.h>
 | 
						|
 | 
						|
#ifndef M_PI
 | 
						|
#define M_PI 3.14159265358979323846
 | 
						|
#endif
 | 
						|
 | 
						|
#include "renderer.h"
 | 
						|
#include "bettola/math/mat4.h"
 | 
						|
 | 
						|
#define GL_CHECK_ERROR() \
 | 
						|
  do { \
 | 
						|
    GLenum err = glGetError(); \
 | 
						|
    if(err != GL_NO_ERROR) { \
 | 
						|
      fprintf(stderr, "OpenGL error at %s:%d: %s\n", __FILE__, __LINE__, \
 | 
						|
              (const char*)glewGetErrorString(err)); \
 | 
						|
     } \
 | 
						|
  } while(0)
 | 
						|
 | 
						|
 | 
						|
Renderer::Renderer(void) : _vao(0), _vbo(0), _sky_vao(0), _sky_vbo(0), _sky_ebo(0),
 | 
						|
                           _cloud_vao(0), _cloud_vbo(0), _cloud_texture(0), _sky_indices_count(0) {}
 | 
						|
 | 
						|
Renderer::~Renderer(void) {
 | 
						|
  if(_vao != 0) {
 | 
						|
    glDeleteVertexArrays(1, &_vao);
 | 
						|
  }
 | 
						|
  if(_vbo != 0) {
 | 
						|
    glDeleteBuffers(1, &_vbo);
 | 
						|
  }
 | 
						|
  if(_sky_vao != 0) {
 | 
						|
    glDeleteVertexArrays(1, &_sky_vao);
 | 
						|
  }
 | 
						|
  if(_sky_vbo != 0) {
 | 
						|
    glDeleteBuffers(1, &_sky_vbo);
 | 
						|
  }
 | 
						|
  if(_sky_ebo != 0) {
 | 
						|
    glDeleteBuffers(1, &_sky_ebo);
 | 
						|
  }
 | 
						|
  if(_cloud_texture != 0) {
 | 
						|
    glDeleteTextures(1, &_cloud_texture);
 | 
						|
  }
 | 
						|
  if(_cloud_vao != 0) {
 | 
						|
    glDeleteVertexArrays(1, &_cloud_vao);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
bool Renderer::init(int screen_width, int screen_height) {
 | 
						|
  glewExperimental = GL_TRUE;
 | 
						|
  GLenum glew_error = glewInit();
 | 
						|
  if(glew_error != GLEW_OK) {
 | 
						|
    fprintf(stderr, "Failed to init GLEW! %s\n", glewGetErrorString(glew_error));
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if(!_init_shaders()) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if(!_sky_shader.load_from_files("assets/shaders/sky.vert",
 | 
						|
                                  "assets/shaders/sky.frag")) {
 | 
						|
    fprintf(stderr, "Failed to load sky shaders\n");
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if(!_cloud_shader.load_from_files("assets/shaders/cloud.vert",
 | 
						|
                                    "assets/shaders/cloud.frag")) {
 | 
						|
    fprintf(stderr, "Failed to load cloud shaders\n");
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if(!_init_textures()) {
 | 
						|
    fprintf(stderr, "Failed to init textures\n");
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  // Definitive, correct 3D cube vertices
 | 
						|
  float vertices[] = {
 | 
						|
      /* Positions. */          /* Normals. */
 | 
						|
      -0.5f, -0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
       0.5f, -0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
       0.5f,  0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
       0.5f,  0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
      -0.5f,  0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
      -0.5f, -0.5f, -0.5f,   0.0f,  0.0f, -1.0f,
 | 
						|
 | 
						|
      -0.5f, -0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
       0.5f, -0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
       0.5f,  0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
       0.5f,  0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
      -0.5f,  0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
      -0.5f, -0.5f,  0.5f,   0.0f,  0.0f,  1.0f,
 | 
						|
 | 
						|
      -0.5f,  0.5f,  0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
      -0.5f,  0.5f, -0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
      -0.5f, -0.5f, -0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
      -0.5f, -0.5f, -0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
      -0.5f, -0.5f,  0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
      -0.5f,  0.5f,  0.5f,  -1.0f,  0.0f,  0.0f,
 | 
						|
 | 
						|
       0.5f,  0.5f,  0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
       0.5f,  0.5f, -0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
       0.5f, -0.5f, -0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
       0.5f, -0.5f, -0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
       0.5f, -0.5f,  0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
       0.5f,  0.5f,  0.5f,   1.0f,  0.0f,  0.0f,
 | 
						|
 | 
						|
      -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
       0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
       0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
       0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
      -0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
      -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,
 | 
						|
 | 
						|
      -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,
 | 
						|
       0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,
 | 
						|
       0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,
 | 
						|
       0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,
 | 
						|
      -0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,
 | 
						|
      -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f
 | 
						|
  };
 | 
						|
 | 
						|
  glGenVertexArrays(1, &_vao);
 | 
						|
  glGenBuffers(1, &_vbo);
 | 
						|
  glBindVertexArray(_vao);
 | 
						|
  glBindBuffer(GL_ARRAY_BUFFER, _vbo);
 | 
						|
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 | 
						|
  /* Position attribute. */
 | 
						|
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
 | 
						|
  glEnableVertexAttribArray(0);
 | 
						|
  /* Normal attribute. */
 | 
						|
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
 | 
						|
  glEnableVertexAttribArray(1);
 | 
						|
  glBindBuffer(GL_ARRAY_BUFFER, 0);
 | 
						|
  glBindVertexArray(0);
 | 
						|
 | 
						|
  /* Sky Sphere Generation. */
 | 
						|
  std::vector<float> sky_vertices;
 | 
						|
  std::vector<unsigned int> sky_indices;
 | 
						|
  const int segments = 32;
 | 
						|
  const int rings = 32;
 | 
						|
 | 
						|
  for(int i = 0; i <= rings; i++) {
 | 
						|
    float phi = i * M_PI / rings;
 | 
						|
    for(int j = 0; j <= segments; j++) {
 | 
						|
      float theta = j * 2.0f * M_PI / segments;
 | 
						|
      float x = cos(theta) * sin(phi);
 | 
						|
      float y = cos(phi);
 | 
						|
      float z = sin(theta) * sin(phi);
 | 
						|
      sky_vertices.push_back(x);
 | 
						|
      sky_vertices.push_back(y);
 | 
						|
      sky_vertices.push_back(z);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  for(int i = 0; i < rings; i++) {
 | 
						|
    for(int j = 0; j < segments; j++) {
 | 
						|
      int first = (i * (segments + 1)) + j;
 | 
						|
      int second = first + segments + 1;
 | 
						|
      sky_indices.push_back(first);
 | 
						|
      sky_indices.push_back(second);
 | 
						|
      sky_indices.push_back(first+1);
 | 
						|
 | 
						|
      sky_indices.push_back(second);
 | 
						|
      sky_indices.push_back(second+1);
 | 
						|
      sky_indices.push_back(first+1);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  _sky_indices_count = sky_indices.size();
 | 
						|
 | 
						|
  glGenVertexArrays(1, &_sky_vao);
 | 
						|
  glGenBuffers(1, &_sky_vbo);
 | 
						|
  glGenBuffers(1, &_sky_ebo);
 | 
						|
 | 
						|
  glBindVertexArray(_sky_vao);
 | 
						|
  glBindBuffer(GL_ARRAY_BUFFER, _sky_vbo);
 | 
						|
  glBufferData(GL_ARRAY_BUFFER, sky_vertices.size()*sizeof(float),
 | 
						|
               sky_vertices.data(), GL_STATIC_DRAW);
 | 
						|
 | 
						|
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _sky_ebo);
 | 
						|
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sky_indices.size()*sizeof(unsigned int),
 | 
						|
               sky_indices.data(), GL_STATIC_DRAW);
 | 
						|
 | 
						|
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
 | 
						|
  glEnableVertexAttribArray(0);
 | 
						|
  glBindBuffer(GL_ARRAY_BUFFER, 0);
 | 
						|
  glBindVertexArray(0);
 | 
						|
 | 
						|
  glViewport(0,0,screen_width, screen_height);
 | 
						|
  GL_CHECK_ERROR();
 | 
						|
  glEnable(GL_DEPTH_TEST); /* Depth testing for 3D! */
 | 
						|
  GL_CHECK_ERROR();
 | 
						|
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
bool Renderer::_init_textures(void) {
 | 
						|
  _cloud_texture = _generate_cloud_texture();
 | 
						|
  if(_cloud_texture == 0) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
unsigned int Renderer::_generate_cloud_texture(void) {
 | 
						|
  const int width = 512;
 | 
						|
  const int height = 512;
 | 
						|
  std::vector<float> buffer(width*height);
 | 
						|
 | 
						|
  FastNoiseLite noise;
 | 
						|
  noise.SetNoiseType(FastNoiseLite::NoiseType_Perlin);
 | 
						|
 | 
						|
  noise.SetFractalType(FastNoiseLite::FractalType_FBm);
 | 
						|
  noise.SetFractalOctaves(4);
 | 
						|
  noise.SetFractalLacunarity(2.0f);
 | 
						|
  noise.SetFractalGain(0.5f);
 | 
						|
  noise.SetFrequency(0.05f);
 | 
						|
 | 
						|
  for(int y = 0; y < height; y++) {
 | 
						|
    for(int x = 0; x < width; x++) {
 | 
						|
      const float R = 1.0f; /* Major radius of the torus. */
 | 
						|
      const float r = 0.5f; /* Minor radius of the torus. */
 | 
						|
 | 
						|
      float u = (float)x / width  * 2.0f * M_PI;
 | 
						|
      float v = (float)y / height * 2.0f * M_PI;
 | 
						|
 | 
						|
      float nx = (R + r * cos(v)) * cos(u);
 | 
						|
      float ny = (R + r * cos(v)) * sin(u);
 | 
						|
      float nz = r * sin(v);
 | 
						|
 | 
						|
      const float noise_scale = 45.0f;
 | 
						|
      float noise_val = noise.GetNoise(nx * noise_scale, ny * noise_scale, nz * noise_scale);
 | 
						|
      buffer[y*width+x] = (noise_val + 1.0f) / 2.0f;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  unsigned int texture_id;
 | 
						|
  glGenTextures(1, &texture_id);
 | 
						|
  glBindTexture(GL_TEXTURE_2D, texture_id);
 | 
						|
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_FLOAT, buffer.data());
 | 
						|
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
 | 
						|
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
 | 
						|
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 | 
						|
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | 
						|
 | 
						|
  return texture_id;
 | 
						|
}
 | 
						|
 | 
						|
void Renderer::_render_sky(const Camera& camera) {
 | 
						|
  glDepthFunc(GL_LEQUAL);
 | 
						|
  _sky_shader.use();
 | 
						|
 | 
						|
  /* Remove translation from the view matrix so the skybox follows the camera. */
 | 
						|
  BettolaMath::Mat4 view = camera.get_view_matrix();
 | 
						|
  view.elements[12] = 0;
 | 
						|
  view.elements[13] = 0;
 | 
						|
  view.elements[14] = 0;
 | 
						|
 
 | 
						|
  _sky_shader.set_mat4("view", view);
 | 
						|
  _sky_shader.set_mat4("projection", _projection);
 | 
						|
  _sky_shader.set_vec3("u_SunPos", {0.0f, 1.0f, 0.0f});
 | 
						|
 | 
						|
  glBindVertexArray(_sky_vao);
 | 
						|
  glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0);
 | 
						|
  glBindVertexArray(0);
 | 
						|
  glDepthFunc(GL_LESS); /* Put depth function back to default. */
 | 
						|
}
 | 
						|
 | 
						|
void Renderer::_render_clouds(const Camera& camera) {
 | 
						|
  glEnable(GL_BLEND);
 | 
						|
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 | 
						|
  glDisable(GL_DEPTH_TEST);
 | 
						|
 | 
						|
  _cloud_shader.use();
 | 
						|
 | 
						|
  /* Remove translation from the view matrix so clouds feel infintely far away. */
 | 
						|
  BettolaMath::Mat4 cloud_view = camera.get_view_matrix();
 | 
						|
  cloud_view.elements[12] = 0;
 | 
						|
  cloud_view.elements[13] = 0;
 | 
						|
  cloud_view.elements[14] = 0;
 | 
						|
 | 
						|
  _cloud_shader.set_mat4("projection", _projection);
 | 
						|
  _cloud_shader.set_mat4("view", cloud_view);
 | 
						|
  _cloud_shader.set_vec3("u_LightDir", {-0.5f, -1.0f, -0.5f});
 | 
						|
  _cloud_shader.set_vec3("u_SunPos", {0.0f, 1.0f, 0.0f});
 | 
						|
 | 
						|
  glBindVertexArray(_sky_vao);
 | 
						|
  glActiveTexture(GL_TEXTURE0);
 | 
						|
  glBindTexture(GL_TEXTURE_2D, _cloud_texture);
 | 
						|
  /* Tell the shader to use texture unit 0. */
 | 
						|
  glUniform1i(glGetUniformLocation(_cloud_shader.get_id(), "u_CloudTexture"),0);
 | 
						|
 | 
						|
  /* Draw first cloud layer. */
 | 
						|
  BettolaMath::Mat4 model1 = BettolaMath::Mat4::scale(1.0f, 2.0f, 1.0f);
 | 
						|
  _cloud_shader.set_mat4("model", model1);
 | 
						|
  _cloud_shader.set_float("u_Time", (float)SDL_GetTicks() / 1000.0f);
 | 
						|
  _cloud_shader.set_float("u_ScrollSpeed", 0.02f);
 | 
						|
  glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0);
 | 
						|
 | 
						|
  /* Draw second cloud layer. */
 | 
						|
  BettolaMath::Mat4 model2 = BettolaMath::Mat4::scale(1.2f, 1.2f, 1.2f);
 | 
						|
  model2 = model2.multiply(BettolaMath::Mat4::rotation(30.0f, {0.0f, 1.0f, 0.0f}));
 | 
						|
  _cloud_shader.set_mat4("model", model2);
 | 
						|
  _cloud_shader.set_float("u_ScrollSpeed", 0.03f);
 | 
						|
  glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0);
 | 
						|
 | 
						|
  glBindVertexArray(0);
 | 
						|
  glEnable(GL_DEPTH_TEST);
 | 
						|
  glDisable(GL_BLEND);
 | 
						|
}
 | 
						|
 | 
						|
void Renderer::render(const Camera& camera, const Player& player,
 | 
						|
                      const std::vector<RemotePlayer>& remote_players,
 | 
						|
                      const World& world) {
 | 
						|
  glClearColor(0.1f, 0.1f, 0.3f, 1.0f);
 | 
						|
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* Need to clear depth buffer too. */
 | 
						|
  GL_CHECK_ERROR();
 | 
						|
 | 
						|
  _projection = BettolaMath::Mat4::perspective((45.0f*M_PI)/180.0f, 800.0f/600.0f, 0.1f, 2000.0f);
 | 
						|
 | 
						|
  _render_sky(camera);
 | 
						|
  _render_clouds(camera);
 | 
						|
 | 
						|
  _shader.use();
 | 
						|
  GL_CHECK_ERROR();
 | 
						|
 | 
						|
  _shader.set_mat4("view", camera.get_view_matrix());
 | 
						|
  _shader.set_mat4(("projection"), _projection);
 | 
						|
 | 
						|
  /* Render world. */
 | 
						|
  /* Set lighting uniforms for terrain. */
 | 
						|
  _shader.set_vec3("lightColor",  { 1.0f,  1.0f,  1.0f});
 | 
						|
  _shader.set_vec3("lightDir",    {-0.5f, -1.0f, -0.5f}); /* From above and to the side. */
 | 
						|
 | 
						|
  _shader.set_bool("u_IsTerrain", true);
 | 
						|
  for(auto const& [pos, mesh] : world.get_chunk_meshes()) {
 | 
						|
    BettolaMath::Mat4 model;
 | 
						|
    _shader.set_mat4("model", model);
 | 
						|
    mesh->draw();
 | 
						|
  }
 | 
						|
  _shader.set_bool("u_IsTerrain", false);
 | 
						|
 | 
						|
  /* Draw the local player's cube. */
 | 
						|
  _shader.set_vec3("objectColor", {0.2f, 0.5f, 0.8f}); /* Player colour. */
 | 
						|
  const auto& player_pos = player.get_position();
 | 
						|
  BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(player_pos.x,
 | 
						|
                                                                  player_pos.y, player_pos.z);
 | 
						|
  BettolaMath::Mat4 rot_matrix = BettolaMath::Mat4::rotation(-camera.get_yaw()-90.f,
 | 
						|
                                                             {0.0f, 1.0f, 0.0f});
 | 
						|
  BettolaMath::Mat4 model = trans_matrix.multiply(rot_matrix);
 | 
						|
 | 
						|
  _shader.set_mat4("model", model);
 | 
						|
  GL_CHECK_ERROR();
 | 
						|
 | 
						|
  glBindVertexArray(_vao);
 | 
						|
  glDrawArrays(GL_TRIANGLES, 0, 36);
 | 
						|
  glBindVertexArray(0); /* F.ck me! Forgot to unbind here too?!?!? */
 | 
						|
 | 
						|
  /* Draw remote players' cube. */
 | 
						|
  for(const auto& remote_player : remote_players) {
 | 
						|
    glBindVertexArray(_vao); /* bind cube VAO for each remote player. */
 | 
						|
    const auto& remote_pos = remote_player.get_position();
 | 
						|
    BettolaMath::Mat4 remote_trans = BettolaMath::Mat4::translation(remote_pos.x,
 | 
						|
                                                                    remote_pos.y, remote_pos.z);
 | 
						|
    BettolaMath::Mat4 remote_rot = BettolaMath::Mat4::rotation(-remote_player.get_yaw()-90.0f,
 | 
						|
                                                               {0.0f,1.0f,0.0f});
 | 
						|
    BettolaMath::Mat4 remote_model = remote_trans.multiply(remote_rot);
 | 
						|
    _shader.set_mat4("model", remote_model);
 | 
						|
    glDrawArrays(GL_TRIANGLES, 0, 36);
 | 
						|
    glBindVertexArray(0); /* Unbind it! */
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
bool Renderer::_init_shaders(void) {
 | 
						|
  if(!_shader.load_from_files("assets/shaders/simple.vert",
 | 
						|
                              "assets/shaders/simple.frag")) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  GLint success;
 | 
						|
  glGetProgramiv(_shader.get_id(), GL_LINK_STATUS, &success);
 | 
						|
  if(!success) {
 | 
						|
    char infoLog[512];
 | 
						|
    glGetProgramInfoLog(_shader.get_id(), 512, NULL, infoLog);
 | 
						|
    fprintf(stderr, "Shader linking failed: %s\n", infoLog);
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 |