Added support for Instancing

Former-commit-id: 62f5f3186423b01f7a0ac2762969dca12bea1327
This commit is contained in:
Lynix 2013-02-26 01:40:57 +01:00
parent 9b2eb8ce3f
commit 3b0751fb88
6 changed files with 337 additions and 24 deletions

View File

@ -28,8 +28,10 @@ enum nzOpenGLExtension
{
nzOpenGLExtension_AnisotropicFilter,
nzOpenGLExtension_DebugOutput,
nzOpenGLExtension_DrawInstanced,
nzOpenGLExtension_FP64,
nzOpenGLExtension_FrameBufferObject,
nzOpenGLExtension_InstancedArray,
nzOpenGLExtension_PixelBufferObject,
nzOpenGLExtension_SamplerObjects,
nzOpenGLExtension_SeparateShaderObjects,
@ -139,9 +141,11 @@ NAZARA_API extern PFNGLDEPTHMASKPROC glDepthMask;
NAZARA_API extern PFNGLDISABLEPROC glDisable;
NAZARA_API extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray;
NAZARA_API extern PFNGLDRAWARRAYSPROC glDrawArrays;
NAZARA_API extern PFNGLDRAWARRAYSINSTANCEDPROC glDrawArraysInstanced;
NAZARA_API extern PFNGLDRAWBUFFERPROC glDrawBuffer;
NAZARA_API extern PFNGLDRAWBUFFERSPROC glDrawBuffers;
NAZARA_API extern PFNGLDRAWELEMENTSPROC glDrawElements;
NAZARA_API extern PFNGLDRAWELEMENTSINSTANCEDPROC glDrawElementsInstanced;
NAZARA_API extern PFNGLENABLEPROC glEnable;
NAZARA_API extern PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
NAZARA_API extern PFNGLENDQUERYPROC glEndQuery;
@ -234,6 +238,7 @@ NAZARA_API extern PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv;
NAZARA_API extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
NAZARA_API extern PFNGLUSEPROGRAMPROC glUseProgram;
NAZARA_API extern PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f;
NAZARA_API extern PFNGLVERTEXATTRIBDIVISORPROC glVertexAttribDivisor;
NAZARA_API extern PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
NAZARA_API extern PFNGLVIEWPORTPROC glViewport;
#if defined(NAZARA_PLATFORM_WINDOWS)

View File

@ -27,16 +27,27 @@ class NzVertexDeclaration;
class NAZARA_API NzRenderer
{
public:
struct InstancingData
{
NzMatrix4f worldMatrix;
};
NzRenderer() = delete;
~NzRenderer() = delete;
static void Clear(unsigned long flags = nzRendererClear_Color | nzRendererClear_Depth);
static void DrawIndexedPrimitives(nzPrimitiveType primitive, unsigned int firstIndex, unsigned int indexCount);
static void DrawIndexedPrimitivesInstanced(unsigned int instanceCount, nzPrimitiveType primitive, unsigned int firstIndex, unsigned int indexCount);
static void DrawPrimitives(nzPrimitiveType primitive, unsigned int firstVertex, unsigned int vertexCount);
static void DrawPrimitivesInstanced(unsigned int instanceCount, nzPrimitiveType primitive, unsigned int firstVertex, unsigned int vertexCount);
static void Enable(nzRendererParameter parameter, bool enable);
static void FillInstancingBuffer(const InstancingData* instancingData, unsigned int instanceCount);
static void Flush();
static float GetLineWidth();
//static NzMatrix4f GetMatrix(nzMatrixCombination combination);
static NzMatrix4f GetMatrix(nzMatrixType type);

View File

@ -111,24 +111,25 @@ bool NzGLSLShader::Create()
return false;
}
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+8, "InstanceMatrix");
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_Position], "VertexPosition");
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_Normal], "VertexNormal");
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_Diffuse], "VertexDiffuse");
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_Tangent], "VertexTangent");
NzString uniform;
uniform.Reserve(16); // 14 + 2
uniform = "VertexTexCoord";
char texCoordsUniform[] = "VertexTexCoord*";
unsigned int maxTexCoords = NzRenderer::GetMaxTextureUnits();
unsigned int maxTexCoords = std::min(8U, NzRenderer::GetMaxTextureUnits());
for (unsigned int i = 0; i < maxTexCoords; ++i)
{
NzString uniformName = uniform + NzString::Number(i);
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+i, uniformName.GetConstBuffer());
texCoordsUniform[14] = '0' + i;
glBindAttribLocation(m_program, NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+i, texCoordsUniform);
}
if (NzRenderer::HasCapability(nzRendererCap_MultipleRenderTargets))
{
NzString uniform;
uniform.Reserve(14); // 12 + 2
uniform = "RenderTarget";

View File

@ -413,6 +413,37 @@ bool NzOpenGL::Initialize()
}
}
// DrawInstanced
if (s_openglVersion >= 330)
{
try
{
glDrawArraysInstanced = reinterpret_cast<PFNGLDRAWARRAYSINSTANCEDPROC>(LoadEntry("glDrawArraysInstanced"));
glDrawElementsInstanced = reinterpret_cast<PFNGLDRAWELEMENTSINSTANCEDPROC>(LoadEntry("glDrawElementsInstanced"));
s_openGLextensions[nzOpenGLExtension_DrawInstanced] = true;
}
catch (const std::exception& e)
{
NazaraWarning("Failed to load GL_ARB_draw_instanced: " + NzString(e.what()));
}
}
if (!s_openGLextensions[nzOpenGLExtension_DrawInstanced] && IsSupported("GL_ARB_draw_instanced"))
{
try
{
glDrawArraysInstanced = reinterpret_cast<PFNGLDRAWARRAYSINSTANCEDARBPROC>(LoadEntry("glDrawArraysInstancedARB"));
glDrawElementsInstanced = reinterpret_cast<PFNGLDRAWELEMENTSINSTANCEDARBPROC>(LoadEntry("glDrawElementsInstancedARB"));
s_openGLextensions[nzOpenGLExtension_DrawInstanced] = true;
}
catch (const std::exception& e)
{
NazaraWarning("Failed to load GL_ARB_draw_instanced: " + NzString(e.what()));
}
}
// FP64
if (s_openglVersion >= 400 || IsSupported("GL_ARB_gpu_shader_fp64"))
{
@ -460,6 +491,35 @@ bool NzOpenGL::Initialize()
}
}
// InstancedArray
if (s_openglVersion >= 330)
{
try
{
glVertexAttribDivisor = reinterpret_cast<PFNGLVERTEXATTRIBDIVISORPROC>(LoadEntry("glVertexAttribDivisor"));
s_openGLextensions[nzOpenGLExtension_InstancedArray] = true;
}
catch (const std::exception& e)
{
NazaraWarning("Failed to load GL_ARB_instanced_arrays: " + NzString(e.what()));
}
}
if (!s_openGLextensions[nzOpenGLExtension_InstancedArray] && IsSupported("GL_ARB_instanced_arrays"))
{
try
{
glVertexAttribDivisor = reinterpret_cast<PFNGLVERTEXATTRIBDIVISORARBPROC>(LoadEntry("glVertexAttribDivisorARB"));
s_openGLextensions[nzOpenGLExtension_InstancedArray] = true;
}
catch (const std::exception& e)
{
NazaraWarning("Failed to load GL_ARB_instanced_arrays: " + NzString(e.what()));
}
}
// PixelBufferObject
s_openGLextensions[nzOpenGLExtension_PixelBufferObject] = (s_openglVersion >= 210 || IsSupported("GL_ARB_pixel_buffer_object"));
@ -980,9 +1040,11 @@ PFNGLDEPTHMASKPROC glDepthMask = nullptr;
PFNGLDISABLEPROC glDisable = nullptr;
PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray = nullptr;
PFNGLDRAWARRAYSPROC glDrawArrays = nullptr;
PFNGLDRAWARRAYSINSTANCEDPROC glDrawArraysInstanced = nullptr;
PFNGLDRAWBUFFERPROC glDrawBuffer = nullptr;
PFNGLDRAWBUFFERSPROC glDrawBuffers = nullptr;
PFNGLDRAWELEMENTSPROC glDrawElements = nullptr;
PFNGLDRAWELEMENTSINSTANCEDPROC glDrawElementsInstanced = nullptr;
PFNGLENABLEPROC glEnable = nullptr;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = nullptr;
PFNGLENDQUERYPROC glEndQuery = nullptr;
@ -1075,6 +1137,7 @@ PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv = nullptr;
PFNGLUNMAPBUFFERPROC glUnmapBuffer = nullptr;
PFNGLUSEPROGRAMPROC glUseProgram = nullptr;
PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f = nullptr;
PFNGLVERTEXATTRIBDIVISORPROC glVertexAttribDivisor = nullptr;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = nullptr;
PFNGLVIEWPORTPROC glViewport = nullptr;
#if defined(NAZARA_PLATFORM_WINDOWS)

View File

@ -60,9 +60,8 @@ namespace
std::map<VAO_Key, unsigned int> s_vaos;
std::vector<TextureUnit> s_textureUnits;
NzBuffer* s_instancingBuffer = nullptr;
NzMatrix4f s_matrix[totalMatrixCount];
int s_matrixLocation[totalMatrixCount];
bool s_matrixUpdated[totalMatrixCount];
nzBlendFunc s_srcBlend;
nzBlendFunc s_dstBlend;
nzFaceCulling s_faceCulling;
@ -79,12 +78,15 @@ namespace
const NzShader* s_shader;
const NzVertexBuffer* s_vertexBuffer;
const NzVertexDeclaration* s_vertexDeclaration;
bool s_vaoUpdated;
bool s_capabilities[nzRendererCap_Max+1];
bool s_instancingEnabled;
bool s_matrixUpdated[totalMatrixCount];
bool s_stencilFuncUpdated;
bool s_stencilOpUpdated;
bool s_useSamplerObjects;
bool s_useVertexArrayObjects;
bool s_vaoUpdated;
int s_matrixLocation[totalMatrixCount];
unsigned int s_maxRenderTarget;
unsigned int s_maxTextureUnit;
unsigned int s_stencilReference;
@ -147,6 +149,12 @@ void NzRenderer::DrawIndexedPrimitives(nzPrimitiveType primitive, unsigned int f
return;
}
if (s_instancingEnabled)
{
glDisableVertexAttribArray(NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+8);
s_instancingEnabled = false;
}
if (s_indexBuffer->IsSequential())
glDrawArrays(NzOpenGL::PrimitiveType[primitive], s_indexBuffer->GetStartIndex(), s_indexBuffer->GetIndexCount());
else
@ -168,6 +176,81 @@ void NzRenderer::DrawIndexedPrimitives(nzPrimitiveType primitive, unsigned int f
}
}
void NzRenderer::DrawIndexedPrimitivesInstanced(unsigned int instanceCount, nzPrimitiveType primitive, unsigned int firstIndex, unsigned int indexCount)
{
#ifdef NAZARA_DEBUG
if (NzContext::GetCurrent() == nullptr)
{
NazaraError("No active context");
return;
}
if (primitive > nzPrimitiveType_Max)
{
NazaraError("Primitive type out of enum");
return;
}
#endif
#if NAZARA_RENDERER_SAFE
if (!s_capabilities[nzRendererCap_Instancing])
{
NazaraError("Instancing not supported");
return;
}
if (!s_indexBuffer)
{
NazaraError("No index buffer");
return;
}
if (instanceCount == 0)
{
NazaraError("Instance count must be over 0");
return;
}
if (instanceCount > NAZARA_RENDERER_INSTANCING_MAX)
{
NazaraError("Instance count is over maximum instance count (" + NzString::Number(instanceCount) + " >= " + NzString::Number(NAZARA_RENDERER_INSTANCING_MAX) + ')');
return;
}
#endif
if (!EnsureStateUpdate())
{
NazaraError("Failed to update states");
return;
}
if (!s_instancingEnabled)
{
glEnableVertexAttribArray(NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+8);
s_instancingEnabled = true;
}
if (s_indexBuffer->IsSequential())
glDrawArraysInstanced(NzOpenGL::PrimitiveType[primitive], s_indexBuffer->GetStartIndex(), s_indexBuffer->GetIndexCount(), instanceCount);
else
{
GLenum type;
const nzUInt8* ptr = reinterpret_cast<const nzUInt8*>(s_indexBuffer->GetPointer());
if (s_indexBuffer->HasLargeIndices())
{
ptr += firstIndex*sizeof(nzUInt32);
type = GL_UNSIGNED_INT;
}
else
{
ptr += firstIndex*sizeof(nzUInt16);
type = GL_UNSIGNED_SHORT;
}
glDrawElementsInstanced(NzOpenGL::PrimitiveType[primitive], indexCount, type, ptr, instanceCount);
}
}
void NzRenderer::DrawPrimitives(nzPrimitiveType primitive, unsigned int firstVertex, unsigned int vertexCount)
{
#ifdef NAZARA_DEBUG
@ -190,9 +273,66 @@ void NzRenderer::DrawPrimitives(nzPrimitiveType primitive, unsigned int firstVer
return;
}
if (s_instancingEnabled)
{
glDisableVertexAttribArray(NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+8);
s_instancingEnabled = false;
}
glDrawArrays(NzOpenGL::PrimitiveType[primitive], firstVertex, vertexCount);
}
void NzRenderer::DrawPrimitivesInstanced(unsigned int instanceCount, nzPrimitiveType primitive, unsigned int firstVertex, unsigned int vertexCount)
{
#ifdef NAZARA_DEBUG
if (NzContext::GetCurrent() == nullptr)
{
NazaraError("No active context");
return;
}
if (primitive > nzPrimitiveType_Max)
{
NazaraError("Primitive type out of enum");
return;
}
#endif
#if NAZARA_RENDERER_SAFE
if (!s_capabilities[nzRendererCap_Instancing])
{
NazaraError("Instancing not supported");
return;
}
if (instanceCount == 0)
{
NazaraError("Instance count must be over 0");
return;
}
if (instanceCount > NAZARA_RENDERER_INSTANCING_MAX)
{
NazaraError("Instance count is over maximum instance count (" + NzString::Number(instanceCount) + " >= " + NzString::Number(NAZARA_RENDERER_INSTANCING_MAX) + ')');
return;
}
#endif
if (!EnsureStateUpdate())
{
NazaraError("Failed to update states");
return;
}
if (!s_instancingEnabled)
{
glEnableVertexAttribArray(NzOpenGL::AttributeIndex[nzElementUsage_TexCoord]+8);
s_instancingEnabled = true;
}
glDrawArraysInstanced(NzOpenGL::PrimitiveType[primitive], firstVertex, vertexCount, instanceCount);
}
void NzRenderer::Enable(nzRendererParameter parameter, bool enable)
{
#ifdef NAZARA_DEBUG
@ -229,6 +369,43 @@ void NzRenderer::Enable(nzRendererParameter parameter, bool enable)
}
}
void NzRenderer::FillInstancingBuffer(const NzRenderer::InstancingData* instancingData, unsigned int instanceCount)
{
#if NAZARA_RENDERER_SAFE
if (!s_capabilities[nzRendererCap_Instancing])
{
NazaraError("Instancing not supported");
return;
}
if (!instancingData)
{
NazaraError("Instancing data must be valid");
return;
}
if (instanceCount == 0)
{
NazaraError("Instance count must be over 0");
return;
}
if (instanceCount > NAZARA_RENDERER_INSTANCING_MAX)
{
NazaraError("Instance count is over maximum instance count (" + NzString::Number(instanceCount) + " >= " + NzString::Number(NAZARA_RENDERER_INSTANCING_MAX) + ')');
return;
}
#endif
if (!s_instancingBuffer->Fill(instancingData, 0, instanceCount))
NazaraError("Failed to fill instancing buffer");
}
void NzRenderer::Flush()
{
glFlush();
}
float NzRenderer::GetLineWidth()
{
#ifdef NAZARA_DEBUG
@ -387,6 +564,8 @@ bool NzRenderer::Initialize(bool initializeDebugDrawer)
NzContext::EnsureContext();
NzBuffer::SetBufferFunction(nzBufferStorage_Hardware, HardwareBufferFunction);
for (unsigned int i = 0; i < totalMatrixCount; ++i)
{
s_matrix[i].MakeIdentity();
@ -398,8 +577,8 @@ bool NzRenderer::Initialize(bool initializeDebugDrawer)
s_capabilities[nzRendererCap_AnisotropicFilter] = NzOpenGL::IsSupported(nzOpenGLExtension_AnisotropicFilter);
s_capabilities[nzRendererCap_FP64] = NzOpenGL::IsSupported(nzOpenGLExtension_FP64);
s_capabilities[nzRendererCap_HardwareBuffer] = true; // Natif depuis OpenGL 1.5
// MultipleRenderTargets (Techniquement natif depuis OpenGL 2.0 mais inutile sans glBindFragDataLocation)
s_capabilities[nzRendererCap_MultipleRenderTargets] = (glBindFragDataLocation != nullptr);
s_capabilities[nzRendererCap_Instancing] = NzOpenGL::IsSupported(nzOpenGLExtension_DrawInstanced) && NzOpenGL::IsSupported(nzOpenGLExtension_InstancedArray);
s_capabilities[nzRendererCap_MultipleRenderTargets] = (glBindFragDataLocation != nullptr); // Natif depuis OpenGL 2.0 mais inutile sans glBindFragDataLocation
s_capabilities[nzRendererCap_OcclusionQuery] = true; // Natif depuis OpenGL 1.5
s_capabilities[nzRendererCap_PixelBufferObject] = NzOpenGL::IsSupported(nzOpenGLExtension_PixelBufferObject);
s_capabilities[nzRendererCap_RenderTexture] = NzOpenGL::IsSupported(nzOpenGLExtension_FrameBufferObject);
@ -418,6 +597,35 @@ bool NzRenderer::Initialize(bool initializeDebugDrawer)
else
s_maxAnisotropyLevel = 1;
if (s_capabilities[nzRendererCap_Instancing])
{
s_instancingBuffer = new NzBuffer(nzBufferType_Vertex);
if (s_instancingBuffer->Create(NAZARA_RENDERER_INSTANCING_MAX, sizeof(InstancingData), nzBufferStorage_Hardware, nzBufferUsage_Dynamic))
{
static_cast<NzHardwareBuffer*>(s_instancingBuffer->GetImpl())->Bind();
unsigned int instanceMatrixIndex = NzOpenGL::AttributeIndex[nzElementUsage_TexCoord] + 8;
for (unsigned int i = 0; i < 4; ++i)
{
glVertexAttribPointer(instanceMatrixIndex, 4, GL_FLOAT, GL_FALSE, sizeof(InstancingData), reinterpret_cast<GLvoid*>(offsetof(InstancingData, worldMatrix) + sizeof(float)*4*i));
glVertexAttribDivisor(instanceMatrixIndex, 1);
instanceMatrixIndex++;
}
s_instancingEnabled = false;
}
else
{
s_capabilities[nzRendererCap_Instancing] = false;
delete s_instancingBuffer;
s_instancingBuffer = nullptr;
NazaraWarning("Failed to create instancing buffer, disabled instancing.");
}
}
if (s_capabilities[nzRendererCap_MultipleRenderTargets])
{
GLint maxDrawBuffers;
@ -467,8 +675,6 @@ bool NzRenderer::Initialize(bool initializeDebugDrawer)
s_vertexBuffer = nullptr;
s_vertexDeclaration = nullptr;
NzBuffer::SetBufferFunction(nzBufferStorage_Hardware, HardwareBufferFunction);
if (initializeDebugDrawer && !NzDebugDrawer::Initialize())
NazaraWarning("Failed to initialize debug drawer"); // Non-critique
@ -1040,6 +1246,13 @@ void NzRenderer::Uninitialize()
NzContext::EnsureContext();
// Libération du buffer d'instancing
if (s_instancingBuffer)
{
delete s_instancingBuffer;
s_instancingBuffer = nullptr;
}
// Libération des VAOs
for (auto it = s_vaos.begin(); it != s_vaos.end(); ++it)
{

View File

@ -303,14 +303,22 @@ namespace
sourceCode += '\n';
/********************Uniformes********************/
if (flags & nzShaderBuilder_Lighting)
sourceCode += "uniform mat4 WorldMatrix;\n";
if (flags & nzShaderBuilder_Instancing)
sourceCode += "uniform mat4 ViewProjMatrix;\n";
else
{
if (flags & nzShaderBuilder_Lighting)
sourceCode += "uniform mat4 WorldMatrix;\n";
sourceCode += "uniform mat4 WorldViewProjMatrix;\n";
sourceCode += "uniform mat4 WorldViewProjMatrix;\n";
}
sourceCode += '\n';
/********************Entrant********************/
if (flags & nzShaderBuilder_Instancing)
sourceCode += inKW + " mat4 InstanceMatrix;\n";
sourceCode += inKW + " vec3 VertexPosition;\n";
if (flags & nzShaderBuilder_Lighting)
@ -343,31 +351,43 @@ namespace
/********************Code********************/
sourceCode += "void main()\n"
"{\n"
"gl_Position = WorldViewProjMatrix * vec4(VertexPosition, 1.0);\n";
"{\n";
if (flags & nzShaderBuilder_Instancing)
sourceCode += "gl_Position = InstanceMatrix * ViewProjMatrix * vec4(VertexPosition, 1.0);\n";
else
sourceCode += "gl_Position = WorldViewProjMatrix * vec4(VertexPosition, 1.0);\n";
if (flags & nzShaderBuilder_Lighting)
{
sourceCode += "mat3 RotationMatrix = mat3(WorldMatrix);\n";
if (flags & nzShaderBuilder_Instancing)
sourceCode += "mat3 rotationMatrix = mat3(InstanceMatrix);\n";
else
sourceCode += "mat3 rotationMatrix = mat3(WorldMatrix);\n";
if (flags & nzShaderBuilder_NormalMapping)
{
sourceCode += "\n"
"vec3 binormal = cross(VertexNormal, VertexTangent);\n"
"vLightToWorld[0] = normalize(VertexTangent * RotationMatrix);\n"
"vLightToWorld[1] = normalize(binormal * RotationMatrix);\n"
"vLightToWorld[2] = normalize(VertexNormal * RotationMatrix);\n"
"vLightToWorld[0] = normalize(VertexTangent * rotationMatrix);\n"
"vLightToWorld[1] = normalize(binormal * rotationMatrix);\n"
"vLightToWorld[2] = normalize(VertexNormal * rotationMatrix);\n"
"\n";
}
else
sourceCode += "vNormal = normalize(RotationMatrix * VertexNormal);\n";
sourceCode += "vNormal = normalize(rotationMatrix * VertexNormal);\n";
}
if (flags & nzShaderBuilder_DiffuseMapping || flags & nzShaderBuilder_NormalMapping)
sourceCode += "vTexCoord = VertexTexCoord0;\n";
if (flags & nzShaderBuilder_Lighting)
sourceCode += "vWorldPos = vec3(WorldMatrix * vec4(VertexPosition, 1.0));\n";
{
if (flags & nzShaderBuilder_Instancing)
sourceCode += "vWorldPos = vec3(InstanceMatrix * vec4(VertexPosition, 1.0));\n";
else
sourceCode += "vWorldPos = vec3(WorldMatrix * vec4(VertexPosition, 1.0));\n";
}
sourceCode += "}\n";