Project

General

Profile

« Previous | Next » 

Revision 02de77c9

Added by Leszek Koltunski almost 8 years ago

Progress with multi-program rendering.

View differences:

src/main/java/org/distorted/library/Distorted.java
77 77
   */
78 78
  public static final int CLONE_CHILDREN= 0x20;
79 79

  
80
  static int[] mMainProgramAttributes;
81
  static int[] mPostProgramAttributes;
82

  
83
  static int mainProgramH, postProgramH;
80
  static DistortedProgram mMainProgram, mPostProgram;
84 81

  
85 82
///////////////////////////////////////////////////////////////////////////////////////////////////
86 83
// private: hide this from Javadoc
......
94 91

  
95 92
  static boolean isInitialized()
96 93
    {
97
    return (mMainProgramAttributes!=null);
94
    return (mMainProgram!=null);
98 95
    }
99 96

  
100 97
///////////////////////////////////////////////////////////////////////////////////////////////////
......
135 132
        mainFragmentHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
136 133
      }
137 134

  
138
    DistortedProgram mainProgram = new DistortedProgram(mainVertexStream,mainFragmentStream, mainVertexHeader, mainFragmentHeader);
139
    mainProgramH = mainProgram.getProgramHandle();
140
    GLES20.glUseProgram(mainProgramH);
141
    mainProgram.bindAndEnableAttributes();
142
    mMainProgramAttributes = mainProgram.getAttributes();
135
    mMainProgram = new DistortedProgram(mainVertexStream,mainFragmentStream, mainVertexHeader, mainFragmentHeader);
136
    int mainProgramH = mMainProgram.getProgramHandle();
143 137

  
144 138
    GLES20.glDepthFunc(GLES20.GL_LEQUAL);
145 139
    GLES20.glEnable(GLES20.GL_BLEND);
......
164 158
        postFragmentHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
165 159
      }
166 160

  
167
    DistortedProgram postProgram = new DistortedProgram(postVertexStream,postFragmentStream, "", postFragmentHeader);
168
    postProgramH = postProgram.getProgramHandle();
169
    GLES20.glUseProgram(postProgramH);
170
    postProgram.bindAndEnableAttributes();
171
    mPostProgramAttributes = postProgram.getAttributes();
161
    mPostProgram = new DistortedProgram(postVertexStream,postFragmentStream, "", postFragmentHeader);
162
    int postProgramH = mPostProgram.getProgramHandle();
172 163

  
173 164
    EffectQueuePostprocess.getUniforms(postProgramH);
174 165

  
......
190 181
    DistortedEffects.onDestroy();
191 182
    EffectMessageSender.stopSending();
192 183

  
193
    mMainProgramAttributes = null;
194
    mPostProgramAttributes = null;
184
    mMainProgram = null;
185
    mPostProgram = null;
195 186
    }
196 187
  }
src/main/java/org/distorted/library/DistortedEffects.java
133 133

  
134 134
    float halfZ = halfInputW*mesh.zFactor;
135 135

  
136
    GLES20.glUseProgram(Distorted.mainProgramH);
137
    GLES20.glViewport(0, 0, df.mWidth, df.mHeight);
138

  
139 136
    if( mP.mNumEffects==0 )
140 137
      {
138
      Distorted.mMainProgram.useProgram();
139
      GLES20.glViewport(0, 0, df.mWidth, df.mHeight);
140

  
141 141
      mM.send(df,halfInputW,halfInputH,halfZ);
142 142
      mV.send(halfInputW,halfInputH,halfZ);
143 143
      mF.send(halfInputW,halfInputH);
......
146 146
      }
147 147
    else
148 148
      {
149
      if( mV.mNumEffects>0 || mP.mNumEffects>0 )
149
      if( mV.mNumEffects>0 || mF.mNumEffects>0 )
150 150
        {
151
        Distorted.mMainProgram.useProgram();
152
        GLES20.glViewport(0, 0, df.mWidth, df.mHeight);
153

  
151 154
        mBufferFBO.resizeFast(df.mWidth, df.mHeight);
152 155
        mBufferFBO.setAsOutput();
153 156

  
......
157 160

  
158 161
        mesh.draw();
159 162

  
160
        GLES20.glUseProgram(Distorted.postProgramH);
161
        GLES20.glViewport(0, 0, df.mWidth, df.mHeight);
163
        Distorted.mPostProgram.useProgram();
162 164
        mBufferFBO.setAsInput();
163 165
        df.setAsOutput();
164 166
        mP.send(df);
165 167

  
166
        GLES20.glVertexAttribPointer(Distorted.mPostProgramAttributes[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mQuadPositions);
167
        GLES20.glVertexAttribPointer(Distorted.mPostProgramAttributes[1], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mQuadTexture);
168
        GLES20.glVertexAttribPointer(Distorted.mPostProgram.mAttribute[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mQuadPositions);
169
        GLES20.glVertexAttribPointer(Distorted.mPostProgram.mAttribute[1], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mQuadTexture);
168 170
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
169 171
        }
170 172
      else
171 173
        {
172
        GLES20.glUseProgram(Distorted.postProgramH);
174
        Distorted.mPostProgram.useProgram();
173 175
        GLES20.glViewport(0, 0, df.mWidth, df.mHeight);
174
        mP.send(df);
175 176

  
176
        GLES20.glVertexAttribPointer(Distorted.mPostProgramAttributes[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mQuadPositions);
177
        GLES20.glVertexAttribPointer(Distorted.mPostProgramAttributes[1], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mQuadTexture);
177
        mM.constructMatrices(df,halfInputW,halfInputH);
178
        mP.send(2*halfInputW,2*halfInputH, mM.getMVP() );
179

  
180
        GLES20.glVertexAttribPointer(Distorted.mPostProgram.mAttribute[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mQuadPositions);
181
        GLES20.glVertexAttribPointer(Distorted.mPostProgram.mAttribute[1], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mQuadTexture);
178 182
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
179 183
        }
180 184
      }
src/main/java/org/distorted/library/DistortedFramebuffer.java
154 154
    mMarked = false;
155 155
    }
156 156

  
157
///////////////////////////////////////////////////////////////////////////////////////////////////
158

  
159
  void reset()
160
    {
161
    if( colorIds[0]!=DONT_CREATE )
162
      colorIds[0] = NOT_CREATED_YET;
163

  
164
    if( mDepthWanted ) depthIds[0] = NOT_CREATED_YET;
165
    }
166

  
167 157
///////////////////////////////////////////////////////////////////////////////////////////////////
168 158

  
169 159
  void setAsOutput()
......
228 218

  
229 219
  static synchronized void onDestroy()
230 220
    {
231
    // There are issues with this. Namely, if one
232
    // 1. creates a DObjectTree (somewhere else than onSurfaceCreated of constructor so it does not get re-created on re-launch)
233
    // 2. exits the app (here mList would be cleared)
234
    // 3. re-launches the app
235
    // 4. deletes some nodes
236
    // then the underlying Framebuffers will never be deleted!
237

  
238
    mListMarked = false;
239
    mList.clear();
221
    for( DistortedFramebuffer fbo : mList)
222
      {
223
      if( fbo.colorIds[0]!=DONT_CREATE ) fbo.colorIds[0] = NOT_CREATED_YET;
224
      if( fbo.mDepthWanted             ) fbo.depthIds[0] = NOT_CREATED_YET;
225
      }
240 226
    }
241 227

  
242 228
///////////////////////////////////////////////////////////////////////////////////////////////////
src/main/java/org/distorted/library/DistortedTree.java
160 160
      {
161 161
      tmp = mMapNodeID.get(key);
162 162
          
163
      if( tmp.mFBO != null )
164
        {
165
    	  tmp.mFBO.reset();
166
        tmp.numRendered = 0;
167
        }
163
      if( tmp.mFBO != null ) tmp.numRendered = 0;
168 164
      }
169 165
    }
170 166

  
src/main/java/org/distorted/library/EffectQueueMatrix.java
105 105
    }
106 106

  
107 107
///////////////////////////////////////////////////////////////////////////////////////////////////
108
// here construct the ModelView and the ModelViewProjection Matrices
108 109

  
109
  static void getUniforms(int mProgramH)
110
    {
111
    mObjDH     = GLES20.glGetUniformLocation(mProgramH, "u_objD");
112
    mDepthH    = GLES20.glGetUniformLocation(mProgramH, "u_Depth");
113
    mMVPMatrixH= GLES20.glGetUniformLocation(mProgramH, "u_MVPMatrix");
114
    mMVMatrixH = GLES20.glGetUniformLocation(mProgramH, "u_MVMatrix"); 
115
    }
116

  
117
///////////////////////////////////////////////////////////////////////////////////////////////////
118
  
119
  synchronized void compute(long currTime) 
120
    {
121
    if( currTime==mTime ) return;
122
    if( mTime==0 ) mTime = currTime;
123
    long step = (currTime-mTime);
124
   
125
    for(int i=0; i<mNumEffects; i++)
126
      {
127
      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
128
        {
129
        for(int j=0; j<mNumListeners; j++)
130
          EffectMessageSender.newMessage( mListeners.elementAt(j),
131
                                          EffectMessage.EFFECT_FINISHED,
132
                                         (mID[i]<<EffectTypes.LENGTH)+EffectTypes.MATRIX.type,
133
                                          mName[i],
134
                                          mObjectID);
135

  
136
        if( EffectNames.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
137
          {
138
          remove(i);
139
          i--;
140
          continue;
141
          }
142
        else mInter[0][i] = null;
143
        }
144

  
145
      if( mInter[1][i]!=null )
146
        {
147
        mInter[1][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+4, mCurrentDuration[i], step);
148
        }
149

  
150
      mCurrentDuration[i] += step;
151
      }
152
     
153
    mTime = currTime;  
154
    }  
155

  
156
///////////////////////////////////////////////////////////////////////////////////////////////////
157

  
158
  protected void moveEffect(int index)
159
    {
160
    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
161
    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
162
    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
163
    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
164
    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
165
    mUniforms[NUM_UNIFORMS*index+5] = mUniforms[NUM_UNIFORMS*(index+1)+5];
166
    mUniforms[NUM_UNIFORMS*index+6] = mUniforms[NUM_UNIFORMS*(index+1)+6];
167
    }
168

  
169
///////////////////////////////////////////////////////////////////////////////////////////////////
170
// here construct the ModelView Matrix
171

  
172
  synchronized void send(DistortedFramebuffer df, float halfX, float halfY, float halfZ)
110
  void constructMatrices(DistortedFramebuffer df, float halfX, float halfY)
173 111
    {
174 112
    Matrix.setIdentityM(mViewMatrix, 0);
175 113
    Matrix.translateM(mViewMatrix, 0, -df.mWidth/2, df.mHeight/2, -df.mDistance);
176
    
114

  
177 115
    float x,y,z, sx,sy,sz;
178
   
116

  
179 117
    for(int i=0; i<mNumEffects; i++)
180 118
      {
181 119
      if (mName[i] == EffectNames.ROTATE.ordinal() )
......
193 131
        x = mUniforms[NUM_UNIFORMS*i+4];
194 132
        y = mUniforms[NUM_UNIFORMS*i+5];
195 133
        z = mUniforms[NUM_UNIFORMS*i+6];
196
     	
134

  
197 135
        Matrix.translateM(mViewMatrix, 0, x,-y, z);
198 136
        multiplyByQuat(mViewMatrix, mUniforms[NUM_UNIFORMS*i], mUniforms[NUM_UNIFORMS*i+1], mUniforms[NUM_UNIFORMS*i+2], mUniforms[NUM_UNIFORMS*i+3]);
199 137
        Matrix.translateM(mViewMatrix, 0,-x, y,-z);
......
203 141
        sx = mUniforms[NUM_UNIFORMS*i  ];
204 142
        sy = mUniforms[NUM_UNIFORMS*i+1];
205 143
        sz = mUniforms[NUM_UNIFORMS*i+2];
206
        
144

  
207 145
        Matrix.translateM(mViewMatrix, 0, sx,-sy, sz);
208 146
        }
209 147
      else if(mName[i] == EffectNames.SCALE.ordinal() )
......
225 163
        z  = mUniforms[NUM_UNIFORMS*i+6];
226 164

  
227 165
        Matrix.translateM(mViewMatrix, 0, x,-y, z);
228
      
166

  
229 167
        mViewMatrix[4] += sx*mViewMatrix[0]; // Multiply viewMatrix by 1 x 0 0 , i.e. X-shear.
230 168
        mViewMatrix[5] += sx*mViewMatrix[1]; //                        0 1 0 0
231 169
        mViewMatrix[6] += sx*mViewMatrix[2]; //                        0 0 1 0
232 170
        mViewMatrix[7] += sx*mViewMatrix[3]; //                        0 0 0 1
233
      
171

  
234 172
        mViewMatrix[0] += sy*mViewMatrix[4]; // Multiply viewMatrix by 1 0 0 0 , i.e. Y-shear.
235 173
        mViewMatrix[1] += sy*mViewMatrix[5]; //                        y 1 0 0
236 174
        mViewMatrix[2] += sy*mViewMatrix[6]; //                        0 0 1 0
237 175
        mViewMatrix[3] += sy*mViewMatrix[7]; //                        0 0 0 1
238
      
176

  
239 177
        mViewMatrix[4] += sz*mViewMatrix[8]; // Multiply viewMatrix by 1 0 0 0 , i.e. Z-shear.
240 178
        mViewMatrix[5] += sz*mViewMatrix[9]; //                        0 1 0 0
241 179
        mViewMatrix[6] += sz*mViewMatrix[10];//                        0 z 1 0
......
244 182
        Matrix.translateM(mViewMatrix, 0,-x, y, -z);
245 183
        }
246 184
      }
247
   
185

  
248 186
    Matrix.translateM(mViewMatrix, 0, halfX,-halfY, 0);
249 187
    Matrix.multiplyMM(mMVPMatrix, 0, df.mProjectionMatrix, 0, mViewMatrix, 0);
250
    
188
    }
189

  
190
///////////////////////////////////////////////////////////////////////////////////////////////////
191

  
192
  static void getUniforms(int mProgramH)
193
    {
194
    mObjDH     = GLES20.glGetUniformLocation(mProgramH, "u_objD");
195
    mDepthH    = GLES20.glGetUniformLocation(mProgramH, "u_Depth");
196
    mMVPMatrixH= GLES20.glGetUniformLocation(mProgramH, "u_MVPMatrix");
197
    mMVMatrixH = GLES20.glGetUniformLocation(mProgramH, "u_MVMatrix"); 
198
    }
199

  
200
///////////////////////////////////////////////////////////////////////////////////////////////////
201

  
202
  float[] getMVP()
203
    {
204
    return mMVPMatrix;
205
    }
206

  
207
///////////////////////////////////////////////////////////////////////////////////////////////////
208

  
209
  synchronized void compute(long currTime) 
210
    {
211
    if( currTime==mTime ) return;
212
    if( mTime==0 ) mTime = currTime;
213
    long step = (currTime-mTime);
214
   
215
    for(int i=0; i<mNumEffects; i++)
216
      {
217
      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
218
        {
219
        for(int j=0; j<mNumListeners; j++)
220
          EffectMessageSender.newMessage( mListeners.elementAt(j),
221
                                          EffectMessage.EFFECT_FINISHED,
222
                                         (mID[i]<<EffectTypes.LENGTH)+EffectTypes.MATRIX.type,
223
                                          mName[i],
224
                                          mObjectID);
225

  
226
        if( EffectNames.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
227
          {
228
          remove(i);
229
          i--;
230
          continue;
231
          }
232
        else mInter[0][i] = null;
233
        }
234

  
235
      if( mInter[1][i]!=null )
236
        {
237
        mInter[1][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+4, mCurrentDuration[i], step);
238
        }
239

  
240
      mCurrentDuration[i] += step;
241
      }
242
     
243
    mTime = currTime;  
244
    }  
245

  
246
///////////////////////////////////////////////////////////////////////////////////////////////////
247

  
248
  protected void moveEffect(int index)
249
    {
250
    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
251
    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
252
    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
253
    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
254
    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
255
    mUniforms[NUM_UNIFORMS*index+5] = mUniforms[NUM_UNIFORMS*(index+1)+5];
256
    mUniforms[NUM_UNIFORMS*index+6] = mUniforms[NUM_UNIFORMS*(index+1)+6];
257
    }
258

  
259
///////////////////////////////////////////////////////////////////////////////////////////////////
260

  
261
  synchronized void send(DistortedFramebuffer df, float halfX, float halfY, float halfZ)
262
    {
263
    constructMatrices(df,halfX,halfY);
264

  
251 265
    GLES20.glUniform3f( mObjDH , halfX, halfY, halfZ);
252 266
    GLES20.glUniform1f( mDepthH, df.mDepth);
253 267
    GLES20.glUniformMatrix4fv(mMVMatrixH , 1, false, mViewMatrix, 0);
src/main/java/org/distorted/library/EffectQueuePostprocess.java
113 113
    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
114 114
    }
115 115

  
116
///////////////////////////////////////////////////////////////////////////////////////////////////
117

  
118
  synchronized void send(float w, float h, float[] mvp)
119
    {
120
    GLES20.glUniform1i( mNumEffectsH, mNumEffects);
121
    GLES20.glUniform2f( mObjDH , w, h );
122
    GLES20.glUniformMatrix4fv(mMVPMatrixH, 1, false, mvp , 0);
123

  
124
    if( mNumEffects>0 )
125
      {
126
      GLES20.glUniform1iv( mTypeH    ,  mNumEffects, mName    ,0);
127
      GLES20.glUniform4fv( mUniformsH,2*mNumEffects, mUniforms,0);
128
      }
129
    }
130

  
116 131
///////////////////////////////////////////////////////////////////////////////////////////////////
117 132

  
118 133
  synchronized void send(DistortedFramebuffer df)
src/main/java/org/distorted/library/MeshObject.java
55 55

  
56 56
   void draw()
57 57
     { 
58
     GLES20.glVertexAttribPointer(Distorted.mMainProgramAttributes[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mMeshPositions);
59
     GLES20.glVertexAttribPointer(Distorted.mMainProgramAttributes[1], NORMAL_DATA_SIZE  , GLES20.GL_FLOAT, false, 0, mMeshNormals);
60
     GLES20.glVertexAttribPointer(Distorted.mMainProgramAttributes[2], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mMeshTexture);
58
     GLES20.glVertexAttribPointer(Distorted.mMainProgram.mAttribute[0], POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mMeshPositions);
59
     GLES20.glVertexAttribPointer(Distorted.mMainProgram.mAttribute[1], NORMAL_DATA_SIZE  , GLES20.GL_FLOAT, false, 0, mMeshNormals);
60
     GLES20.glVertexAttribPointer(Distorted.mMainProgram.mAttribute[2], TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mMeshTexture);
61 61

  
62 62
     GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, dataLength); 
63 63
     }
src/main/java/org/distorted/library/program/DistortedProgram.java
23 23
import android.os.Build;
24 24

  
25 25
import org.distorted.library.DistortedEffects;
26
import org.distorted.library.EffectNames;
27
import org.distorted.library.EffectTypes;
28 26

  
29 27
import java.io.BufferedReader;
30 28
import java.io.IOException;
......
40 38
  private int mProgramHandle;
41 39

  
42 40
  private int mNumAttributes;
43
  private int[] mAttribute;
44 41
  private String[] mAttributeName;
45 42

  
43
  public final int[] mAttribute;
44

  
46 45
///////////////////////////////////////////////////////////////////////////////////////////////////
47 46

  
48 47
  private int createAndLinkProgram(final int vertexShaderHandle, final int fragmentShaderHandle, final String[] attributes)
......
168 167

  
169 168
    if( doAttributes )
170 169
      {
171
      mAttribute = new int[mNumAttributes];
172 170
      mAttributeName = attrList.split(" ");
173 171
      }
174 172

  
......
274 272
    final int fragmentShaderHandle = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentHeader + fragmentShader);
275 273

  
276 274
    mProgramHandle = createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle, mAttributeName);
277
    }
278 275

  
279
///////////////////////////////////////////////////////////////////////////////////////////////////
280
/**
281
 * Return the indexes off all attributes.
282
 */
283
  public int[] getAttributes()
284
    {
285
    return mAttribute;
286
    }
276
    mAttribute = new int[mNumAttributes];
287 277

  
288
///////////////////////////////////////////////////////////////////////////////////////////////////
289
/**
290
 * Return the names of all vertex attributes.
291
 */
292
  public String[] getAttributeNames()
293
    {
294
    return mAttributeName;
278
    for(int i=0; i<mNumAttributes; i++)
279
      {
280
      mAttribute[i] = GLES20.glGetAttribLocation( mProgramHandle, mAttributeName[i]);
281
      }
295 282
    }
296 283

  
297 284
///////////////////////////////////////////////////////////////////////////////////////////////////
......
305 292

  
306 293
///////////////////////////////////////////////////////////////////////////////////////////////////
307 294
/**
308
 * Bind all vertex attributes and enable them.
309
 * <p>
310
 * This assumes Program is in use. Call glUseProgram first.
295
 * Use the program and enable all vertex attribute arrays.
296
 *
311 297
 * Needs to be called from a thread holding the OpenGL context.
312 298
 */
313

  
314
  public void bindAndEnableAttributes()
299
  public void useProgram()
315 300
    {
301
    GLES20.glUseProgram(mProgramHandle);
302

  
316 303
    for(int i=0; i<mNumAttributes; i++)
317
      {
318
      mAttribute[i] = GLES20.glGetAttribLocation( mProgramHandle, mAttributeName[i]);
319 304
      GLES20.glEnableVertexAttribArray(mAttribute[i]);
320
      }
321 305
    }
322 306
  }
307

  
308

  

Also available in: Unified diff