diff options
4 files changed, 243 insertions, 212 deletions
diff --git a/source/blender/compositor/nodes/COM_LensDistortionNode.cpp b/source/blender/compositor/nodes/COM_LensDistortionNode.cpp index f91744d88b6..9af1fceaae0 100644 --- a/source/blender/compositor/nodes/COM_LensDistortionNode.cpp +++ b/source/blender/compositor/nodes/COM_LensDistortionNode.cpp @@ -41,21 +41,20 @@ void LensDistortionNode::convertToOperations(ExecutionSystem *graph, CompositorC this->getInputSocket(2)->relinkConnections(operation->getInputSocket(1), 2, graph); this->getOutputSocket(0)->relinkConnections(operation->getOutputSocket(0)); - operation->setData(data); graph->addOperation(operation); } else { ScreenLensDistortionOperation *operation = new ScreenLensDistortionOperation(); operation->setbNode(editorNode); - operation->setData(data); - if (!(this->getInputSocket(1)->isConnected() || this->getInputSocket(2)->isConnected())) { - // no nodes connected to the distortion and dispersion. We can precalculate some values - float distortion = this->getInputSocket(1)->getEditorValueFloat(); - float dispersion = this->getInputSocket(2)->getEditorValueFloat(); - operation->setDistortionAndDispersion(distortion, dispersion); - } + operation->setFit(data->fit); + operation->setJitter(data->jit); + if (!getInputSocket(1)->isConnected()) + operation->setDistortion(getInputSocket(1)->getEditorValueFloat()); + if (!getInputSocket(2)->isConnected()) + operation->setDispersion(getInputSocket(2)->getEditorValueFloat()); + this->getInputSocket(0)->relinkConnections(operation->getInputSocket(0), 0, graph); this->getInputSocket(1)->relinkConnections(operation->getInputSocket(1), 1, graph); this->getInputSocket(2)->relinkConnections(operation->getInputSocket(2), 2, graph); diff --git a/source/blender/compositor/operations/COM_ProjectorLensDistortionOperation.h b/source/blender/compositor/operations/COM_ProjectorLensDistortionOperation.h index 3c910815efc..fc9c760ed77 100644 --- a/source/blender/compositor/operations/COM_ProjectorLensDistortionOperation.h +++ b/source/blender/compositor/operations/COM_ProjectorLensDistortionOperation.h @@ -32,8 +32,6 @@ private: */ SocketReader *m_inputProgram; - NodeLensDist *m_data; - float m_dispersion; bool m_dispersionAvailable; @@ -57,8 +55,6 @@ public: */ void deinitExecution(); - void setData(NodeLensDist *data) { this->m_data = data; } - bool determineDependingAreaOfInterest(rcti *input, ReadBufferOperation *readOperation, rcti *output); void updateDispersion(); diff --git a/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.cpp b/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.cpp index 29c104d0b55..035789e09e6 100644 --- a/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.cpp +++ b/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.cpp @@ -36,101 +36,149 @@ ScreenLensDistortionOperation::ScreenLensDistortionOperation() : NodeOperation() this->addOutputSocket(COM_DT_COLOR); this->setComplex(true); this->m_inputProgram = NULL; - this->m_valuesAvailable = false; - this->m_dispersion = 0.0f; this->m_distortion = 0.0f; + this->m_dispersion = 0.0f; + this->m_distortion_const = false; + this->m_dispersion_const = false; + this->m_variables_ready = false; +} + +void ScreenLensDistortionOperation::setDistortion(float distortion) +{ + m_distortion = distortion; + m_distortion_const = true; } + +void ScreenLensDistortionOperation::setDispersion(float dispersion) +{ + m_dispersion = dispersion; + m_dispersion_const = true; +} + void ScreenLensDistortionOperation::initExecution() { this->m_inputProgram = this->getInputSocketReader(0); this->initMutex(); + this->m_cx = 0.5f * (float)getWidth(); this->m_cy = 0.5f * (float)getHeight(); + /* if both are constant, init variables once */ + if (m_distortion_const && m_dispersion_const) { + updateVariables(m_distortion, m_dispersion); + m_variables_ready = true; + } } void *ScreenLensDistortionOperation::initializeTileData(rcti *rect) { void *buffer = this->m_inputProgram->initializeTileData(NULL); - updateDispersionAndDistortion(); + + /* get distortion/dispersion values once, by reading inputs at (0,0) + * XXX this assumes invariable values (no image inputs), + * we don't have a nice generic system for that yet + */ + if (!m_variables_ready) { + this->lockMutex(); + + if (!m_distortion_const) { + float result[4]; + getInputSocketReader(1)->readSampled(result, 0, 0, COM_PS_NEAREST); + m_distortion = result[0]; + } + if (!m_dispersion_const) { + float result[4]; + getInputSocketReader(2)->readSampled(result, 0, 0, COM_PS_NEAREST); + m_dispersion = result[0]; + } + + updateVariables(m_distortion, m_dispersion); + m_variables_ready = true; + + this->unlockMutex(); + } + return buffer; } -void ScreenLensDistortionOperation::executePixel(float output[4], int x, int y, void *data) +void ScreenLensDistortionOperation::get_uv(const float xy[2], float uv[2]) const { - const float height = this->getHeight(); - const float width = this->getWidth(); - MemoryBuffer *buffer = (MemoryBuffer *)data; + uv[0] = m_sc * ((xy[0] + 0.5f) - m_cx) / m_cx; + uv[1] = m_sc * ((xy[1] + 0.5f) - m_cy) / m_cy; +} - int dr = 0, dg = 0, db = 0; - float d, t, ln[6] = {0, 0, 0, 0, 0, 0}; - float tc[4] = {0, 0, 0, 0}; - const float v = this->m_sc * ((y + 0.5f) - this->m_cy) / this->m_cy; - const float u = this->m_sc * ((x + 0.5f) - this->m_cx) / this->m_cx; - const float uv_dot = u * u + v * v; - int sta = 0, mid = 0, end = 0; - - if ((t = 1.0f - this->m_kr4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - ln[0] = (u * d + 0.5f) * width - 0.5f, ln[1] = (v * d + 0.5f) * height - 0.5f; - sta = 1; - } - if ((t = 1.0f - this->m_kg4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - ln[2] = (u * d + 0.5f) * width - 0.5f, ln[3] = (v * d + 0.5f) * height - 0.5f; - mid = 1; +void ScreenLensDistortionOperation::distort_uv(const float uv[2], float t, float xy[2]) const +{ + float d = 1.0f / (1.0f + sqrtf(t)); + xy[0] = (uv[0] * d + 0.5f) * getWidth() - 0.5f; + xy[1] = (uv[1] * d + 0.5f) * getHeight() - 0.5f; +} + +bool ScreenLensDistortionOperation::get_delta(float r_sq, float k4, const float uv[2], float delta[2]) const +{ + float t = 1.0f - k4 * r_sq; + if (t >= 0.0f) { + distort_uv(uv, t, delta); + return true; } - if ((t = 1.0f - this->m_kb4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - ln[4] = (u * d + 0.5f) * width - 0.5f, ln[5] = (v * d + 0.5f) * height - 0.5f; - end = 1; + else + return false; + +} + +void ScreenLensDistortionOperation::accumulate(MemoryBuffer *buffer, + int a, int b, + float r_sq, const float uv[2], + const float delta[3][2], + float sum[4], int count[3]) const +{ + float color[4]; + + float dsf = len_v2v2(delta[a], delta[b]) + 1.0f; + int ds = m_jitter ? (dsf < 4.0f ? 2 : (int)sqrtf(dsf)) : (int)dsf; + float sd = 1.0f / (float)ds; + + float k4 = m_k4[a]; + float dk4 = m_dk4[a]; + + for (float z = 0; z < ds; ++z) { + float tz = (z + (m_jitter ? BLI_frand() : 0.5f)) * sd; + float t = 1.0f - (k4 + tz * dk4) * r_sq; + + float xy[2]; + distort_uv(uv, t, xy); + buffer->readBilinear(color, xy[0], xy[1]); + + sum[a] += (1.0f - tz) * color[a], sum[b] += tz * color[b]; + ++count[a]; + ++count[b]; } +} - if (sta && mid && end) { - float jit = this->m_data->jit; - float z; - float color[4]; - { - // RG - const int dx = ln[2] - ln[0], dy = ln[3] - ln[1]; - const float dsf = sqrtf((float)dx * dx + dy * dy) + 1.0f; - const int ds = (int)(jit ? ((dsf < 4.0f) ? 2.0f : sqrtf(dsf)) : dsf); - const float sd = 1.0f / (float)ds; - - for (z = 0; z < ds; ++z) { - const float tz = (z + (jit ? BLI_frand() : 0.5f)) * sd; - t = 1.0f - (this->m_kr4 + tz * this->m_drg) * uv_dot; - d = 1.0f / (1.0f + sqrtf(t)); - const float nx = (u * d + 0.5f) * width - 0.5f; - const float ny = (v * d + 0.5f) * height - 0.5f; - buffer->readBilinear(color, nx, ny); - tc[0] += (1.0f - tz) * color[0], tc[1] += tz * color[1]; - dr++, dg++; - } - } - { - // GB - const int dx = ln[4] - ln[2], dy = ln[5] - ln[3]; - const float dsf = sqrtf((float)dx * dx + dy * dy) + 1.0f; - const int ds = (int)(jit ? ((dsf < 4.0f) ? 2.0f : sqrtf(dsf)) : dsf); - const float sd = 1.0f / (float)ds; - - for (z = 0; z < ds; ++z) { - const float tz = (z + (jit ? BLI_frand() : 0.5f)) * sd; - t = 1.0f - (this->m_kg4 + tz * this->m_dgb) * uv_dot; - d = 1.0f / (1.0f + sqrtf(t)); - const float nx = (u * d + 0.5f) * width - 0.5f; - const float ny = (v * d + 0.5f) * height - 0.5f; - buffer->readBilinear(color, nx, ny); - tc[1] += (1.0f - tz) * color[1], tc[2] += tz * color[2]; - dg++, db++; - } +void ScreenLensDistortionOperation::executePixel(float output[4], int x, int y, void *data) +{ + MemoryBuffer *buffer = (MemoryBuffer *)data; + float xy[2] = { (float)x, (float)y }; + float uv[2]; + get_uv(xy, uv); + float uv_dot = len_squared_v2(uv); - } - if (dr) output[0] = 2.0f * tc[0] / (float)dr; - if (dg) output[1] = 2.0f * tc[1] / (float)dg; - if (db) output[2] = 2.0f * tc[2] / (float)db; + int count[3] = { 0, 0, 0 }; + float delta[3][2]; + float sum[4] = { 0, 0, 0, 0 }; + bool valid_r = get_delta(uv_dot, m_k4[0], uv, delta[0]); + bool valid_g = get_delta(uv_dot, m_k4[1], uv, delta[1]); + bool valid_b = get_delta(uv_dot, m_k4[2], uv, delta[2]); + + if (valid_r && valid_g && valid_b) { + accumulate(buffer, 0, 1, uv_dot, uv, delta, sum, count); + accumulate(buffer, 1, 2, uv_dot, uv, delta, sum, count); + + if (count[0]) output[0] = 2.0f * sum[0] / (float)count[0]; + if (count[1]) output[1] = 2.0f * sum[1] / (float)count[1]; + if (count[2]) output[2] = 2.0f * sum[2] / (float)count[2]; + /* set alpha */ output[3] = 1.0f; } @@ -145,44 +193,19 @@ void ScreenLensDistortionOperation::deinitExecution() this->m_inputProgram = NULL; } -void ScreenLensDistortionOperation::determineUV(float result[6], float x, float y, float distortion, float dispersion) -{ - if (!this->m_valuesAvailable) { - updateVariables(distortion, dispersion); - } - determineUV(result, x, y); -} - void ScreenLensDistortionOperation::determineUV(float result[6], float x, float y) const { - const float height = this->getHeight(); - const float width = this->getWidth(); - - result[0] = x; - result[1] = y; - result[2] = x; - result[3] = y; - result[4] = x; - result[5] = y; - - float d, t; - const float v = this->m_sc * ((y + 0.5f) - this->m_cy) / this->m_cy; - const float u = this->m_sc * ((x + 0.5f) - this->m_cx) / this->m_cx; - const float uv_dot = u * u + v * v; - - if ((t = 1.0f - this->m_kr4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - result[0] = (u * d + 0.5f) * width - 0.5f, result[1] = (v * d + 0.5f) * height - 0.5f; - } - if ((t = 1.0f - this->m_kg4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - result[2] = (u * d + 0.5f) * width - 0.5f, result[3] = (v * d + 0.5f) * height - 0.5f; - } - if ((t = 1.0f - this->m_kb4 * uv_dot) >= 0.0f) { - d = 1.0f / (1.0f + sqrtf(t)); - result[4] = (u * d + 0.5f) * width - 0.5f, result[5] = (v * d + 0.5f) * height - 0.5f; - } + float xy[2] = { x, y }; + float uv[2]; + get_uv(xy, uv); + float uv_dot = len_squared_v2(uv); + copy_v2_v2(result+0, xy); + copy_v2_v2(result+2, xy); + copy_v2_v2(result+4, xy); + get_delta(uv_dot, m_k4[0], uv, result+0); + get_delta(uv_dot, m_k4[1], uv, result+2); + get_delta(uv_dot, m_k4[2], uv, result+4); } bool ScreenLensDistortionOperation::determineDependingAreaOfInterest(rcti *input, ReadBufferOperation *readOperation, rcti *output) @@ -202,58 +225,88 @@ bool ScreenLensDistortionOperation::determineDependingAreaOfInterest(rcti *input if (operation->determineDependingAreaOfInterest(&newInputValue, readOperation, output) ) { return true; } - -#define UPDATE_INPUT { \ - newInput.xmin = min_ffff(newInput.xmin, coords[0], coords[2], coords[4]); \ - newInput.ymin = min_ffff(newInput.ymin, coords[1], coords[3], coords[5]); \ - newInput.xmax = max_ffff(newInput.xmax, coords[0], coords[2], coords[4]); \ - newInput.ymax = max_ffff(newInput.ymax, coords[1], coords[3], coords[5]); \ - } (void)0 + /* XXX the original method of estimating the area-of-interest does not work + * it assumes a linear increase/decrease of mapped coordinates, which does not + * yield correct results for the area and leaves uninitialized buffer areas. + * So now just use the full image area, which may not be as efficient but works at least ... + */ +#if 1 + rcti imageInput; + + operation = getInputOperation(0); + imageInput.xmax = operation->getWidth(); + imageInput.xmin = 0; + imageInput.ymax = operation->getHeight(); + imageInput.ymin = 0; + + if (operation->determineDependingAreaOfInterest(&imageInput, readOperation, output) ) { + return true; + } + return false; +#else rcti newInput; const float margin = 2; - float coords[6]; - if (m_valuesAvailable) { - determineUV(coords, input->xmin, input->ymin); - newInput.xmin = coords[0]; - newInput.ymin = coords[1]; - newInput.xmax = coords[0]; - newInput.ymax = coords[1]; - UPDATE_INPUT; - determineUV(coords, input->xmin, input->ymax); - UPDATE_INPUT; - determineUV(coords, input->xmax, input->ymax); - UPDATE_INPUT; - determineUV(coords, input->xmax, input->ymin); - UPDATE_INPUT; + + BLI_rcti_init_minmax(&newInput); + + if (m_dispersion_const && m_distortion_const) { + /* update from fixed distortion/dispersion */ +#define UPDATE_INPUT(x, y) \ + { \ + float coords[6]; \ + determineUV(coords, x, y); \ + newInput.xmin = min_ffff(newInput.xmin, coords[0], coords[2], coords[4]); \ + newInput.ymin = min_ffff(newInput.ymin, coords[1], coords[3], coords[5]); \ + newInput.xmax = max_ffff(newInput.xmax, coords[0], coords[2], coords[4]); \ + newInput.ymax = max_ffff(newInput.ymax, coords[1], coords[3], coords[5]); \ + } (void)0 + + UPDATE_INPUT(input->xmin, input->xmax); + UPDATE_INPUT(input->xmin, input->ymax); + UPDATE_INPUT(input->xmax, input->ymax); + UPDATE_INPUT(input->xmax, input->ymin); + +#undef UPDATE_INPUT } else { - determineUV(coords, input->xmin, input->ymin, 1.0f, 1.0f); - newInput.xmin = coords[0]; - newInput.ymin = coords[1]; - newInput.xmax = coords[0]; - newInput.ymax = coords[1]; - UPDATE_INPUT; - determineUV(coords, input->xmin, input->ymin, -1.0f, 1.0f); - UPDATE_INPUT; - - determineUV(coords, input->xmin, input->ymax, -1.0f, 1.0f); - UPDATE_INPUT; - determineUV(coords, input->xmin, input->ymax, 1.0f, 1.0f); - UPDATE_INPUT; - - determineUV(coords, input->xmax, input->ymax, -1.0f, 1.0f); - UPDATE_INPUT; - determineUV(coords, input->xmax, input->ymax, 1.0f, 1.0f); - UPDATE_INPUT; + /* use maximum dispersion 1.0 if not const */ + float dispersion = m_dispersion_const ? m_dispersion : 1.0f; + +#define UPDATE_INPUT(x, y, distortion) \ + { \ + float coords[6]; \ + updateVariables(distortion, dispersion); \ + determineUV(coords, x, y); \ + newInput.xmin = min_ffff(newInput.xmin, coords[0], coords[2], coords[4]); \ + newInput.ymin = min_ffff(newInput.ymin, coords[1], coords[3], coords[5]); \ + newInput.xmax = max_ffff(newInput.xmax, coords[0], coords[2], coords[4]); \ + newInput.ymax = max_ffff(newInput.ymax, coords[1], coords[3], coords[5]); \ + } (void)0 - determineUV(coords, input->xmax, input->ymin, -1.0f, 1.0f); - UPDATE_INPUT; - determineUV(coords, input->xmax, input->ymin, 1.0f, 1.0f); - UPDATE_INPUT; - } + if (m_distortion_const) { + /* update from fixed distortion */ + UPDATE_INPUT(input->xmin, input->xmax, m_distortion); + UPDATE_INPUT(input->xmin, input->ymax, m_distortion); + UPDATE_INPUT(input->xmax, input->ymax, m_distortion); + UPDATE_INPUT(input->xmax, input->ymin, m_distortion); + } + else { + /* update from min/max distortion (-1..1) */ + UPDATE_INPUT(input->xmin, input->xmax, -1.0f); + UPDATE_INPUT(input->xmin, input->ymax, -1.0f); + UPDATE_INPUT(input->xmax, input->ymax, -1.0f); + UPDATE_INPUT(input->xmax, input->ymin, -1.0f); + + UPDATE_INPUT(input->xmin, input->xmax, 1.0f); + UPDATE_INPUT(input->xmin, input->ymax, 1.0f); + UPDATE_INPUT(input->xmax, input->ymax, 1.0f); + UPDATE_INPUT(input->xmax, input->ymin, 1.0f); #undef UPDATE_INPUT + } + } + newInput.xmin -= margin; newInput.ymin -= margin; newInput.xmax += margin; @@ -264,39 +317,22 @@ bool ScreenLensDistortionOperation::determineDependingAreaOfInterest(rcti *input return true; } return false; +#endif } void ScreenLensDistortionOperation::updateVariables(float distortion, float dispersion) { - this->m_kg = max_ff(min_ff(distortion, 1.0f), -0.999f); + m_k[1] = max_ff(min_ff(distortion, 1.0f), -0.999f); // smaller dispersion range for somewhat more control - const float d = 0.25f * max_ff(min_ff(dispersion, 1.0f), 0.0f); - this->m_kr = max_ff(min_ff((this->m_kg + d), 1.0f), -0.999f); - this->m_kb = max_ff(min_ff((this->m_kg - d), 1.0f), -0.999f); - this->m_maxk = max_fff(this->m_kr, this->m_kg, this->m_kb); - this->m_sc = (this->m_data->fit && (this->m_maxk > 0.0f)) ? (1.0f / (1.0f + 2.0f * this->m_maxk)) : - (1.0f / (1.0f + this->m_maxk)); - this->m_drg = 4.0f * (this->m_kg - this->m_kr); - this->m_dgb = 4.0f * (this->m_kb - this->m_kg); - - this->m_kr4 = this->m_kr * 4.0f; - this->m_kg4 = this->m_kg * 4.0f; - this->m_kb4 = this->m_kb * 4.0f; -} + float d = 0.25f * max_ff(min_ff(dispersion, 1.0f), 0.0f); + m_k[0] = max_ff(min_ff((m_k[1] + d), 1.0f), -0.999f); + m_k[2] = max_ff(min_ff((m_k[1] - d), 1.0f), -0.999f); + m_maxk = max_fff(m_k[0], m_k[1], m_k[2]); + m_sc = (m_fit && (m_maxk > 0.0f)) ? (1.0f / (1.0f + 2.0f * m_maxk)) : + (1.0f / (1.0f + m_maxk)); + m_dk4[0] = 4.0f * (m_k[1] - m_k[0]); + m_dk4[1] = 4.0f * (m_k[2] - m_k[1]); + m_dk4[2] = 0.0f; /* unused */ -void ScreenLensDistortionOperation::updateDispersionAndDistortion() -{ - if (this->m_valuesAvailable) return; - - this->lockMutex(); - if (!this->m_valuesAvailable) { - float result[4]; - this->getInputSocketReader(1)->readSampled(result, 0, 0, COM_PS_NEAREST); - this->m_distortion = result[0]; - this->getInputSocketReader(2)->readSampled(result, 0, 0, COM_PS_NEAREST); - this->m_dispersion = result[0]; - updateVariables(this->m_distortion, this->m_dispersion); - this->m_valuesAvailable = true; - } - this->unlockMutex(); + mul_v3_v3fl(m_k4, m_k, 4.0f); } diff --git a/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.h b/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.h index 2e2105c764d..c35bbdef4d0 100644 --- a/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.h +++ b/source/blender/compositor/operations/COM_ScreenLensDistortionOperation.h @@ -32,16 +32,18 @@ private: */ SocketReader *m_inputProgram; - NodeLensDist *m_data; + bool m_fit; + bool m_jitter; float m_dispersion; float m_distortion; - bool m_valuesAvailable; - float m_kr, m_kg, m_kb; - float m_kr4, m_kg4, m_kb4; + bool m_dispersion_const; + bool m_distortion_const; + bool m_variables_ready; + float m_k[3]; + float m_k4[3]; + float m_dk4[3]; float m_maxk; - float m_drg; - float m_dgb; float m_sc, m_cx, m_cy; public: ScreenLensDistortionOperation(); @@ -62,27 +64,25 @@ public: */ void deinitExecution(); - void setData(NodeLensDist *data) { this->m_data = data; } + void setFit(bool fit) { m_fit = fit; } + void setJitter(bool jitter) { m_jitter = jitter; } + + /** Set constant distortion value */ + void setDistortion(float distortion); + /** Set constant dispersion value */ + void setDispersion(float dispersion); bool determineDependingAreaOfInterest(rcti *input, ReadBufferOperation *readOperation, rcti *output); - /** - * @brief Set the distortion and dispersion and precalc some values - * @param distortion - * @param dispersion - */ - void setDistortionAndDispersion(float distortion, float dispersion) { - this->m_distortion = distortion; - this->m_dispersion = dispersion; - updateVariables(distortion, dispersion); - this->m_valuesAvailable = true; - } - private: void determineUV(float result[6], float x, float y) const; - void determineUV(float result[6], float x, float y, float distortion, float dispersion); - void updateDispersionAndDistortion(); void updateVariables(float distortion, float dispersion); + void get_uv(const float xy[2], float uv[2]) const; + void distort_uv(const float uv[2], float t, float xy[2]) const; + bool get_delta(float r_sq, float k4, const float uv[2], float delta[2]) const; + void accumulate(MemoryBuffer *buffer, int a, int b, + float r_sq, const float uv[2], const float delta[3][2], + float sum[4], int count[3]) const; }; #endif |