mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-08 07:28:22 -10:00
Remove arc information from bsp_info and move it to submodel_instance. Add a model instance to each debris chunk to keep track of it.
514 lines
16 KiB
C++
514 lines
16 KiB
C++
/*
|
|
* Copyright (C) Freespace Open 2013. All rights reserved.
|
|
*
|
|
* All source code herein is the property of Freespace Open. You may not sell
|
|
* or otherwise commercially exploit the source or things you created based on the
|
|
* source.
|
|
*
|
|
*/
|
|
|
|
#include "graphics/shadows.h"
|
|
#include "asteroid/asteroid.h"
|
|
#include "cmdline/cmdline.h"
|
|
#include "debris/debris.h"
|
|
#include "graphics/matrix.h"
|
|
#include "lighting/lighting.h"
|
|
#include "math/vecmat.h"
|
|
#include "model/model.h"
|
|
#include "model/modelrender.h"
|
|
#include "options/Option.h"
|
|
#include "render/3d.h"
|
|
#include "tracing/tracing.h"
|
|
|
|
extern vec3d check_offsets[8];
|
|
|
|
matrix4 Shadow_view_matrix;
|
|
matrix4 Shadow_proj_matrix[MAX_SHADOW_CASCADES];
|
|
float Shadow_cascade_distances[MAX_SHADOW_CASCADES];
|
|
|
|
light_frustum_info Shadow_frustums[MAX_SHADOW_CASCADES];
|
|
|
|
ShadowQuality Shadow_quality = ShadowQuality::Disabled;
|
|
|
|
auto ShadowQualityOption =
|
|
options::OptionBuilder<ShadowQuality>("Graphics.Shadows", "Shadow Quality", "The quality of the shadows")
|
|
.values({{ShadowQuality::Disabled, "Disabled"},
|
|
{ShadowQuality::Low, "Low"},
|
|
{ShadowQuality::Medium, "Medium"},
|
|
{ShadowQuality::High, "High"},
|
|
{ShadowQuality::Ultra, "Ultra"}})
|
|
.change_listener([](ShadowQuality val, bool initial) {
|
|
if (initial) {
|
|
Shadow_quality = val;
|
|
}
|
|
return initial; // No dynamic changes implemented
|
|
})
|
|
.level(options::ExpertLevel::Advanced)
|
|
.category("Graphics")
|
|
.default_val(ShadowQuality::Disabled)
|
|
.importance(80)
|
|
.finish();
|
|
|
|
bool shadows_obj_in_frustum(object *objp, matrix *light_orient, vec3d *min, vec3d *max)
|
|
{
|
|
vec3d pos, pos_rot;
|
|
|
|
vm_vec_sub(&pos, &objp->pos, &Eye_position);
|
|
vm_vec_rotate(&pos_rot, &pos, light_orient);
|
|
|
|
if ( (pos_rot.xyz.x - objp->radius) > max->xyz.x
|
|
|| (pos_rot.xyz.x + objp->radius) < min->xyz.x
|
|
|| (pos_rot.xyz.y - objp->radius) > max->xyz.y
|
|
|| (pos_rot.xyz.y + objp->radius) < min->xyz.y
|
|
|| (pos_rot.xyz.z - objp->radius) > max->xyz.z ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void shadows_construct_light_proj(light_frustum_info *shadow_data)
|
|
{
|
|
memset(&shadow_data->proj_matrix, 0, sizeof(matrix4));
|
|
|
|
shadow_data->proj_matrix.a1d[0] = 2.0f / ( shadow_data->max.xyz.x - shadow_data->min.xyz.x );
|
|
shadow_data->proj_matrix.a1d[5] = 2.0f / ( shadow_data->max.xyz.y - shadow_data->min.xyz.y );
|
|
shadow_data->proj_matrix.a1d[10] = -2.0f / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
|
|
shadow_data->proj_matrix.a1d[12] = -(shadow_data->max.xyz.x + shadow_data->min.xyz.x) / ( shadow_data->max.xyz.x - shadow_data->min.xyz.x );
|
|
shadow_data->proj_matrix.a1d[13] = -(shadow_data->max.xyz.y + shadow_data->min.xyz.y) / ( shadow_data->max.xyz.y - shadow_data->min.xyz.y );
|
|
shadow_data->proj_matrix.a1d[14] = -(shadow_data->max.xyz.z + shadow_data->min.xyz.z) / ( shadow_data->max.xyz.z - shadow_data->min.xyz.z );
|
|
shadow_data->proj_matrix.a1d[15] = 1.0f;
|
|
}
|
|
|
|
void shadows_debug_show_frustum(matrix* orient, vec3d *pos, float fov, float aspect, float z_near, float z_far)
|
|
{
|
|
// find the widths and heights of the near plane and far plane to determine the points of this frustum
|
|
float near_height = tanf(fov * 0.5f) * z_near;
|
|
float near_width = near_height * aspect;
|
|
|
|
float far_height = tanf(fov * 0.5f) * z_far;
|
|
float far_width = far_height * aspect;
|
|
|
|
vec3d up_scale = ZERO_VECTOR;
|
|
vec3d right_scale = ZERO_VECTOR;
|
|
vec3d forward_scale_near = orient->vec.fvec;
|
|
vec3d forward_scale_far = orient->vec.fvec;
|
|
|
|
vm_vec_scale(&forward_scale_near, z_near);
|
|
vm_vec_scale(&forward_scale_far, z_far);
|
|
|
|
// find the eight points using eye orientation and position
|
|
vec3d near_top_left = ZERO_VECTOR;
|
|
vec3d near_top_right = ZERO_VECTOR;
|
|
vec3d near_bottom_left = ZERO_VECTOR;
|
|
vec3d near_bottom_right = ZERO_VECTOR;
|
|
|
|
// near top left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -near_width);
|
|
|
|
vm_vec_add(&near_top_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_top_left, &forward_scale_near);
|
|
|
|
// near top right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, near_width);
|
|
|
|
vm_vec_add(&near_top_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_top_right, &forward_scale_near);
|
|
|
|
// near bottom left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -near_width);
|
|
|
|
vm_vec_add(&near_bottom_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_bottom_left, &forward_scale_near);
|
|
|
|
// near bottom right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, near_width);
|
|
|
|
vm_vec_add(&near_bottom_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_bottom_right, &forward_scale_near);
|
|
|
|
vec3d far_top_left = ZERO_VECTOR;
|
|
vec3d far_top_right = ZERO_VECTOR;
|
|
vec3d far_bottom_left = ZERO_VECTOR;
|
|
vec3d far_bottom_right = ZERO_VECTOR;
|
|
|
|
// far top left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -far_width);
|
|
|
|
vm_vec_add(&far_top_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_top_left, &forward_scale_far);
|
|
|
|
// far top right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, far_width);
|
|
|
|
vm_vec_add(&far_top_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_top_right, &forward_scale_far);
|
|
|
|
// far bottom left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -far_width);
|
|
|
|
vm_vec_add(&far_bottom_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_bottom_left, &forward_scale_far);
|
|
|
|
// far bottom right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, far_width);
|
|
|
|
vm_vec_add(&far_bottom_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_bottom_right, &forward_scale_far);
|
|
|
|
// translate frustum
|
|
vm_vec_add2(&near_bottom_left, pos);
|
|
vm_vec_add2(&near_bottom_right, pos);
|
|
vm_vec_add2(&near_top_right, pos);
|
|
vm_vec_add2(&near_top_left, pos);
|
|
vm_vec_add2(&far_top_left, pos);
|
|
vm_vec_add2(&far_top_right, pos);
|
|
vm_vec_add2(&far_bottom_right, pos);
|
|
vm_vec_add2(&far_bottom_left, pos);
|
|
|
|
gr_set_color(0, 255, 255);
|
|
// g3_draw_htl_line(&near_bottom_left, &near_bottom_right);
|
|
// g3_draw_htl_line(&near_bottom_right, &near_top_right);
|
|
// g3_draw_htl_line(&near_bottom_right, &near_top_left);
|
|
// g3_draw_htl_line(&near_top_right, &near_top_left);
|
|
// g3_draw_htl_line(&far_top_left, &far_top_right);
|
|
// g3_draw_htl_line(&far_top_right, &far_bottom_right);
|
|
// g3_draw_htl_line(&far_bottom_right, &far_bottom_left);
|
|
// g3_draw_htl_line(&far_bottom_left, &far_top_left);
|
|
|
|
g3_render_line_3d(true, &near_bottom_left, &near_bottom_right);
|
|
g3_render_line_3d(true, &near_bottom_right, &near_top_right);
|
|
g3_render_line_3d(true, &near_bottom_right, &near_top_left);
|
|
g3_render_line_3d(true, &near_top_right, &near_top_left);
|
|
g3_render_line_3d(true, &far_top_left, &far_top_right);
|
|
g3_render_line_3d(true, &far_top_right, &far_bottom_right);
|
|
g3_render_line_3d(true, &far_bottom_right, &far_bottom_left);
|
|
g3_render_line_3d(true, &far_bottom_left, &far_top_left);
|
|
}
|
|
|
|
void shadows_construct_light_frustum(light_frustum_info *shadow_data, matrix *light_matrix, matrix *orient, vec3d * /*pos*/, float fov, float aspect, float z_near, float z_far)
|
|
{
|
|
// find the widths and heights of the near plane and far plane to determine the points of this frustum
|
|
float near_height = tanf(fov * 0.5f) * z_near;
|
|
float near_width = near_height * aspect;
|
|
|
|
float far_height = tanf(fov * 0.5f) * z_far;
|
|
float far_width = far_height * aspect;
|
|
|
|
vec3d up_scale = ZERO_VECTOR;
|
|
vec3d right_scale = ZERO_VECTOR;
|
|
vec3d forward_scale_near = orient->vec.fvec;
|
|
vec3d forward_scale_far = orient->vec.fvec;
|
|
|
|
vm_vec_scale(&forward_scale_near, z_near);
|
|
vm_vec_scale(&forward_scale_far, z_far);
|
|
|
|
// find the eight points using eye orientation and position
|
|
vec3d near_top_left = ZERO_VECTOR;
|
|
vec3d near_top_right = ZERO_VECTOR;
|
|
vec3d near_bottom_left = ZERO_VECTOR;
|
|
vec3d near_bottom_right = ZERO_VECTOR;
|
|
|
|
// near top left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -near_width);
|
|
|
|
vm_vec_add(&near_top_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_top_left, &forward_scale_near);
|
|
|
|
// near top right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, near_width);
|
|
|
|
vm_vec_add(&near_top_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_top_right, &forward_scale_near);
|
|
|
|
// near bottom left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -near_width);
|
|
|
|
vm_vec_add(&near_bottom_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_bottom_left, &forward_scale_near);
|
|
|
|
// near bottom right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, near_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, near_width);
|
|
|
|
vm_vec_add(&near_bottom_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&near_bottom_right, &forward_scale_near);
|
|
|
|
vec3d far_top_left = ZERO_VECTOR;
|
|
vec3d far_top_right = ZERO_VECTOR;
|
|
vec3d far_bottom_left = ZERO_VECTOR;
|
|
vec3d far_bottom_right = ZERO_VECTOR;
|
|
|
|
// far top left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -far_width);
|
|
|
|
vm_vec_add(&far_top_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_top_left, &forward_scale_far);
|
|
|
|
// far top right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, -far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, far_width);
|
|
|
|
vm_vec_add(&far_top_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_top_right, &forward_scale_far);
|
|
|
|
// far bottom left
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, -far_width);
|
|
|
|
vm_vec_add(&far_bottom_left, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_bottom_left, &forward_scale_far);
|
|
|
|
// far bottom right
|
|
up_scale = orient->vec.uvec;
|
|
vm_vec_scale(&up_scale, far_height);
|
|
|
|
right_scale = orient->vec.rvec;
|
|
vm_vec_scale(&right_scale, far_width);
|
|
|
|
vm_vec_add(&far_bottom_right, &up_scale, &right_scale);
|
|
vm_vec_add2(&far_bottom_right, &forward_scale_far);
|
|
|
|
vec3d frustum_pts[8];
|
|
|
|
// bring frustum points into light space
|
|
vm_vec_rotate(&frustum_pts[0], &near_bottom_left, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[1], &near_bottom_right, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[2], &near_top_right, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[3], &near_top_left, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[4], &far_top_left, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[5], &far_top_right, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[6], &far_bottom_right, light_matrix);
|
|
vm_vec_rotate(&frustum_pts[7], &far_bottom_left, light_matrix);
|
|
|
|
vec3d min = ZERO_VECTOR;
|
|
vec3d max = ZERO_VECTOR;
|
|
|
|
min = frustum_pts[0];
|
|
max = frustum_pts[0];
|
|
|
|
// find min and max of frustum points
|
|
for (int i = 0; i < 8; ++i) {
|
|
if ( frustum_pts[i].xyz.x < min.xyz.x ) {
|
|
min.xyz.x = frustum_pts[i].xyz.x;
|
|
}
|
|
|
|
if ( frustum_pts[i].xyz.x > max.xyz.x ) {
|
|
max.xyz.x = frustum_pts[i].xyz.x;
|
|
}
|
|
|
|
if ( frustum_pts[i].xyz.y < min.xyz.y ) {
|
|
min.xyz.y = frustum_pts[i].xyz.y;
|
|
}
|
|
|
|
if ( frustum_pts[i].xyz.y > max.xyz.y ) {
|
|
max.xyz.y = frustum_pts[i].xyz.y;
|
|
}
|
|
|
|
if ( frustum_pts[i].xyz.z < min.xyz.z ) {
|
|
min.xyz.z = frustum_pts[i].xyz.z;
|
|
}
|
|
|
|
if ( frustum_pts[i].xyz.z > max.xyz.z ) {
|
|
max.xyz.z = frustum_pts[i].xyz.z;
|
|
}
|
|
}
|
|
|
|
shadow_data->min = min;
|
|
shadow_data->max = max;
|
|
|
|
shadows_construct_light_proj(shadow_data);
|
|
}
|
|
|
|
matrix shadows_start_render(matrix *eye_orient, vec3d *eye_pos, float fov, float aspect, float veryneardist, float neardist, float middist, float fardist)
|
|
{
|
|
if(Static_light.empty())
|
|
return vmd_identity_matrix;
|
|
|
|
auto& lp = Static_light.front();
|
|
|
|
vec3d light_dir;
|
|
matrix light_matrix;
|
|
|
|
vm_vec_copy_normalize(&light_dir, &lp.vec);
|
|
vm_vector_2_matrix(&light_matrix, &light_dir, &eye_orient->vec.uvec, NULL);
|
|
|
|
shadows_construct_light_frustum(&Shadow_frustums[0], &light_matrix, eye_orient, eye_pos, fov, aspect, 0.0f, veryneardist);
|
|
shadows_construct_light_frustum(&Shadow_frustums[1], &light_matrix, eye_orient, eye_pos, fov, aspect, veryneardist - (veryneardist - 0.0f)* 0.2f, neardist);
|
|
shadows_construct_light_frustum(&Shadow_frustums[2], &light_matrix, eye_orient, eye_pos, fov, aspect, neardist - (neardist - veryneardist) * 0.2f, middist);
|
|
shadows_construct_light_frustum(&Shadow_frustums[3], &light_matrix, eye_orient, eye_pos, fov, aspect, middist - (middist - neardist) * 0.2f, fardist);
|
|
|
|
Shadow_cascade_distances[0] = veryneardist;
|
|
Shadow_cascade_distances[1] = neardist;
|
|
Shadow_cascade_distances[2] = middist;
|
|
Shadow_cascade_distances[3] = fardist;
|
|
|
|
Shadow_proj_matrix[0] = Shadow_frustums[0].proj_matrix;
|
|
Shadow_proj_matrix[1] = Shadow_frustums[1].proj_matrix;
|
|
Shadow_proj_matrix[2] = Shadow_frustums[2].proj_matrix;
|
|
Shadow_proj_matrix[3] = Shadow_frustums[3].proj_matrix;
|
|
|
|
gr_shadow_map_start(&Shadow_view_matrix, &light_matrix);
|
|
|
|
return light_matrix;
|
|
}
|
|
|
|
void shadows_end_render()
|
|
{
|
|
gr_shadow_map_end();
|
|
}
|
|
|
|
void shadows_render_all(float fov, matrix *eye_orient, vec3d *eye_pos)
|
|
{
|
|
GR_DEBUG_SCOPE("Render shadows");
|
|
TRACE_SCOPE(tracing::BuildShadowMap);
|
|
|
|
if ( Static_light.empty() ) {
|
|
return;
|
|
}
|
|
|
|
if (Shadow_quality == ShadowQuality::Disabled) {
|
|
return;
|
|
}
|
|
|
|
//shadows_debug_show_frustum(&Player_obj->orient, &Player_obj->pos, fov, gr_screen.clip_aspect, Min_draw_distance, 3000.0f);
|
|
|
|
gr_end_proj_matrix();
|
|
gr_end_view_matrix();
|
|
|
|
// these cascade distances are a result of some arbitrary tuning to give a good balance of quality and banding.
|
|
// maybe we could use a more programmatic algorithim?
|
|
matrix light_matrix = shadows_start_render(eye_orient, eye_pos, fov, gr_screen.clip_aspect, 200.0f, 600.0f, 2500.0f, 8000.0f);
|
|
|
|
model_draw_list scene;
|
|
object *objp = Objects;
|
|
|
|
for ( int i = 0; i <= Highest_object_index; i++, objp++ ) {
|
|
bool cull = true;
|
|
|
|
for ( int j = 0; j < MAX_SHADOW_CASCADES; ++j ) {
|
|
if ( shadows_obj_in_frustum(objp, &light_matrix, &Shadow_frustums[j].min, &Shadow_frustums[j].max) ) {
|
|
cull = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( cull ) {
|
|
continue;
|
|
}
|
|
|
|
switch(objp->type)
|
|
{
|
|
case OBJ_SHIP:
|
|
{
|
|
obj_queue_render(objp, &scene);
|
|
}
|
|
break;
|
|
case OBJ_ASTEROID:
|
|
{
|
|
model_render_params render_info;
|
|
|
|
render_info.set_object_number(OBJ_INDEX(objp));
|
|
render_info.set_flags(MR_IS_ASTEROID | MR_NO_TEXTURING | MR_NO_LIGHTING);
|
|
|
|
model_clear_instance( Asteroid_info[Asteroids[objp->instance].asteroid_type].model_num[Asteroids[objp->instance].asteroid_subtype]);
|
|
model_render_queue(&render_info, &scene, Asteroid_info[Asteroids[objp->instance].asteroid_type].model_num[Asteroids[objp->instance].asteroid_subtype], &objp->orient, &objp->pos);
|
|
}
|
|
break;
|
|
|
|
case OBJ_DEBRIS:
|
|
{
|
|
debris *db;
|
|
db = &Debris[objp->instance];
|
|
|
|
if ( !(db->flags[Debris_Flags::Used])){
|
|
continue;
|
|
}
|
|
|
|
auto pmi = model_get_instance(db->model_instance_num);
|
|
auto pm = model_get(pmi->model_num);
|
|
|
|
objp = &Objects[db->objnum];
|
|
|
|
model_render_params render_info;
|
|
|
|
render_info.set_flags(MR_NO_TEXTURING | MR_NO_LIGHTING);
|
|
|
|
submodel_render_queue(&render_info, &scene, pm, pmi, db->submodel_num, &objp->orient, &objp->pos);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
scene.init_render();
|
|
scene.render_all(ZBUFFER_TYPE_FULL);
|
|
|
|
shadows_end_render();
|
|
|
|
gr_zbias(0);
|
|
gr_zbuffer_set(ZBUFFER_TYPE_READ);
|
|
gr_set_cull(0);
|
|
|
|
gr_clear_states();
|
|
|
|
gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
|
|
gr_set_view_matrix(&Eye_position, &Eye_matrix);
|
|
}
|