1 /*---------------------------------------------------------------------------*
2   Project:  NintendoWare
3   File:     gfx_MeshRenderer.cpp
4 
5   Copyright (C)2009-2010 Nintendo Co., Ltd./HAL Laboratory, Inc.  All rights reserved.
6 
7   These coded instructions, statements, and computer programs contain
8   proprietary information of Nintendo of America Inc. and/or Nintendo
9   Company Ltd., and are protected by Federal copyright law.  They may
10   not be disclosed to third parties or copied or duplicated in any form,
11   in whole or in part, without the prior written consent of Nintendo.
12 
13   $Revision: 27935 $
14  *---------------------------------------------------------------------------*/
15 
16 // パーティクルのストリームに不正値が含まれていないかを調べる定義です。
17 // ストリーム内の全要素を調べるため、デバッグ時のみ有効にしてあります。
18 #ifdef NW_DEBUG
19 #define NW_GFX_CHECK_PARTICLE_STREAMS
20 #endif
21 
22 #include "precompiled.h"
23 
24 #include <nw/gfx/gfx_Common.h>
25 #include <nw/gfx/res/gfx_ResMesh.h>
26 #include <nw/gfx/res/gfx_ResParticleShape.h>
27 #include <nw/gfx/gfx_SceneObject.h>
28 #include <nw/gfx/gfx_MeshRenderer.h>
29 #include <nw/gfx/gfx_RenderContext.h>
30 #include <nw/gfx/gfx_ShaderProgram.h>
31 #include <nw/gfx/gfx_SkeletalModel.h>
32 #include <nw/gfx/gfx_Skeleton.h>
33 #include <nw/gfx/gfx_ParticleModel.h>
34 #include <nw/gfx/gfx_ParticleShape.h>
35 #include <nw/os/os_Memory.h>
36 #include <nw/types.h>
37 #include <nw/ut/ut_Inlines.h>
38 #include <nw/ut/ut_ResUtil.h>
39 #include <nw/ut/ut_ResDictionary.h>
40 #include <nw/ut/ut_Foreach.h>
41 #include <nw/dev.h>
42 #include <nw/gfx/gfx_CommandUtil.h>
43 #include <nw/gfx/gfx_ActivateCommand.h>
44 
45 #include <GLES2/gl2.h>
46 #include <GLES2/gl2ext.h>
47 
48 #include <cmath>
49 
50 namespace nw
51 {
52 namespace gfx
53 {
54 
55 namespace internal
56 {
57 
58 //! @brief 頂点シェーダーのレジスタが上書きされていないか確認します。
TestRegisterOverride(int matrixpaletteCount,RenderContext * renderContext)59 bool TestRegisterOverride(int matrixpaletteCount, RenderContext* renderContext)
60 {
61     NW_ASSERT(renderContext->GetShaderProgram()->GetActiveDescription().IsValid());
62 
63     int vertexLightEndUniform = renderContext->GetShaderProgram()->GetActiveDescription().GetVertexLightEndUniform();
64     int vertexLightCount = renderContext->GetSceneEnvironment().GetVertexLightCount();
65     const int UNIT_COUNT = 3;
66     const int VERTEX_LIGHT_REGISTER_COUNT = 6;
67     int usedMatrixpaletteCount = 0;
68     usedMatrixpaletteCount = UNIT_COUNT * matrixpaletteCount + vertexLightCount * VERTEX_LIGHT_REGISTER_COUNT;
69 
70     return (usedMatrixpaletteCount <= vertexLightEndUniform);
71 }
72 
73 }
74 
75 //----------------------------------------
76 MeshRenderer*
Create(nw::os::IAllocator * pAllocator)77 MeshRenderer::Create(nw::os::IAllocator* pAllocator)
78 {
79     NW_NULL_ASSERT(pAllocator);
80 
81     size_t size = sizeof(MeshRenderer);
82 
83     void* buf = pAllocator->Alloc(size);
84 
85     return new(buf) MeshRenderer(pAllocator);
86 }
87 
88 //----------------------------------------
89 void
RenderMesh(ResMesh mesh,Model * model)90 MeshRenderer::RenderMesh(ResMesh mesh, Model* model)
91 {
92     NW_ASSERT(mesh.IsValid());
93 
94     // Mesh の頂点設定を有効化。
95     ResShape shape = model->GetResModel().GetShapes(mesh.GetShapeIndex());
96 
97     switch ( shape.ref().typeInfo )
98     {
99     case ResSeparateDataShape::TYPE_INFO:
100         {
101             m_RenderContext->SetMaterial(model->GetMaterial(mesh.GetMaterialIndex()));
102 
103             // コンテキストを有効化します
104             m_RenderContext->ActivateContext();
105 
106             m_RenderContext->ActivateVertexAttribute( mesh );
107 
108             RenderSeparateDataShape(
109                 model,
110                 ResStaticCast<ResSeparateDataShape>(shape),
111                 mesh.GetCurrentPrimitiveIndex());
112 
113             m_RenderContext->DeactivateVertexAttribute( mesh );
114         }
115         break;
116     case ResParticleShape::TYPE_INFO:
117         {
118             ParticleModel* particleModel = static_cast<ParticleModel*>(model);
119             NW_NULL_ASSERT(particleModel);
120 
121             ParticleSet* particleSet = particleModel->GetParticleSets(mesh.GetShapeIndex());
122             if (particleSet->GetParticleCollection()->GetCount() == 0)
123             {
124                 return;
125             }
126 
127             m_RenderContext->SetMaterial(model->GetMaterial(mesh.GetMaterialIndex()));
128 
129             // コンテキストを有効化します
130             m_RenderContext->ActivateParticleContext();
131 
132             RenderParticleShape(
133                 model,
134                 ResStaticCast<ResParticleShape>(shape),
135                 mesh.GetShapeIndex());
136         }
137         break;
138     default:
139         {
140             NW_FATAL_ERROR("Unsupported data shape type.");
141         }
142     }
143 }
144 
145 //----------------------------------------
146 void
RenderSeparateDataShape(Model * model,ResSeparateDataShape shape,s32 currentPrimitiveIndex)147 MeshRenderer::RenderSeparateDataShape(
148     Model* model,
149     ResSeparateDataShape shape,
150     s32 currentPrimitiveIndex
151 )
152 {
153     NW_NULL_ASSERT(m_RenderContext);
154     if (!shape.IsValid()) { return; }
155 
156     internal::NWSetVertexUniform3fv( VERTEX_SHADER_UNIFORM_POSOFFS_INDEX, 1, shape.GetPositionOffset() );
157 
158     SkeletalModel* skeletalModel = ut::DynamicCast<SkeletalModel*>(model);
159 
160     if (m_RenderContext->GetModelCache() != model)
161     {
162         if (skeletalModel)
163         {
164             m_RenderContext->SetModelMatrixForSkeletalModel(skeletalModel);
165         }
166         else
167         {
168             m_RenderContext->SetModelMatrixForModel(model);
169         }
170     }
171 
172     ResPrimitiveSetArray primitiveSets = shape.GetPrimitiveSets();
173     ResPrimitiveSetArray::iterator primitiveSetEnd = primitiveSets.end();
174 
175     for (ResPrimitiveSetArray::iterator primitiveSet = primitiveSets.begin();
176         primitiveSet != primitiveSetEnd; ++primitiveSet)
177     {
178         NW_ASSERT((*primitiveSet).IsValid());
179         NW_ASSERT(0 <= currentPrimitiveIndex &&
180                   currentPrimitiveIndex < (*primitiveSet).GetPrimitivesCount());
181 
182         s32 boneIndexCount = (*primitiveSet).GetBoneIndexTableCount();
183 
184         NW_ASSERTMSG(
185             internal::TestRegisterOverride(
186                 boneIndexCount,
187                 m_RenderContext
188              ),
189             "Vertex-lights or user-registers might be overridden by bone matrices.%s", model->GetResModel().GetName()
190         );
191 
192         if (skeletalModel == NULL || boneIndexCount == 0)
193         {
194             const ShaderProgram* shaderProgram = m_RenderContext->GetShaderProgram();
195 
196             shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISSMOSK), false);
197             shaderProgram->SetVertexUniformBool(NW_GFX_VERTEX_UNIFORM(ISRGDSK), false);
198 
199 #ifdef NW_GFX_MODEL_TRANSLATE_OFFSET_ENABLED
200             math::MTX34 worldMatrix(model->WorldMatrix());
201             MTX34MultTranslate(&worldMatrix, m_RenderContext->ModelTranslateOffset(), worldMatrix);
202             shaderProgram->SetUniversal(0, worldMatrix);
203 #else
204             shaderProgram->SetUniversal(0, model->WorldMatrix());
205 #endif
206         }
207         else
208         {
209             this->SetMatrixPalette(
210                 skeletalModel,
211                 *primitiveSet,
212                 boneIndexCount
213             );
214         }
215 
216         m_RenderContext->RenderPrimitive((*primitiveSet).GetPrimitives(currentPrimitiveIndex));
217     }
218 }
219 
220 //----------------------------------------
221 namespace internal
222 {
223 static bool
isIllegal(f32 val)224 isIllegal(f32 val)
225 {
226 //    return isnan(val);
227     return !isfinite(val);
228 }
229 
230 static bool
isIllegal(const math::VEC3 & vec)231 isIllegal(const math::VEC3& vec)
232 {
233     return isIllegal(vec.x) || isIllegal(vec.y) || isIllegal(vec.z);
234 }
235 
236 static bool
isIllegal(const math::MTX34 & mtx)237 isIllegal(const math::MTX34& mtx)
238 {
239     return
240         isIllegal(mtx._00) || isIllegal(mtx._01) || isIllegal(mtx._02) || isIllegal(mtx._03) ||
241         isIllegal(mtx._10) || isIllegal(mtx._11) || isIllegal(mtx._12) || isIllegal(mtx._13) ||
242         isIllegal(mtx._20) || isIllegal(mtx._21) || isIllegal(mtx._22) || isIllegal(mtx._23);
243 }
244 }
245 
246 //----------------------------------------
247 void
RenderParticleShape(Model * model,ResParticleShape resource,int shapeIndex)248 MeshRenderer::RenderParticleShape(
249     Model* model,
250     ResParticleShape resource,
251     int shapeIndex
252 )
253 {
254     NW_NULL_ASSERT(m_RenderContext);
255     if (!resource.IsValid()) { return; }
256 
257     ParticleModel* particleModel = static_cast<ParticleModel*>(model);
258     NW_NULL_ASSERT(particleModel);
259 
260     ParticleSet* particleSet = particleModel->GetParticleSets(shapeIndex);
261 
262     const ResParticleSet& resParticleSet = particleSet->GetResParticleSet();
263 
264     ParticleShape* particleShape = particleModel->GetParticleShapes(shapeIndex);
265     NW_NULL_ASSERT(particleShape);
266 
267     NW_ASSERT(particleShape->GetResParticleShape().ptr() == resource.ptr());
268 
269     if (particleSet->GetParticleCollection()->GetCount() == 0)
270     {
271         return;
272     }
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