1 /*---------------------------------------------------------------------------*
2   Project:  NintendoWare
3   File:     gfx_MeshRenderer.cpp
4 
5   Copyright (C)2009-2011 Nintendo/HAL Laboratory, Inc.  All rights reserved.
6 
7   These coded instructions, statements, and computer programs contain proprietary
8   information of Nintendo and/or its licensed developers and are protected by
9   national and international copyright laws. They may not be disclosed to third
10   parties or copied or duplicated in any form, in whole or in part, without the
11   prior written consent of Nintendo.
12 
13   The content herein is highly confidential and should be handled accordingly.
14 
15   $Revision: 31829 $
16  *---------------------------------------------------------------------------*/
17 
18 // パーティクルのストリームに不正値が含まれていないかを調べる定義です。
19 // ストリーム内の全要素を調べるため、デバッグ時のみ有効にしてあります。
20 #ifdef NW_DEBUG
21 #define NW_GFX_CHECK_PARTICLE_STREAMS
22 #endif
23 
24 #include "precompiled.h"
25 
26 #include <nw/gfx/gfx_Common.h>
27 #include <nw/gfx/res/gfx_ResMesh.h>
28 #include <nw/gfx/res/gfx_ResParticleShape.h>
29 #include <nw/gfx/gfx_SceneObject.h>
30 #include <nw/gfx/gfx_MeshRenderer.h>
31 #include <nw/gfx/gfx_RenderContext.h>
32 #include <nw/gfx/gfx_ShaderProgram.h>
33 #include <nw/gfx/gfx_SkeletalModel.h>
34 #include <nw/gfx/gfx_Skeleton.h>
35 #include <nw/gfx/gfx_ParticleModel.h>
36 #include <nw/gfx/gfx_ParticleShape.h>
37 #include <nw/os/os_Memory.h>
38 #include <nw/types.h>
39 #include <nw/ut/ut_Inlines.h>
40 #include <nw/ut/ut_ResUtil.h>
41 #include <nw/ut/ut_ResDictionary.h>
42 #include <nw/ut/ut_Foreach.h>
43 #include <nw/dev.h>
44 #include <nw/gfx/gfx_CommandUtil.h>
45 #include <nw/gfx/gfx_ActivateCommand.h>
46 
47 #include <GLES2/gl2.h>
48 #include <GLES2/gl2ext.h>
49 
50 #include <cmath>
51 
52 namespace nw
53 {
54 namespace gfx
55 {
56 
57 namespace internal
58 {
59 
60 //! @brief 頂点シェーダーのレジスタが上書きされていないか確認します。
TestRegisterOverride(int matrixpaletteCount,RenderContext * renderContext)61 bool TestRegisterOverride(int matrixpaletteCount, RenderContext* renderContext)
62 {
63     NW_ASSERT(renderContext->GetShaderProgram()->GetActiveDescription().IsValid());
64 
65     int vertexLightEndUniform = renderContext->GetShaderProgram()->GetActiveDescription().GetVertexLightEndUniform();
66     int vertexLightCount = renderContext->GetSceneEnvironment().GetVertexLightCount();
67     const int UNIT_COUNT = 3;
68     const int VERTEX_LIGHT_REGISTER_COUNT = 6;
69     int usedMatrixpaletteCount = 0;
70     usedMatrixpaletteCount = UNIT_COUNT * matrixpaletteCount + vertexLightCount * VERTEX_LIGHT_REGISTER_COUNT;
71 
72     return (usedMatrixpaletteCount <= vertexLightEndUniform);
73 }
74 
75 }
76 
77 //----------------------------------------
78 MeshRenderer*
Create(nw::os::IAllocator * pAllocator)79 MeshRenderer::Create(nw::os::IAllocator* pAllocator)
80 {
81     NW_NULL_ASSERT(pAllocator);
82 
83     size_t size = sizeof(MeshRenderer);
84 
85     void* buf = pAllocator->Alloc(size);
86 
87     return new(buf) MeshRenderer(pAllocator);
88 }
89 
90 //----------------------------------------
91 void
RenderMesh(ResMesh mesh,Model * model)92 MeshRenderer::RenderMesh(ResMesh mesh, Model* model)
93 {
94     NW_ASSERT(mesh.IsValid());
95 
96     // Mesh の頂点設定を有効化。
97     ResShape shape = model->GetResModel().GetShapes(mesh.GetShapeIndex());
98 
99     switch ( shape.ref().typeInfo )
100     {
101     case ResSeparateDataShape::TYPE_INFO:
102         {
103             m_RenderContext->SetMaterial(model->GetMaterial(mesh.GetMaterialIndex()));
104 
105             // コンテキストを有効化します
106             m_RenderContext->ActivateContext();
107 
108             m_RenderContext->ActivateVertexAttribute( mesh );
109 
110             RenderSeparateDataShape(
111                 model,
112                 ResStaticCast<ResSeparateDataShape>(shape),
113                 mesh.GetCurrentPrimitiveIndex());
114 
115             m_RenderContext->DeactivateVertexAttribute( mesh );
116         }
117         break;
118     case ResParticleShape::TYPE_INFO:
119         {
120             ParticleModel* particleModel = static_cast<ParticleModel*>(model);
121             NW_NULL_ASSERT(particleModel);
122 
123             ParticleSet* particleSet = particleModel->GetParticleSets(mesh.GetShapeIndex());
124 #ifdef NW_CHECK_PARTICLE_PROCESS
125             particleSet->BeginDraw();
126 #endif
127             if (particleSet->GetParticleCollection()->GetCount() == 0)
128             {
129                 return;
130             }
131 
132             m_RenderContext->SetMaterial(model->GetMaterial(mesh.GetMaterialIndex()));
133 
134             // コンテキストを有効化します
135             m_RenderContext->ActivateParticleContext();
136 
137             RenderParticleShape(
138                 model,
139                 ResStaticCast<ResParticleShape>(shape),
140                 mesh.GetShapeIndex());
141         }
142         break;
143     default:
144         {
145             NW_FATAL_ERROR("Unsupported data shape type.");
146         }
147     }
148 }
149 
150 //----------------------------------------
151 void
RenderSeparateDataShape(Model * model,ResSeparateDataShape shape,s32 currentPrimitiveIndex)152 MeshRenderer::RenderSeparateDataShape(
153     Model* model,
154     ResSeparateDataShape shape,
155     s32 currentPrimitiveIndex
156 )
157 {
158     NW_NULL_ASSERT(m_RenderContext);
159     if (!shape.IsValid()) { return; }
160 
161     internal::NWSetVertexUniform3fv( VERTEX_SHADER_UNIFORM_POSOFFS_INDEX, 1, shape.GetPositionOffset() );
162 
163     SkeletalModel* skeletalModel = ut::DynamicCast<SkeletalModel*>(model);
164 
165     if (m_RenderContext->GetModelCache() != model)
166     {
167         if (skeletalModel)
168         {
169             m_RenderContext->SetModelMatrixForSkeletalModel(skeletalModel);
170         }
171         else
172         {
173             m_RenderContext->SetModelMatrixForModel(model);
174         }
175     }
176 
177     ResPrimitiveSetArray primitiveSets = shape.GetPrimitiveSets();
178     ResPrimitiveSetArray::iterator primitiveSetEnd = primitiveSets.end();
179 
180     for (ResPrimitiveSetArray::iterator primitiveSet = primitiveSets.begin();
181         primitiveSet != primitiveSetEnd; ++primitiveSet)
182     {
183         NW_ASSERT((*primitiveSet).IsValid());
184         NW_ASSERT(0 <= currentPrimitiveIndex &&
185                   currentPrimitiveIndex < (*primitiveSet).GetPrimitivesCount());
186 
187         s32 boneIndexCount = (*primitiveSet).GetBoneIndexTableCount();
188 
189         NW_ASSERTMSG(
190             internal::TestRegisterOverride(
191                 boneIndexCount,
192                 m_RenderContext
193              ),
194             "Vertex-lights or user-registers might be overridden by bone matrices.%s", model->GetResModel().GetName()
195         );
196 
197         if (skeletalModel == NULL || boneIndexCount == 0)
198         {
199             const ShaderProgram* shaderProgram = m_RenderContext->GetShaderProgram();
200 
201             shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISSMOSK), false);
202             shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISRGDSK), false);
203 
204 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
205             math::MTX34 worldMatrix(model->WorldMatrix());
206             MTX34MultTranslate(&worldMatrix, m_RenderContext->ModelTranslateOffset(), worldMatrix);
207             shaderProgram->SetUniversal(0, worldMatrix);
208 #else
209             shaderProgram->SetUniversal(0, model->WorldMatrix());
210 #endif
211         }
212         else
213         {
214             this->SetMatrixPalette(
215                 skeletalModel,
216                 *primitiveSet,
217                 boneIndexCount
218             );
219         }
220 
221         m_RenderContext->RenderPrimitive((*primitiveSet).GetPrimitives(currentPrimitiveIndex));
222     }
223 }
224 
225 //----------------------------------------
226 namespace internal
227 {
228 static bool
isIllegal(f32 val)229 isIllegal(f32 val)
230 {
231 //    return isnan(val);
232     return !isfinite(val);
233 }
234 
235 static bool
isIllegal(const math::VEC3 & vec)236 isIllegal(const math::VEC3& vec)
237 {
238     return isIllegal(vec.x) || isIllegal(vec.y) || isIllegal(vec.z);
239 }
240 
241 static bool
isIllegal(const math::MTX34 & mtx)242 isIllegal(const math::MTX34& mtx)
243 {
244     return
245         isIllegal(mtx._00) || isIllegal(mtx._01) || isIllegal(mtx._02) || isIllegal(mtx._03) ||
246         isIllegal(mtx._10) || isIllegal(mtx._11) || isIllegal(mtx._12) || isIllegal(mtx._13) ||
247         isIllegal(mtx._20) || isIllegal(mtx._21) || isIllegal(mtx._22) || isIllegal(mtx._23);
248 }
249 }
250 
251 //----------------------------------------
252 void
RenderParticleShape(Model * model,ResParticleShape resource,int shapeIndex)253 MeshRenderer::RenderParticleShape(
254     Model* model,
255     ResParticleShape resource,
256     int shapeIndex
257 )
258 {
259     NW_NULL_ASSERT(m_RenderContext);
260     if (!resource.IsValid()) { return; }
261 
262     ParticleModel* particleModel = static_cast<ParticleModel*>(model);
263     NW_NULL_ASSERT(particleModel);
264 
265     ParticleSet* particleSet = particleModel->GetParticleSets(shapeIndex);
266 
267     const ResParticleSet& resParticleSet = particleSet->GetResParticleSet();
268 
269     ParticleShape* particleShape = particleModel->GetParticleShapes(shapeIndex);
270     NW_NULL_ASSERT(particleShape);
271 
272     NW_ASSERT(particleShape->GetResParticleShape().ptr() == resource.ptr());
273 
274     if (resParticleSet.GetIsBufferFlushEnabled())
275     {
276         particleShape->FlushBuffer();
277     }
278 
279     int bufferSide = particleShape->GetBufferSide();
280     internal::NWUseCmdlist(particleShape->m_CommandCache[bufferSide], particleShape->m_CommandCacheSize[bufferSide]);
281 
282     enum
283     {
284         REG_UNIFORM_FLOAT_INDEX = 0x2c0,
285         REG_VS_FLOAT_DATA1      = 0x2c1,
286         REG_INDEX_STREAM_OFFSET = 0x227,
287         REG_INDEX_STREAM_COUNT      = 0x228
288     };
289 
290     // c6-c7
291     u32* command = (u32*)internal::NWGetCurrentCmdBuffer();
292     int commandIndex = 0;
293 
294     command[commandIndex++] = 0x80000000 | VERTEX_SHADER_UNIFORM_POSOFFS_INDEX;
295     command[commandIndex++] = internal::MakeCommandHeader(REG_UNIFORM_FLOAT_INDEX, 1 + 4, true, 0xF);
296     command[commandIndex++] = 0;
297     command[commandIndex++] = *(u32*)&resource.GetPositionOffset().z;
298     command[commandIndex++] = *(u32*)&resource.GetPositionOffset().y;
299     command[commandIndex++] = *(u32*)&resource.GetPositionOffset().x;
300     NW_ASSERT(!internal::isIllegal(resource.GetPositionOffset()));
301 
302     // universal
303     Camera* camera = m_RenderContext->GetActiveCamera();
304     NW_NULL_ASSERT(camera);
305 
306     const u32 HEADER_UNIFORM_FLOAT_INDEX = internal::MakeCommandHeader(REG_UNIFORM_FLOAT_INDEX, 1, false, 0xF);
307 
308     command[commandIndex++] = 0x80000000 | VERTEX_SHADER_UNIFORM_UNIVREG_INDEX;
309     command[commandIndex++] = HEADER_UNIFORM_FLOAT_INDEX;
310 
311     const int universalNum = 15;
312 
313     if (particleSet->GetResParticleSet().GetIsForceWorld())
314     {
315         // modelを強制的に単位行列とする
316 
317         // view . model = view . I = view
318         NW_ASSERT(!internal::isIllegal(camera->ViewMatrix()));
319         internal::NWCopyMtx34WithHeader(
320             (f32*)&command[commandIndex],
321             (f32*)&camera->ViewMatrix(),
322             internal::MakeCommandHeader(REG_VS_FLOAT_DATA1, 4 * universalNum, false, 0xF));
323         commandIndex += 12 + 1;
324 
325         // Inv(view . model) = Inv(view . I) = Inv(view)
326         NW_ASSERT(!internal::isIllegal(camera->InverseViewMatrix()));
327         internal::NWCopyMtx34Reverse(
328             (f32*)&command[commandIndex],
329             (f32*)&camera->InverseViewMatrix());
330         commandIndex += 12;
331 
332         // Inv(model)
333         command[commandIndex++] = 0;
334         command[commandIndex++] = 0;
335         command[commandIndex++] = 0;
336         command[commandIndex++] = 0x3f800000;
337 
338         command[commandIndex++] = 0;
339         command[commandIndex++] = 0;
340         command[commandIndex++] = 0x3f800000;
341         command[commandIndex++] = 0;
342 
343         command[commandIndex++] = 0;
344         command[commandIndex++] = 0x3f800000;
345         command[commandIndex++] = 0;
346         command[commandIndex++] = 0;
347 
348         // model
349         command[commandIndex++] = 0;
350         command[commandIndex++] = 0;
351         command[commandIndex++] = 0;
352         command[commandIndex++] = 0x3f800000;
353 
354         command[commandIndex++] = 0;
355         command[commandIndex++] = 0;
356         command[commandIndex++] = 0x3f800000;
357         command[commandIndex++] = 0;
358 
359         command[commandIndex++] = 0;
360         command[commandIndex++] = 0x3f800000;
361         command[commandIndex++] = 0;
362         command[commandIndex++] = 0;
363     }
364     else
365     {
366         nw::math::MTX34* worldMatrix;
367 
368 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
369         math::MTX34 offsetMatrix(model->WorldMatrix());
370         worldMatrix = &offsetMatrix;
371         MTX34MultTranslate(worldMatrix, m_RenderContext->ModelTranslateOffset(), *worldMatrix);
372 #else
373         worldMatrix = &model->WorldMatrix();
374 #endif
375 
376         nw::math::MTX34 modelView;
377         nw::math::MTX34Mult(&modelView, camera->ViewMatrix(), *worldMatrix);
378         NW_ASSERT(!internal::isIllegal(modelView));
379         internal::NWCopyMtx34WithHeader(
380             (f32*)&command[commandIndex],
381             (f32*)&modelView,
382             internal::MakeCommandHeader(REG_VS_FLOAT_DATA1, 4 * universalNum, false, 0xF));
383         commandIndex += 12 + 1;
384 
385         // Inv(view . model) = Inv(model) . Inv(view)
386         nw::math::MTX34 invModelView;
387 
388 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
389         MTX34MultTranslate(&invModelView, -m_RenderContext->ModelTranslateOffset(), model->InverseWorldMatrix());
390         nw::math::MTX34Mult(&invModelView, invModelView, camera->InverseViewMatrix());
391 #else
392         nw::math::MTX34Mult(&invModelView, model->InverseWorldMatrix(), camera->InverseViewMatrix());
393 #endif
394 
395         NW_ASSERT(!internal::isIllegal(invModelView));
396         internal::NWCopyMtx34Reverse(
397             (f32*)&command[commandIndex],
398             (f32*)&invModelView);
399         commandIndex += 12;
400 
401         // Inv(model)
402         NW_ASSERT(!internal::isIllegal(model->InverseWorldMatrix()));
403         internal::NWCopyMtx34Reverse(
404             (f32*)&command[commandIndex],
405             (f32*)&model->InverseWorldMatrix());
406         commandIndex += 12;
407 
408         // model
409         NW_ASSERT(!internal::isIllegal(model->WorldMatrix()));
410         internal::NWCopyMtx34Reverse(
411             (f32*)&command[commandIndex],
412             (f32*)worldMatrix);
413         commandIndex += 12;
414     }
415 
416     math::VEC3 offset(0.0f, 0.0f, 0.0f);
417     const ResParticleShapeBuilder& resShapeBuilder = resParticleSet.GetParticleShapeBuilder();
418     if (resShapeBuilder.IsValid())
419     {
420         offset = resShapeBuilder.GetDrawOffset();
421     }
422 
423     NW_ASSERT(!internal::isIllegal(offset));
424     internal::NWCopyVec3Reverse(
425         (f32*)&command[commandIndex],
426         (f32*)&offset);
427     commandIndex += 4;
428 
429     NW_ASSERT(!internal::isIllegal(particleSet->GetScaleOffset()));
430     internal::NWCopyVec3Reverse(
431         (f32*)&command[commandIndex],
432         (f32*)&particleSet->GetScaleOffset());
433     commandIndex += 4;
434 
435     NW_ASSERT(!internal::isIllegal(particleSet->GetRotateOffset()));
436     internal::NWCopyVec3Reverse(
437         (f32*)&command[commandIndex],
438         (f32*)&particleSet->GetRotateOffset());
439     commandIndex += 4;
440 
441     if ((commandIndex & 1) == 1)
442     {
443         command[commandIndex++] = 0; // padding
444     }
445 
446     const u32 HEADER_INDEX_STREAM_COUNT = internal::MakeCommandHeader(REG_INDEX_STREAM_COUNT, 1, false, 0xF);
447 
448     command[commandIndex++] = particleSet->GetParticleCollection()->GetCount();
449     command[commandIndex++] = HEADER_INDEX_STREAM_COUNT;
450 
451     u32 streamOffset = particleShape->GetPrimitiveStreamOffset(PARTICLE_BUFFER_FRONT);
452 
453     const bool isAscendingOrder = (resShapeBuilder.IsValid())? resShapeBuilder.IsAscendingOrder() : true;
454     if (!isAscendingOrder)
455     {
456         ParticleCollection* collection = particleSet->GetParticleCollection();
457         streamOffset += sizeof(u16) * (collection->GetCapacity() - collection->GetCount());
458     }
459 
460     const u32 HEADER_INDEX_STREAM_OFFSET = internal::MakeCommandHeader(REG_INDEX_STREAM_OFFSET, 1, false, 0xF);
461 
462     command[commandIndex++] = 0x80000000 | streamOffset;
463     command[commandIndex++] = HEADER_INDEX_STREAM_OFFSET;
464 
465     internal::NWForwardCurrentCmdBuffer(sizeof(u32) * commandIndex);
466 
467 #ifdef NW_GFX_CHECK_PARTICLE_STREAMS
468     for (int index = 0; index < particleShape->GetVertexAttributesCount(); ++index)
469     {
470         int format = particleShape->GetVertexAttributeFormatType(index);
471         if (format != GL_FLOAT)
472         {
473             continue;
474         }
475 
476         if (particleShape->IsVertexStream(index))
477         {
478             // stream
479             int count = particleShape->GetVertexCapacity();
480             count *= particleShape->GetVertexAttributeDimension(index);
481 
482             const f32* ptr = reinterpret_cast<const f32*>(
483                 particleShape->GetVertexStreamPtr(index, PARTICLE_BUFFER_FRONT));
484             for (int i = 0; i < count; ++i)
485             {
486                 NW_ASSERT(!internal::isIllegal(ptr[i]));
487             }
488         }
489         else
490         {
491             // param
492             int count = 1;
493             count *= particleShape->GetVertexAttributeDimension(index);
494 
495             const f32* ptr = particleShape->GetVertexParameter(index);
496             for (int i = 0; i < count; ++i)
497             {
498                 NW_ASSERT(!internal::isIllegal(ptr[i]));
499             }
500         }
501     }
502 #endif
503 
504     internal::NWUseCmdlist(
505         particleShape->m_PrimitiveCommandCache,
506         particleShape->m_PrimitiveCommandCacheSize);
507 
508     internal::NWUseCmdlist(
509         particleShape->m_DeactivateVertexCommandCache,
510         particleShape->m_DeactivateVertexCommandCacheSize);
511 }
512 
513 //----------------------------------------
514 void
SetMatrixPalette(SkeletalModel * skeletalModel,ResPrimitiveSet primitiveSet,s32 boneIndexCount)515 MeshRenderer::SetMatrixPalette(
516     SkeletalModel* skeletalModel,
517     ResPrimitiveSet primitiveSet,
518     s32 boneIndexCount)
519 {
520     NW_NULL_ASSERT(m_RenderContext);
521     NW_ASSERT(primitiveSet.IsValid());
522 
523     const int UNIT_COUNT = 3;
524 
525     //----------------------------------------
526     // マトリクスパレットに関する設定用レジスタをシェーダに設定します。
527     const ShaderProgram* shaderProgram = m_RenderContext->GetShaderProgram();
528 
529     //bool isRigid = boneIndexCount == 1 || primitiveSet.GetSkinningMode() != ResPrimitiveSet::SKINNING_MODE_SMOOTH;
530     bool isRigid = primitiveSet.GetSkinningMode() != ResPrimitiveSet::SKINNING_MODE_SMOOTH;
531     shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISSMOSK), !isRigid);
532     shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISRGDSK), isRigid);
533 
534     //----------------------------------------
535     // ボーンの種類に合わせたマトリクスを取得し、シェーダに設定します。
536     Skeleton* skeleton = skeletalModel->GetSkeleton();
537     ResSkeleton resSkeleton = skeleton->GetResSkeleton();
538     Skeleton::MatrixPose& matrixPose = skeleton->WorldMatrixPose();
539     Skeleton::MatrixPose& skiningPose = skeleton->SkiningMatrixPose();
540 
541 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
542     const bool isModelCoordinate =
543         ut::CheckFlag(resSkeleton.GetFlags(), ResSkeletonData::FLAG_MODEL_COORDINATE);
544 #endif
545 
546     for (int count = 0; count < boneIndexCount; ++count)
547     {
548         s32 boneIndex = primitiveSet.GetBoneIndexTable(count);
549 
550         // マトリクスを取得してきます。
551         ResBone bone = resSkeleton.GetBones(boneIndex);
552         bool hasSkinningMatrix = (bone.GetFlags() & ResBoneData::FLAG_HAS_SKINNING_MATRIX) != 0;
553         Skeleton::MatrixPose* pose =
554             (!isRigid && hasSkinningMatrix) ? &skiningPose : &matrixPose;
555 
556         math::MTX34* matrix;
557 
558 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
559         math::MTX34 offsetMatrix;
560         if (isModelCoordinate)
561         {
562             matrix = pose->GetMatrix(boneIndex);
563         }
564         else
565         {
566             offsetMatrix = *pose->GetMatrix(boneIndex);
567             matrix = &offsetMatrix;
568             MTX34MultTranslate(matrix, m_RenderContext->ModelTranslateOffset(), *matrix);
569         }
570 #else
571         matrix = pose->GetMatrix(boneIndex);
572 #endif
573 
574         // マトリクスを設定します。
575         int index = count * UNIT_COUNT;
576 
577         if (count == 0)
578         {
579             internal::NWSetVertexUniform4fvBegin(
580                 VERTEX_SHADER_UNIFORM_UNIVREG_INDEX + index,
581                 boneIndexCount * UNIT_COUNT,
582                 UNIT_COUNT,
583                 *matrix);
584         }
585         else
586         {
587             internal::NWSetVertexUniform4fvContinuous(UNIT_COUNT, *matrix);
588         }
589     }
590 
591     internal::NWSetVertexUniform4fvEnd();
592 }
593 
594 } // namespace gfx
595 } // namespace nw
596 
597