Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright 2016 Google Inc. | 2 * Copyright 2016 Google Inc. |
| 3 * | 3 * |
| 4 * Use of this source code is governed by a BSD-style license that can be | 4 * Use of this source code is governed by a BSD-style license that can be |
| 5 * found in the LICENSE file. | 5 * found in the LICENSE file. |
| 6 */ | 6 */ |
| 7 | 7 |
| 8 #include "SkError.h" | 8 #include "SkError.h" |
| 9 #include "SkErrorInternals.h" | 9 #include "SkErrorInternals.h" |
| 10 #include "SkLightingShader.h" | 10 #include "SkLightingShader.h" |
| 11 #include "SkMatrix.h" | |
| 11 #include "SkNormalSource.h" | 12 #include "SkNormalSource.h" |
| 12 #include "SkReadBuffer.h" | 13 #include "SkReadBuffer.h" |
| 13 #include "SkWriteBuffer.h" | 14 #include "SkWriteBuffer.h" |
| 14 | 15 |
| 15 // Genretating vtable | 16 // Genretating vtable |
| 16 SkNormalSource::~SkNormalSource() {} | 17 SkNormalSource::~SkNormalSource() {} |
| 17 | 18 |
| 18 /////////////////////////////////////////////////////////////////////////////// | 19 /////////////////////////////////////////////////////////////////////////////// |
| 19 | 20 |
| 20 class NormalMapSourceImpl : public SkNormalSource { | 21 class NormalMapSourceImpl : public SkNormalSource { |
| 21 public: | 22 public: |
| 22 NormalMapSourceImpl(sk_sp<SkShader> mapShader, const SkVector &normRotation) | 23 NormalMapSourceImpl(sk_sp<SkShader> mapShader, const SkMatrix& invCTM) |
| 23 : fMapShader(std::move(mapShader)) | 24 : fMapShader(std::move(mapShader)) |
| 24 , fNormRotation(normRotation) {} | 25 , fInvCTM(invCTM) {} |
| 25 | 26 |
| 26 #if SK_SUPPORT_GPU | 27 #if SK_SUPPORT_GPU |
| 27 sk_sp<GrFragmentProcessor> asFragmentProcessor(GrContext*, | 28 sk_sp<GrFragmentProcessor> asFragmentProcessor(GrContext*, |
| 28 const SkMatrix& viewM, | 29 const SkMatrix& viewM, |
| 29 const SkMatrix* localMatrix, | 30 const SkMatrix* localMatrix, |
| 30 SkFilterQuality, | 31 SkFilterQuality, |
| 31 SkSourceGammaTreatment) const override; | 32 SkSourceGammaTreatment) const override; |
| 32 #endif | 33 #endif |
| 33 | 34 |
| 34 SkNormalSource::Provider* asProvider(const SkShader::ContextRec& rec, | 35 SkNormalSource::Provider* asProvider(const SkShader::ContextRec& rec, |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 51 | 52 |
| 52 void fillScanLine(int x, int y, SkPoint3 output[], int count) const over ride; | 53 void fillScanLine(int x, int y, SkPoint3 output[], int count) const over ride; |
| 53 private: | 54 private: |
| 54 const NormalMapSourceImpl& fSource; | 55 const NormalMapSourceImpl& fSource; |
| 55 SkShader::Context* fMapContext; | 56 SkShader::Context* fMapContext; |
| 56 | 57 |
| 57 typedef SkNormalSource::Provider INHERITED; | 58 typedef SkNormalSource::Provider INHERITED; |
| 58 }; | 59 }; |
| 59 | 60 |
| 60 sk_sp<SkShader> fMapShader; | 61 sk_sp<SkShader> fMapShader; |
| 61 SkVector fNormRotation; | 62 SkMatrix fInvCTM; // Inverse of the canvas total matrix, used for rot ating normals. |
| 62 | 63 |
| 63 friend class SkNormalSource; | 64 friend class SkNormalSource; |
| 64 | 65 |
| 65 typedef SkNormalSource INHERITED; | 66 typedef SkNormalSource INHERITED; |
| 66 }; | 67 }; |
| 67 | 68 |
| 68 //////////////////////////////////////////////////////////////////////////// | 69 //////////////////////////////////////////////////////////////////////////// |
| 69 | 70 |
| 70 #if SK_SUPPORT_GPU | 71 #if SK_SUPPORT_GPU |
| 71 | 72 |
| 72 #include "GrCoordTransform.h" | 73 #include "GrCoordTransform.h" |
| 73 #include "GrInvariantOutput.h" | 74 #include "GrInvariantOutput.h" |
| 74 #include "GrTextureParams.h" | 75 #include "GrTextureParams.h" |
| 75 #include "glsl/GrGLSLFragmentProcessor.h" | 76 #include "glsl/GrGLSLFragmentProcessor.h" |
| 76 #include "glsl/GrGLSLFragmentShaderBuilder.h" | 77 #include "glsl/GrGLSLFragmentShaderBuilder.h" |
| 77 #include "SkGr.h" | 78 #include "SkGr.h" |
| 78 | 79 |
| 79 class NormalMapFP : public GrFragmentProcessor { | 80 class NormalMapFP : public GrFragmentProcessor { |
| 80 public: | 81 public: |
| 81 NormalMapFP(sk_sp<GrFragmentProcessor> mapFP, const SkVector& normRotation) | 82 NormalMapFP(sk_sp<GrFragmentProcessor> mapFP, const SkMatrix& invCTM) |
| 82 : fNormRotation(normRotation) { | 83 : fInvCTM(invCTM) { |
| 83 this->registerChildProcessor(mapFP); | 84 this->registerChildProcessor(mapFP); |
| 84 | 85 |
| 85 this->initClassID<NormalMapFP>(); | 86 this->initClassID<NormalMapFP>(); |
| 86 } | 87 } |
| 87 | 88 |
| 88 class GLSLNormalMapFP : public GrGLSLFragmentProcessor { | 89 class GLSLNormalMapFP : public GrGLSLFragmentProcessor { |
| 89 public: | 90 public: |
| 90 GLSLNormalMapFP() { | 91 GLSLNormalMapFP() { |
| 91 fNormRotation.set(0.0f, 0.0f); | 92 fInvCTM.reset(); |
| 92 } | 93 } |
| 93 | 94 |
| 94 void emitCode(EmitArgs& args) override { | 95 void emitCode(EmitArgs& args) override { |
| 95 | |
| 96 GrGLSLFragmentBuilder* fragBuilder = args.fFragBuilder; | 96 GrGLSLFragmentBuilder* fragBuilder = args.fFragBuilder; |
| 97 GrGLSLUniformHandler* uniformHandler = args.fUniformHandler; | 97 GrGLSLUniformHandler* uniformHandler = args.fUniformHandler; |
| 98 | 98 |
| 99 // add uniform | 99 // add uniform |
| 100 const char* xformUniName = nullptr; | 100 const char* xformUniName = nullptr; |
|
robertphillips
2016/07/06 16:36:33
It looks like we only use the upper left 2x2 - why
dvonbeck
2016/07/06 18:07:48
Done.
| |
| 101 fXformUni = uniformHandler->addUniform(kFragment_GrShaderFlag, | 101 fXformUni = uniformHandler->addUniform(kFragment_GrShaderFlag, kMat3 3f_GrSLType, |
| 102 kVec2f_GrSLType, kDefault_GrS LPrecision, | 102 kDefault_GrSLPrecision, "Xfor m", &xformUniName); |
| 103 "Xform", &xformUniName); | |
| 104 | 103 |
| 105 SkString dstNormalColorName("dstNormalColor"); | 104 SkString dstNormalColorName("dstNormalColor"); |
| 106 this->emitChild(0, nullptr, &dstNormalColorName, args); | 105 this->emitChild(0, nullptr, &dstNormalColorName, args); |
| 107 fragBuilder->codeAppendf("vec3 normal = %s.rgb - vec3(0.5);", | 106 fragBuilder->codeAppendf("vec3 normal = normalize(%s.rgb - vec3(0.5) );", |
| 108 dstNormalColorName.c_str()); | 107 dstNormalColorName.c_str()); |
| 109 | 108 |
| 110 // TODO: inverse map the light direction vectors in the vertex shade r rather than | 109 // TODO: inverse map the light direction vectors in the vertex shade r rather than |
| 111 // transforming all the normals here! | 110 // transforming all the normals here! |
| 112 fragBuilder->codeAppendf( | |
| 113 "mat3 m = mat3(%s.x, -%s.y, 0.0, %s.y, %s.x, 0.0, 0.0, 0.0, 1.0);", | |
| 114 xformUniName, xformUniName, xformUniName, xformUniName); | |
| 115 | 111 |
|
robertphillips
2016/07/06 16:36:34
A branch in the shader is expensive !
dvonbeck
2016/07/06 18:07:48
The code seemed to run just fine ignoring the divi
robertphillips
2016/07/06 19:11:14
I would say leave the guard for now. I just wanted
dvonbeck
2016/07/06 19:34:36
Alright! I think it should be fine here considerin
| |
| 116 fragBuilder->codeAppend("normal = normalize(m*normal);"); | 112 // If there's no x & y components, return (0, 0, +/- 1) instead to a void division by 0 |
| 117 fragBuilder->codeAppendf("%s = vec4(normal, 0);", args.fOutputColor) ; | 113 fragBuilder->codeAppend( "if (abs(normal.z) > 0.9999) {"); |
|
robertphillips
2016/07/06 16:36:33
Might as well just set it to (0, 0, 1, 0).
dvonbeck
2016/07/06 18:07:48
Needs to handle (0, 0, -1, 0) as well. I could set
robertphillips
2016/07/06 19:11:14
Okay - leave it as is.
| |
| 114 fragBuilder->codeAppendf(" %s = normalize(vec4(0.0, 0.0, normal.z , 0.0));", | |
| 115 args.fOutputColor); | |
| 116 // Else, Normalizing the transformed X and Y, while keeping constant both Z and the | |
| 117 // vector's angle in the XY plane. This maintains the "slope" for th e surface while | |
| 118 // appropriately rotating the normal for any anisotropic scaling tha t occurs. | |
| 119 // Here, we call scaling factor the number that must divide the tran sformed X and Y so | |
| 120 // that the normal's length remains equal to 1. | |
| 121 fragBuilder->codeAppend( "} else {"); | |
| 122 fragBuilder->codeAppendf(" vec2 transformed = mat2(%s) * normal.x y;", | |
| 123 xformUniName); | |
| 124 fragBuilder->codeAppend( " float scalingFactorSquared = " | |
|
robertphillips
2016/07/06 16:36:34
Using pows for squares seems a bit overkill.
dvonbeck
2016/07/06 18:07:48
Should I be using *?
robertphillips
2016/07/06 19:11:14
Right, I would just have multiplies. The compiler
dvonbeck
2016/07/06 19:34:36
Done.
| |
| 125 "(pow(transformed.x, 2) + pow(t ransformed.y, 2))" | |
| 126 "/(1.0 - pow(normal.z, 2));"); | |
| 127 fragBuilder->codeAppendf(" %s = vec4(transformed*inversesqrt(scal ingFactorSquared)," | |
| 128 "normal.z, 0.0);", | |
| 129 args.fOutputColor); | |
| 130 fragBuilder->codeAppend( "}"); | |
| 118 } | 131 } |
| 119 | 132 |
| 120 static void GenKey(const GrProcessor& proc, const GrGLSLCaps&, | 133 static void GenKey(const GrProcessor& proc, const GrGLSLCaps&, |
| 121 GrProcessorKeyBuilder* b) { | 134 GrProcessorKeyBuilder* b) { |
| 122 b->add32(0x0); | 135 b->add32(0x0); |
| 123 } | 136 } |
| 124 | 137 |
| 125 protected: | 138 protected: |
| 126 void onSetData(const GrGLSLProgramDataManager& pdman, const GrProcessor& proc) override { | 139 void onSetData(const GrGLSLProgramDataManager& pdman, const GrProcessor& proc) override { |
| 127 const NormalMapFP& normalMapFP = proc.cast<NormalMapFP>(); | 140 const NormalMapFP& normalMapFP = proc.cast<NormalMapFP>(); |
| 128 | 141 |
| 129 const SkVector& normRotation = normalMapFP.normRotation(); | 142 const SkMatrix& invCTM = normalMapFP.invCTM(); |
| 130 if (normRotation != fNormRotation) { | 143 fInvCTM = invCTM; |
| 131 pdman.set2fv(fXformUni, 1, &normRotation.fX); | 144 pdman.setSkMatrix(fXformUni, fInvCTM); |
| 132 fNormRotation = normRotation; | |
| 133 } | |
| 134 } | 145 } |
| 135 | 146 |
| 136 private: | 147 private: |
| 137 SkVector fNormRotation; | 148 SkMatrix fInvCTM; |
| 138 GrGLSLProgramDataManager::UniformHandle fXformUni; | 149 GrGLSLProgramDataManager::UniformHandle fXformUni; |
| 139 }; | 150 }; |
| 140 | 151 |
| 141 void onGetGLSLProcessorKey(const GrGLSLCaps& caps, GrProcessorKeyBuilder* b) const override { | 152 void onGetGLSLProcessorKey(const GrGLSLCaps& caps, GrProcessorKeyBuilder* b) const override { |
| 142 GLSLNormalMapFP::GenKey(*this, caps, b); | 153 GLSLNormalMapFP::GenKey(*this, caps, b); |
| 143 } | 154 } |
| 144 | 155 |
| 145 const char* name() const override { return "NormalMapFP"; } | 156 const char* name() const override { return "NormalMapFP"; } |
| 146 | 157 |
| 147 void onComputeInvariantOutput(GrInvariantOutput* inout) const override { | 158 void onComputeInvariantOutput(GrInvariantOutput* inout) const override { |
| 148 inout->setToUnknown(GrInvariantOutput::ReadInput::kWillNot_ReadInput); | 159 inout->setToUnknown(GrInvariantOutput::ReadInput::kWillNot_ReadInput); |
| 149 } | 160 } |
| 150 | 161 |
| 151 const SkVector& normRotation() const { return fNormRotation; } | 162 const SkMatrix& invCTM() const { return fInvCTM; } |
| 152 | 163 |
| 153 private: | 164 private: |
| 154 GrGLSLFragmentProcessor* onCreateGLSLInstance() const override { return new GLSLNormalMapFP; } | 165 GrGLSLFragmentProcessor* onCreateGLSLInstance() const override { return new GLSLNormalMapFP; } |
| 155 | 166 |
| 156 bool onIsEqual(const GrFragmentProcessor& proc) const override { | 167 bool onIsEqual(const GrFragmentProcessor& proc) const override { |
| 157 const NormalMapFP& normalMapFP = proc.cast<NormalMapFP>(); | 168 const NormalMapFP& normalMapFP = proc.cast<NormalMapFP>(); |
| 158 return fNormRotation == normalMapFP.fNormRotation; | 169 return fInvCTM == normalMapFP.fInvCTM; |
| 159 } | 170 } |
| 160 | 171 |
| 161 SkVector fNormRotation; | 172 SkMatrix fInvCTM; |
| 162 }; | 173 }; |
| 163 | 174 |
| 164 sk_sp<GrFragmentProcessor> NormalMapSourceImpl::asFragmentProcessor( | 175 sk_sp<GrFragmentProcessor> NormalMapSourceImpl::asFragmentProcessor( |
| 165 GrContext *context, | 176 GrContext *context, |
| 166 const SkMatrix &viewM, | 177 const SkMatrix &viewM, |
| 167 const SkMatrix *localMatrix , | 178 const SkMatrix *localMatrix , |
| 168 SkFilterQuality filterQuali ty, | 179 SkFilterQuality filterQuali ty, |
| 169 SkSourceGammaTreatment gamm aTreatment) const { | 180 SkSourceGammaTreatment gamm aTreatment) const { |
| 170 | 181 |
| 171 sk_sp<GrFragmentProcessor> mapFP = fMapShader->asFragmentProcessor(context, viewM, | 182 sk_sp<GrFragmentProcessor> mapFP = fMapShader->asFragmentProcessor(context, viewM, |
| 172 localMatrix, filterQuality, gammaTreatment); | 183 localMatrix, filterQuality, gammaTreatment); |
| 173 | 184 |
| 174 return sk_make_sp<NormalMapFP>(std::move(mapFP), fNormRotation); | 185 return sk_make_sp<NormalMapFP>(std::move(mapFP), fInvCTM); |
| 175 } | 186 } |
| 176 | 187 |
| 177 #endif // SK_SUPPORT_GPU | 188 #endif // SK_SUPPORT_GPU |
| 178 | 189 |
| 179 //////////////////////////////////////////////////////////////////////////// | 190 //////////////////////////////////////////////////////////////////////////// |
| 180 | 191 |
| 181 NormalMapSourceImpl::Provider::Provider(const NormalMapSourceImpl& source, | 192 NormalMapSourceImpl::Provider::Provider(const NormalMapSourceImpl& source, |
| 182 SkShader::Context* mapContext) | 193 SkShader::Context* mapContext) |
| 183 : fSource(source) | 194 : fSource(source) |
| 184 , fMapContext(mapContext) { | 195 , fMapContext(mapContext) { |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 231 | 242 |
| 232 fMapContext->shadeSpan(x, y, tmpNormalColors, n); | 243 fMapContext->shadeSpan(x, y, tmpNormalColors, n); |
| 233 | 244 |
| 234 for (int i = 0; i < n; i++) { | 245 for (int i = 0; i < n; i++) { |
| 235 SkPoint3 tempNorm; | 246 SkPoint3 tempNorm; |
| 236 | 247 |
| 237 tempNorm.set(SkIntToScalar(SkGetPackedR32(tmpNormalColors[i])) - 127 .0f, | 248 tempNorm.set(SkIntToScalar(SkGetPackedR32(tmpNormalColors[i])) - 127 .0f, |
| 238 SkIntToScalar(SkGetPackedG32(tmpNormalColors[i])) - 127 .0f, | 249 SkIntToScalar(SkGetPackedG32(tmpNormalColors[i])) - 127 .0f, |
| 239 SkIntToScalar(SkGetPackedB32(tmpNormalColors[i])) - 127 .0f); | 250 SkIntToScalar(SkGetPackedB32(tmpNormalColors[i])) - 127 .0f); |
| 240 tempNorm.normalize(); | 251 tempNorm.normalize(); |
| 241 | 252 |
|
robertphillips
2016/07/06 16:36:33
There 3 overlength lines here.
dvonbeck
2016/07/06 18:07:48
Done.
| |
| 242 output[i].fX = fSource.fNormRotation.fX * tempNorm.fX + | 253 if (!SkScalarNearlyEqual(SkScalarAbs(tempNorm.fZ), 1.0f)) { |
| 243 fSource.fNormRotation.fY * tempNorm.fY; | 254 SkVector transformed = fSource.fInvCTM.mapVector(tempNorm.fX, te mpNorm.fY); |
| 244 output[i].fY = -fSource.fNormRotation.fY * tempNorm.fX + | 255 |
| 245 fSource.fNormRotation.fX * tempNorm.fY; | 256 // Normalizing the transformed X and Y, while keeping constant b oth Z and the vector's |
| 246 output[i].fZ = tempNorm.fZ; | 257 // angle in the XY plane. This maintains the "slope" for the sur face while appropriately |
| 258 // rotating the normal for any anisotropic scaling that occurs. | |
| 259 // Here, we call scaling factor the number that must divide the transformed X and Y so | |
| 260 // that the normal's length remains equal to 1. | |
| 261 SkScalar scalingFactorSquared = | |
| 262 (SkScalarSquare(transformed.fX) + SkScalarSquare(transfo rmed.fY)) | |
| 263 / (1.0f - SkScalarSquare(tempNorm.fZ)); | |
| 264 SkScalar invScalingFactor = SkScalarInvert(SkScalarSqrt(scalingF actorSquared)); | |
| 265 | |
| 266 output[i].fX = transformed.fX * invScalingFactor; | |
| 267 output[i].fY = transformed.fY * invScalingFactor; | |
| 268 output[i].fZ = tempNorm.fZ; | |
| 269 } else { | |
|
robertphillips
2016/07/06 16:36:34
Might as well just set this to (0, 0, 1) and skip
dvonbeck
2016/07/06 18:07:48
Needs to handle (0, 0, -1) as well. I could set it
robertphillips
2016/07/06 19:11:14
Nope, just leave it as is.
| |
| 270 output[i] = {0.0f, 0.0f, tempNorm.fZ}; | |
| 271 output[i].normalize(); | |
| 272 } | |
| 273 | |
| 274 SkASSERT(SkScalarNearlyEqual(output[i].length(), 1.0f)) | |
| 247 } | 275 } |
| 248 | 276 |
| 249 output += n; | 277 output += n; |
| 250 x += n; | 278 x += n; |
| 251 count -= n; | 279 count -= n; |
| 252 } while (count > 0); | 280 } while (count > 0); |
| 253 } | 281 } |
| 254 | 282 |
| 255 //////////////////////////////////////////////////////////////////////////////// | 283 //////////////////////////////////////////////////////////////////////////////// |
| 256 | 284 |
| 257 sk_sp<SkFlattenable> NormalMapSourceImpl::CreateProc(SkReadBuffer& buf) { | 285 sk_sp<SkFlattenable> NormalMapSourceImpl::CreateProc(SkReadBuffer& buf) { |
| 258 | 286 |
| 259 sk_sp<SkShader> mapShader = buf.readFlattenable<SkShader>(); | 287 sk_sp<SkShader> mapShader = buf.readFlattenable<SkShader>(); |
| 260 | 288 |
| 261 SkVector normRotation = {1,0}; | 289 SkMatrix invCTM; |
| 262 if (!buf.isVersionLT(SkReadBuffer::kLightingShaderWritesInvNormRotation)) { | 290 buf.readMatrix(&invCTM); |
| 263 normRotation = buf.readPoint(); | |
| 264 } | |
| 265 | 291 |
| 266 return sk_make_sp<NormalMapSourceImpl>(std::move(mapShader), normRotation); | 292 return sk_make_sp<NormalMapSourceImpl>(std::move(mapShader), invCTM); |
| 267 } | 293 } |
| 268 | 294 |
| 269 void NormalMapSourceImpl::flatten(SkWriteBuffer& buf) const { | 295 void NormalMapSourceImpl::flatten(SkWriteBuffer& buf) const { |
| 270 this->INHERITED::flatten(buf); | 296 this->INHERITED::flatten(buf); |
| 271 | 297 |
| 272 buf.writeFlattenable(fMapShader.get()); | 298 buf.writeFlattenable(fMapShader.get()); |
| 273 buf.writePoint(fNormRotation); | 299 buf.writeMatrix(fInvCTM); |
| 274 } | 300 } |
| 275 | 301 |
| 276 //////////////////////////////////////////////////////////////////////////// | 302 //////////////////////////////////////////////////////////////////////////// |
| 277 | 303 |
| 278 sk_sp<SkNormalSource> SkNormalSource::MakeFromNormalMap(sk_sp<SkShader> map, | 304 sk_sp<SkNormalSource> SkNormalSource::MakeFromNormalMap(sk_sp<SkShader> map, con st SkMatrix& ctm) { |
| 279 const SkVector &normRota tion) { | 305 SkMatrix invCTM; |
| 280 SkASSERT(SkScalarNearlyEqual(normRotation.lengthSqd(), SK_Scalar1)); | 306 |
| 281 if (!map) { | 307 if (!ctm.invert(&invCTM) || !map) { |
| 282 return nullptr; | 308 return nullptr; |
| 283 } | 309 } |
| 284 | 310 |
| 285 return sk_make_sp<NormalMapSourceImpl>(std::move(map), normRotation); | 311 return sk_make_sp<NormalMapSourceImpl>(std::move(map), invCTM); |
| 286 } | 312 } |
| 287 | 313 |
| 288 //////////////////////////////////////////////////////////////////////////// | 314 //////////////////////////////////////////////////////////////////////////// |
| 289 | 315 |
| 290 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkNormalSource) | 316 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkNormalSource) |
| 291 SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(NormalMapSourceImpl) | 317 SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(NormalMapSourceImpl) |
| 292 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END | 318 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END |
| 293 | 319 |
| 294 //////////////////////////////////////////////////////////////////////////// | 320 //////////////////////////////////////////////////////////////////////////// |
| OLD | NEW |