This type of extensions create geometry on-the-fly at render time. They are used to render objects described by mathematical expressions rather than vertices and faces. This class is defined in mx_geometryextension.h
Code Block |
---|
class CgeometryProceduralExtension : public CbaseExtension { public: virtual bool intersectinitializeForRendering( Cmaxwell::Cobject* objectpMaxwell, const CpointCmaxwell::Cobject& rayOrigin,proceduralObject const) Cvector& dir, real time, const dword subVolumeIndex, Cvector* pNormal, Cvector* pLocalImpact, CfVector &data, Cvector& parametricUVW, Cvector& tangentU, Cvector& tangentV ) = 0;= 0; virtual bool initializePreview( Cmaxwell* pMaxwell, Cmaxwell::Cobject& proceduralObject ) { virtual voidreturn getBoundingBox( Cpoint *bboxPoints, float time ) = 0;true ); } virtual dwordbool getNumSubVolumesintersect( void ) { return 1; }Cmaxwell::Cobject* object, const Cpoint& rayOrigin, const Cvector& dir, real time, dword subVolumeIndex, Cvector* pNormal, Cvector* pLocalImpact, mw::sources::Crandom* threadRandom, CfVector &data, Cvector& parametricUVW, Cvector& tangentU, Cvector& tangentV ) = 0; virtual void getSubVolumeBoundingBoxgetBoundingBox( Cpoint *bboxPoints, dword subVolumeIndex, float time ) = 0; { virtual dword getNumSubVolumes( void ) getBoundingBox( bboxPoints, time{ ); }; return 1; virtual byte getNumberOfUVGenerators( void }; virtual void getSubVolumeBoundingBox( Cpoint *bboxPoints, dword subVolumeIndex, float time ) { getBoundingBox( bboxPoints, returntime 0); }; virtual const char* getUVGeneratorNamebyte getNumberOfUVGenerators( byte indexvoid ) { return NULL0; }; virtual byte getUVForChannelconst char* getUVGeneratorName( CfVector&byte uvw, index ) { return NULL; }; virtual byte getUVForChannel( CfVector& uvw, const Cpoint& point, const Cpoint& normal, dword iGenerator, dword subVolIndex, const Cvector& parametricUVW ) { uvw.assign( 0.f, 0.f, 0.f ); return 1; }; virtual bool isOverlappingBoundingBox( const Cpoint *bboxPoints, dword subVolumeIndex, bool forceHalfTime ) { return true; }; virtual boolreal getProxyDisplayPointsgetSubVolumeArea( const dword& percentsubVolumeIndex, const dword& maxPoints, dword& nPoints, float*& points )Cbase* transformationBase ) //must be fast to evaluate, called many times during render { return false0; }; virtual boolvoid getProxyDisplayLinesgetSubVolumePointAndNormal( const dword& percentsubVolumeIndex, const dwordCpoint& maxLinespoint, dwordCvector& nPointsnormal, float*& points, dword& nLines, dword*& pointsPerLine ) { mw::sources::Crandom* auxRandom, real rndTime )//must be fast to evaluate, called many times during render return{ false; } point.assign( virtual bool getProxyDisplayFaces( const dword& percent, const dword& maxFaces, dword& nPoints, float*& points, dword& nFaces, dword*& faces0.0, 0.0, 0.0 ); normal.assign( 0.0, 0.0, 0.0 ); return; } virtual bool getProxyDisplayPoints( const dword& percent, const dword& maxPoints, dword& nPoints, float*& points ) { return false; } }; |
The actual work of this extension is done in intersect. This is the speed critical routine. Make every effort to optimize this function. Do not allocate memory in it. Avoid any OS and/or SDK function calls in this critical routine. Most of the memory can be pre-allocated in initializeForRendering. If you want to access the global scene pointer, store it in initializeForRendering and use it later. This can save a huge amount of time.
One of the interesting things of these geometries is that they can be split into several subregions or subvolumes. This can increase the efficiency of the intersection tremendously. Instead of having an object with a huge bounding box, this can be divided into several regions, with much more smaller bounding boxes. These regions can be spatially connected or not, and each region could evaluate a different mathematical problem to calculate a point on its surface. Each region or subvolume has its own bounding box, which is calculated in getSubVolumeBoundingBox, and the number of subvolumes is returned in getNumSubVolumes. There is also a function that returns the bounding box of the whole object, getBoundingBox. One of the optional functions, isOverlappingBoundingBox, checks the intersection of two boxes, one sent by the render engine, and the other that of the given subvolume, to optimize the rendering speed. Note that in getSubVolumeBoundingBox, getBoundingBox the returned bounding box in general is not axis-oriented, it should be subvolume-oriented, and because of this eight points must be returned. First the four "lower" ones, and then the four "upper" ones. In isOverlappingBoundingBox the input bounding box is not axis-oriented and is in the local coordinate system of the extension object, and defined by eight points. Don't underestimate the influence of this function. It can make wonders.
These extensions can also have their own custom UV generators. These generators are declared using getNumberOfUVGenerators and getUVGeneratorName. Once declared, they can be used normally with Cmaxwell::Cobject::addChannelUVW( dword uvIndex ) and Cmaxwell::Cobject::generateCustomUVW( dword iChannel, dword iGeneratorType ).The generators are implemented in getUVForChannel. This is also a speed critical function, so it must be optimized very carefully (no file I/O, no OS calls, no SDK calls...).
One of the other three functions getProxyDisplay* should be implemented if the extension writer wants to draw an approximate proxy of the object in the Studio viewport. For example, the MaxwellGrass extension uses getProxyDisplayLines to draw some lines representing grass strands.
intersect
...
Given a ray (direction and origin), calculate and return a point and normal on the surface of the object.
Arguments
Cmaxwell::Cobject& object
: input, reference to the extension object.
const Cpoint& rayOrigin
: input, origin of the ray in local coordinates of the extension object.
const Cvector& rayDir
: input, direction of the ray in local coordinates of the extension object.
real time
: input, ranges from 0.0 to 1.0, time of the frame at which the intersection is evaluated.
const dword subVolumeIndex
: input, index of the current subvolume.
Cvector* pNormal
: output, return here the calculated normal direction to the surface at the intersection point, in local coordinates of the extension object.
Cvector* pLocalImpact
: output, intersection point in local coordinates of the extension object.
CfVector& data
: private, don't use.
Cvector& parametricUVW
: output, in case of analytical surfaces, return here the natural parametric coordinates of the intersection point. Needed later to calculate texture coordinates if using custom UV generators.
Cvector& tangentU, tangentV
: output, tangent vectors to the surface. If P=p(u,v), then tu = dP/du and tv = dP/dv;
Returns
true
on intersection, false
otherwise.
getBoundingBox
virtual void getBoundingBox( Cpoint *bboxPoints, float time ) = 0;
Return the bounding box of the extension object, enclosing all subvolumes. This box should be arbitrarily oriented in order to be the smallest box that surrounds the object.
The ordering of the box points is as follows:
The points must be in local coordinates of the extension object.
Arguments
...
virtual bool getProxyDisplayLines( const dword& percent, const dword& maxLines, dword& nPoints, float*& points, dword& nLines, dword*& pointsPerLine )
{
return false;
}
virtual bool getProxyDisplayFaces( const dword& percent, const dword& maxFaces, dword& nPoints, float*& points, dword& nFaces, dword*& faces )
{
return false;
}
}; |
The actual work of this extension is done in intersect. This is the speed critical routine. Make every effort to optimize this function. Do not allocate memory in it. Avoid any OS and/or SDK function calls in this critical routine. Most of the memory and extension data structures which will be used during the render can be pre-allocated in initializeForRendering. If you want to access the global scene pointer, store it in initializeForRendering and use it later. This can save a huge amount of time.
One of the interesting things about these geometries is that they can be split into several subregions or subvolumes. This can increase the efficiency of the intersection tremendously. Instead of having an object with a huge bounding box, this can be divided into several regions, with much more smaller bounding boxes. These regions can be spatially connected or not, and each region could evaluate a different mathematical problem to calculate a point on its surface. Each region or subvolume has its own bounding box, which is calculated in getSubVolumeBoundingBox, and the number of subvolumes is returned in getNumSubVolumes. There is also a function that returns the bounding box of the whole object, getBoundingBox. One of the optional functions, isOverlappingBoundingBox, checks the intersection of two boxes, one sent by the render engine, and the other that of the given subvolume, to optimize the rendering speed. Note that in getSubVolumeBoundingBox and getBoundingBox the returned bounding box in general is not axis-oriented, it should be subvolume-oriented, and because of this, eight points must be returned. First the four "lower" ones, and then the four "upper" ones. In isOverlappingBoundingBox the input bounding box is not axis-oriented and is in the local coordinate system of the extension object, and defined by eight points. Don't underestimate the influence of this function. It can make wonders.
These extensions can also have their own custom UV generators. These generators are declared using getNumberOfUVGenerators and getUVGeneratorName. Once declared, they can be used normally with Cmaxwell::Cobject::addChannelUVW( dword uvIndex ) and Cmaxwell::Cobject::generateCustomUVW( dword iChannel, dword iGeneratorType ).The generators are implemented in getUVForChannel. This is also a speed critical function, so it must be optimized very carefully (no file I/O, no OS calls, no SDK calls...).
One of the other three functions getProxyDisplay* should be implemented if the extension writer wants to draw an approximate proxy of the object in the Studio viewport. For example, the MaxwellGrass extension uses getProxyDisplayLines to draw some lines representing grass strands.
initializeForRendering
virtual bool
initializeForRendering( Cmaxwell* pMaxwell, Cmaxwell::Cobject& proceduralObject ) = 0;
Called at the initial stage before rendering starts. Useful for pre-allocating everything that will be needed during the render.
Arguments
Cmaxwell* pMaxwell
: input, pointer to the scene.
Cmaxwell::Cobject& proceduralObject
: a reference to the current procedural object.
Returns
true
on success, false
otherwise. false
aborts the render.
initializePreview
virtual bool initializePreview( Cmaxwell* pMaxwell );
Reset all the pointers allocated in getProxyDisplay* and make them ready for reuse.
Arguments
Cmaxwell* pMaxwell
: input, pointer to the scene.
Returns
true
on success, false
otherwise.
intersect
virtual bool intersect( Cmaxwell::Cobject* object, const Cpoint& rayOrigin, const Cvector& rayDir, real time,
const dword subVolumeIndex, Cvector* pNormal, Cvector* pLocalImpact, CfVector& data,
Cvector& parametricUVW, Cvector& tangentU, Cvector& tangentV ) = 0;
Given a ray (direction and origin), calculate and return a point and normal on the surface of the object. If the tangential base cannot be calculated, leave the input vectors tangentU and tangentV as they are.
Arguments
Cmaxwell::Cobject& object
: input, reference to the extension object.
const Cpoint& rayOrigin
: input, origin of the ray in local coordinates of the extension object.
const Cvector& rayDir
: input, direction of the ray in local coordinates of the extension object.
real time
: input, ranges from 0.0 to 1.0, time of the frame at which the intersection is evaluated.
const dword subVolumeIndex
: input, index of the current subvolume.
Cvector* pNormal
: output, return here the calculated normal direction to the surface at the intersection point, in local coordinates of the extension object.
Cvector* pLocalImpact
: output, intersection point in local coordinates of the extension object.
CfVector& data
: private, don't use.
Cvector& parametricUVW
: output, in case of analytical surfaces, return here the natural parametric coordinates of the intersection point. Needed later to calculate texture coordinates if using custom UV generators.
Cvector& tangentU, tangentV
: output, tangent vectors to the surface. If P=p(u,v), then tu = dP/du and tv = dP/dv;
Returns
true
on intersection, false
otherwise.
getBoundingBox
virtual void getBoundingBox( Cpoint* bboxPoints, float time ) = 0;
Return the bounding box of the extension object, enclosing all subvolumes. This box should be arbitrarily oriented in order to be the smallest box that surrounds the object.
The ordering of the box points is as follows:
In the image, points 0 and 6 would be bbmin and bbmax respectively in an axis-aligned bounding box.
The points must be in local coordinates of the extension object.
Arguments
Cpoint* bboxPoints
: output, 0-based array of eight points.
float time
: input, time of evaluation of the box.
Returns
Nothing.
getNumSubVolumes
virtual dword getNumSubVolumes( void );
Return the number of subvolumes that the object is subdivided into.
Arguments
None
Returns
Number of subvolumes.
getSubVolumeBoundingBox
virtual void getSubVolumeBoundingBox( Cpoint* bboxPoints, dword subVolumeIndex, float time );
Return the bounding box of the subvolume, in local coordinates of the extension object, as a 0-based array of eight points, at the given time.
Arguments
Cpoint* bboxPoints
: output, 0-based array of eight points. See getBoundingBox for the point ordering.
dword subVolumeIndex
: input, index of the subvolume to return its box.
float time
: input, time of evaluation of the box.
Returns
Nothing.
...
getNumberOfUVGenerators
virtual
...
byte getNumberOfUVGenerators( void );
Returns the number of implemented custom UV generators.
Arguments
None.
Returns
Number of UV generators. Max 255.
getUVGeneratorName
virtual const char* getUVGeneratorName( byte index );
Return a string with the number name of subvolumes that the object is subdivided intothe UV generator.
Arguments
None
Returns
Number of subvolumes.
getSubVolumeBoundingBox
virtual void getSubVolumeBoundingBox( Cpoint *bboxPoints, dword subVolumeIndex, float time );
Return the bounding box of the subvolume, in local coordinates of the extension object, as a 0-based array of eight points, at the given time.
Arguments
Cpoint* bboxPoints
: output, 0-based array of eight points. See getBoundingBox for the point ordering.
dword subVolumeIndex
: input, index of the subvolume to return its box.
float time
: input, time of evaluation of the box.
Returns
Nothing.
getNumberOfUVGenerators
virtual byte getNumberOfUVGenerators( void );
Returns the number of implemented custom UV generators.
Arguments
None.
Returns
Number of UV generators. Max 255.
getUVGeneratorName
virtual const char* getUVGeneratorName( byte index );
Return a string with the name of the UV generator.
Arguments
byte index
: input, index of the generator, max 255.
Returns
String with the generator name. If index > numberOfUVGenerators or index < 0, return NULL.
getUVForChannel
virtual byte getUVForChannel( CfVector& uvw, const Cpoint& point, const Cpoint& normal, dword iGenerator,
dword subVolIndex, const Cvector& parametricUVW );
Calculates the UV texture coordinates for the given point "point
". When dealing with analytical surfaces, it's often more convenient to use "parametricUVW
" as input data. "normal
" is the normal at the intersection point.
Arguments
CfVector& uvw
: output, calculated texture coordinates, values range from 0.0 to 1.0.
const Cpoint& point
: input, intersection point.
const Cpoint& normal
: input, normal at intersection point.
dword iGenerator
: input, index of the chosen generator.
dword subVolIndex
: input, index of the subvolume.
const Cvector& parametricUVW
: input, natural parametric coordinates of the surface, calculated in intersect.
Returns
1 on success, 0 otherwise.
isOverlappingBoundingBox
...
byte index
: input, index of the generator, max 255.
Returns
String with the generator name. If index > numberOfUVGenerators or index < 0, return NULL.
getUVForChannel
virtual byte getUVForChannel( CfVector& uvw, const Cpoint& point, const Cpoint& normal, dword iGenerator, dword subVolIndex, const Cvector& parametricUVW );
Calculates the UV texture coordinates for the given point "point
". When dealing with analytical surfaces, it's often more convenient to use "parametricUVW
" as input data. "normal
" is the normal at the intersection point.
Arguments
CfVector& uvw
: output, calculated texture coordinates, values range from 0.0 to 1.0.
const Cpoint& point
: input, intersection point.
const Cpoint& normal
: input, normal at intersection point.
dword iGenerator
: input, index of the chosen generator.
dword subVolIndex
: input, index of the subvolume.
const Cvector& parametricUVW
: input, natural parametric coordinates of the surface, calculated in intersect.
Returns
1 on success, 0 otherwise.
isOverlappingBoundingBox
virtual bool isOverlappingBoundingBox( const Cpoint* bboxPoints, dword subVolumeIndex, bool forceHalfTime );
Helper function for the render engine. Tests whether the box given by "bboxPoints
" and that of the subvolume indexed by "subVolumeIndex
" overlap.
Arguments
Cpoint* bboxPoints
: input, 0-based array of eight points. See getBoundingBox for the point ordering.
dword subVolumeIndex
: input, index of the subvolume to compare its box to.
bool forceHalfTime
: input, type of subvolume box for evaluation. If forceHalfTime is true, the subvolume box to comapre is at time 0.5. If true, it is the smallest box that encloses the swept space of the object in the subvolume from time 0.0 to time 1.0.
Returns
true
on intersection, otherwise false
;
getSubVolumeArea
virtual real getSubVolumeArea( dword subVolumeIndex, const Cbase* transformationBase );
Helper function for the render engine. Returns the surface area of the given subVolume indexed by "subVolumeIndex
".
Arguments
dword subVolumeIndex
: input, index of the subvolume to compare its box to.
const Cbase* transformationBase :
input, transform the geometry with this base and then calculate the area.
Returns
area of the subVolume;
getSubVolumePointAndNormal
virtual void getSubVolumePointAndNormal( dword subVolumeIndex, Cpoint& point, Cvector& normal, mw::sources::Crandom* auxRandom, real rndTime );
Helper function for the render engine. Tests whether the box given by "bboxPoints
" and that of the subvolume Returns the surface area of the given subVolume indexed by "subVolumeIndex
" overlap.
Arguments
Cpoint* bboxPoints
dword subVolumeIndex
: input, 0-based array of eight points. See getBoundingBox for the point ordering.
dword subVolumeIndex
: input, index of the subvolume to compare its box to.
bool forceHalfTime
: input, type of subvolume box for evaluation. If forceHalfTime is true, the subvolume box to comapre is at time 0.5. If true, it is the smallest box that encloses the swept space of the object in the subvolume from time 0.0 to time 1.0.
getProxyDisplayPoints
...
index of the subvolume to compare its box to.
Cpoint& point, Cvector& normal :
output, point and normal in local object space
mw::sources::Crandom* auxRandom :
input, random number generator to sample the surface of the subVolume
real rndTime
: time of evaluation of the ray, to take into account motion blur.
Returns
nothing.
getProxyDisplayPoints
virtual bool getProxyDisplayPoints( const dword& percent, const dword& maxPoints, dword& nPoints,
float*& points );
Allocates and fills the "points" array with the coordinates of points to be drawn in Studio viewports. Also sets "nPoints" to the actual number of points filled.
Arguments
const dword& percent
: input, goes from 0 to 100, indicating the degree of desired precision to draw the proxy.
const dword& maxPoint
s : input, the maximum number of points allowed.
dword& nPoints
: output, the actual number of points allocated. Must be <= maxPoints.
float*& points
: output, array with the points, x0y0z0x1y1z1x2y2z2....
Returns
true
on success, otherwise false
;
getProxyDisplayLines
virtual
...
bool
...
getProxyDisplayLines(
...
const
...
dword&
...
percent,
...
const
...
dword&
...
maxPoints,
...
const
...
dword&
...
maxLines,
...
dword&
...
nPoints,
...
float*&
...
points,
...
dword&
...
nLines,
...
dword*&
...
pointsPerLine
...
);
Allocates and fills the "points" array and "pointsPerLine" array with with the coordinates of points, and number of points per line to be drawn in Studio. Also sets "nPoints" to the actual number of points created and nLines to the actual number of lines created.
Arguments
const dword& percent
: input, goes from 0 to 100, indicating the degree of desired precision to draw the proxy.
const dword& maxPoints
: input, the maximum number of points allowed.
const dword& maxLines
: input, the maximum number of lines allowed.
dword& nPoints
: output, the actual number of points allocated. Must be <= maxPoints.
float*& points
: output, array with the points, x0y0z0x1y1z1x2y2z2....
dword& nLines
: output, the actual number of lines created. Must be <= maxLines.
dword*& pointsPerLine
: output, array with the number of points per line, n0n1n2.... If this array is NULL, then the number of points per line is constant, and is nPoints/nLines.
Returns
true
on success, otherwise false
;
getProxyDisplayFaces
virtual
...
bool
...
getProxyDisplayFaces(
...
const
...
dword&
...
percent,
...
const
...
dword&
...
maxFaces,
...
dword&
...
nPoints,
...
...
float*&
...
points,
...
dword&
...
nFaces,
...
dword*&
...
faces
...
);
Allocates and fills the "points" array and "faces" array with with the coordinates of points, and indexes to vertices to be drawn in Studio. Also sets "nPoints" to the actual number of points created and "nFaces" to the actual number of triangles created.
Arguments
const dword& percent
: input, goes from 0 to 100, indicating the degree of desired precision to draw the proxy.
const dword& maxPoints
: input, the maximum number of points allowed.
const dword& maxFaces
: input, the maximum number of faces allowed.
dword& nPoints
: output, the actual number of points allocated. Must be <= maxPoints.
float*& points
: output, array with the points, x0y0z0x1y1z1x2y2z2....
dword& nFaces
: output, the actual number of faces created. Must be <= maxLines.
dword*& faces
: output, array with the indices to vertices in the "points" array v00v01v02v10v11v12v20v21v22....
Returns
true
on success, otherwise false
.
...
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
#include <math.h> #include "extensionmanager.h" #include "geometryextension.h" #include "maxwell.h" #include "maxwellversions.h" #ifndef DEG2RAD #define DEG2RAD(d) ((d) * 3.14159265358979323846 / 180.0) #endif class SphereRenderExtension : public CgeometryProceduralExtension { DECLARE_EXTENSION_METHODS( "SphereRenderExample", SphereRenderExtension, 1 ) Cmaxwell* pMaxwellLocal; double radius; public: SphereRenderExtension() { getExtensionData()->createDouble( "Radius", 1.00, 0, 1000000 ); } ~SphereRenderExtension() { } bool initializeForRendering ( Cmaxwell* pMaxwellpMaxwell, Cmaxwell::Cobject& proceduralObject ) { pMaxwellLocal = pMaxwell; getExtensionData()->getDouble( "Radius", radius ); return true; } //Helper function to spit messages to maxwell. void printMessage( const char* text, const int code ) { if( pMaxwellLocal != NULL ) { char pMessage[ 1024 ]; sprintf ( pMessage, "[Extension %s] %s", getName(), text ); pMaxwellLocal->printMessage( pMessage, code ); } } dword getNumSubVolumes( void ) { return 1;//We just have one region surrounding the whole sphere }; bool intersect( Cmaxwell::Cobject* object, const Cpoint& origin, const Cvector& dir, real time, const dword subVolumeIndex, Cvector* pNormal, Cvector* pLocalImpact, CfVector &data, Cvector& parametricUVW, Cvector& tangentU, Cvector& tangentV ) { real dist, a, b, disc, t0, t1, sqr; dist = -1.0; //Assume sphere centered in ( 0, 0, 0 ) in local coordinates a = 2*( dir.x*origin.x + dir.y*origin.y + dir.z*origin.z ); b = origin.x*origin.x + origin.y*origin.y + origin.z*origin.z - radius*radius; disc = a*a - 4*b; if ( disc < 0.0 ) { return false; } sqr = sqrt ( disc ); t0 = ( -a - sqr ) * 0.5; t1 = ( -a + sqr ) * 0.5; if ( t0 <= 0.0 ) { if ( t1 > 0.0 ) { t1 dist = ( -a + sqr ) * 0.5t1; } else { return false; if ( t0 <= 0.0 ) } } else { if ( t0 < t1 > 0.0 )) dist = t0; {else dist = t1; } elseCvector p; { return false; } } else { if ( t0 < t1 ) dist = t0; else dist = t1; } Cvector p; p.assign( origin + dir * dist );//Point of intersection in local coordinates pLocalImpact->assign( p ); Cvector normal; normal.assign( p ); pNormal->assign( normal );//No need to normalize. Done later on demand. return true; } void getBoundingBox( Cvector& bmin, Cvector& bmax, float time ) { bmin.assign( -radius, -radius, -radius ); bmax.assign( radius, radius, radius ); } }; EXPORT_GEOMETRY_PROCEDURAL_EXTENSION( SphereRenderExtension )p.assign( origin + dir * dist );//Point of intersection in local coordinates pLocalImpact->assign( p ); Cvector normal; normal.assign( p ); pNormal->assign( normal );//No need to normalize. Done later on demand. return true; } void getBoundingBox( Cvector& bmin, Cvector& bmax, float time ) { bmin.assign( -radius, -radius, -radius ); bmax.assign( radius, radius, radius ); } }; extern "C" ALWAYSEXPORT int getSdkVersion() { return MAXWELL_SDK_VERSION; } extern "C" ALWAYSEXPORT int StartExtension( CextensionManager& extensionManager ) { int i = 0; if ( extensionManager.registerGeometryProceduralExtension( new SphereRenderExtension ) ) i++; return i; } |