diff options
Diffstat (limited to 'OpenSim/Region')
-rw-r--r-- | OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs | 596 |
1 files changed, 596 insertions, 0 deletions
diff --git a/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs b/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs new file mode 100644 index 0000000..8adacab --- /dev/null +++ b/OpenSim/Region/ClientStack/Linden/Caps/BunchOfCaps/MeshCost.cs | |||
@@ -0,0 +1,596 @@ | |||
1 | // Proprietary code of Avination Virtual Limited | ||
2 | // (c) 2012 Melanie Thielker, Leal Duarte | ||
3 | // | ||
4 | |||
5 | using System; | ||
6 | using System.IO; | ||
7 | using System.Collections; | ||
8 | using System.Collections.Generic; | ||
9 | using System.Text; | ||
10 | |||
11 | using OpenMetaverse; | ||
12 | using OpenMetaverse.StructuredData; | ||
13 | |||
14 | using OpenSim.Framework; | ||
15 | using OpenSim.Region.Framework; | ||
16 | using OpenSim.Region.Framework.Scenes; | ||
17 | using OpenSim.Framework.Capabilities; | ||
18 | |||
19 | using ComponentAce.Compression.Libs.zlib; | ||
20 | |||
21 | using OSDArray = OpenMetaverse.StructuredData.OSDArray; | ||
22 | using OSDMap = OpenMetaverse.StructuredData.OSDMap; | ||
23 | |||
24 | namespace OpenSim.Region.ClientStack.Linden | ||
25 | { | ||
26 | public class ModelCost | ||
27 | { | ||
28 | float ModelMinCost = 5.0f; // try to favor small meshs versus sculpts | ||
29 | |||
30 | // scale prices relative to basic cost | ||
31 | const float ModelCostScale = 1.0f; | ||
32 | |||
33 | const float primCreationCost = 0.01f; // 256 prims cost extra 2.56 | ||
34 | |||
35 | // weigthed size to money convertion | ||
36 | const float bytecost = 1e-4f; | ||
37 | |||
38 | // for mesh upload fees based on compressed data sizes | ||
39 | // not using streaming physics and server costs as SL apparently does ?? | ||
40 | |||
41 | const float medSizeWth = 1f; // 2x | ||
42 | const float lowSizeWth = 1.5f; // 2.5x | ||
43 | const float lowestSizeWth = 2f; // 3x | ||
44 | // favor potencial optimized meshs versus automatic decomposition | ||
45 | const float physMeshSizeWth = 6f; // counts 7x | ||
46 | const float physHullSizeWth = 8f; // counts 9x | ||
47 | |||
48 | // price compression to promote complex meshs | ||
49 | const float feeCompressionBase = 50.0f; // transition from linear to log cost | ||
50 | const float feeCompressionScale = 250.0f; // 10000 scales to 1000 | ||
51 | |||
52 | // stream cost size factors | ||
53 | const float highLodFactor = 17.36f; | ||
54 | const float midLodFactor = 277.78f; | ||
55 | const float lowLodFactor = 1111.11f; | ||
56 | |||
57 | const int bytesPerCoord = 6; // 3 coords, 2 bytes per each | ||
58 | |||
59 | private class ameshCostParam | ||
60 | { | ||
61 | public int highLODSize; | ||
62 | public int medLODSize; | ||
63 | public int lowLODSize; | ||
64 | public int lowestLODSize; | ||
65 | public float costFee; | ||
66 | public float physicsCost; | ||
67 | } | ||
68 | |||
69 | public bool MeshModelCost(LLSDAssetResource resources, int basicCost, out int totalcost, LLSDAssetUploadResponseData meshcostdata, out string error) | ||
70 | { | ||
71 | totalcost = 0; | ||
72 | error = string.Empty; | ||
73 | |||
74 | if (resources == null || | ||
75 | resources.instance_list == null || | ||
76 | resources.instance_list.Array.Count == 0) | ||
77 | { | ||
78 | error = "Unable to upload mesh model. missing information."; | ||
79 | return false; | ||
80 | } | ||
81 | |||
82 | meshcostdata.model_streaming_cost = 0.0; | ||
83 | meshcostdata.simulation_cost = 0.0; | ||
84 | meshcostdata.physics_cost = 0.0; | ||
85 | meshcostdata.resource_cost = 0.0; | ||
86 | |||
87 | meshcostdata.upload_price_breakdown.mesh_instance = 0; | ||
88 | meshcostdata.upload_price_breakdown.mesh_physics = 0; | ||
89 | meshcostdata.upload_price_breakdown.mesh_streaming = 0; | ||
90 | meshcostdata.upload_price_breakdown.model = 0; | ||
91 | |||
92 | int itmp; | ||
93 | |||
94 | // textures cost | ||
95 | if (resources.texture_list != null && resources.texture_list.Array.Count > 0) | ||
96 | { | ||
97 | int textures_cost = resources.texture_list.Array.Count; | ||
98 | textures_cost *= basicCost; | ||
99 | |||
100 | meshcostdata.upload_price_breakdown.texture = textures_cost; | ||
101 | totalcost += textures_cost; | ||
102 | } | ||
103 | |||
104 | float meshsfee = 0; | ||
105 | |||
106 | // meshs assets cost | ||
107 | |||
108 | int numberMeshs = 0; | ||
109 | List<ameshCostParam> meshsCosts = new List<ameshCostParam>(); | ||
110 | // a model could have no mesh actually | ||
111 | if (resources.mesh_list != null && resources.mesh_list.Array.Count > 0) | ||
112 | { | ||
113 | numberMeshs = resources.mesh_list.Array.Count; | ||
114 | |||
115 | for (int i = 0; i < numberMeshs; i++) | ||
116 | { | ||
117 | ameshCostParam curCost = new ameshCostParam(); | ||
118 | byte[] data = (byte[])resources.mesh_list.Array[i]; | ||
119 | |||
120 | if (!MeshCost(data, curCost, out error)) | ||
121 | { | ||
122 | return false; | ||
123 | } | ||
124 | meshsCosts.Add(curCost); | ||
125 | meshsfee += curCost.costFee; | ||
126 | } | ||
127 | } | ||
128 | |||
129 | // instances (prims) cost | ||
130 | int numberInstances = resources.instance_list.Array.Count; | ||
131 | int mesh; | ||
132 | for (int i = 0; i < numberInstances; i++) | ||
133 | { | ||
134 | Hashtable inst = (Hashtable)resources.instance_list.Array[i]; | ||
135 | |||
136 | // streamming cost | ||
137 | // assume all instances have a mesh | ||
138 | // but in general they can have normal prims | ||
139 | // but for now that seems not suported | ||
140 | // when they do, we will need to inspect pbs information | ||
141 | // and have cost funtions for all prims types | ||
142 | // don't check for shape type none, since | ||
143 | // that could be used to upload meshs with low cost | ||
144 | // changing later inworld | ||
145 | |||
146 | ArrayList ascale = (ArrayList)inst["scale"]; | ||
147 | Vector3 scale; | ||
148 | double tmp; | ||
149 | tmp = (double)ascale[0]; | ||
150 | scale.X = (float)tmp; | ||
151 | tmp = (double)ascale[1]; | ||
152 | scale.Y = (float)tmp; | ||
153 | tmp = (double)ascale[2]; | ||
154 | scale.Z = (float)tmp; | ||
155 | |||
156 | float sqdiam = scale.LengthSquared(); | ||
157 | |||
158 | mesh = (int)inst["mesh"]; | ||
159 | |||
160 | if(mesh >= numberMeshs) | ||
161 | { | ||
162 | error = "Unable to upload mesh model. incoerent information."; | ||
163 | return false; | ||
164 | } | ||
165 | |||
166 | ameshCostParam curCost = meshsCosts[mesh]; | ||
167 | float mesh_streaming = streamingCost(curCost, sqdiam); | ||
168 | |||
169 | meshcostdata.model_streaming_cost += mesh_streaming; | ||
170 | |||
171 | meshcostdata.physics_cost += curCost.physicsCost; | ||
172 | |||
173 | // unscripted and static prim server cost | ||
174 | meshcostdata.simulation_cost += 0.5f; | ||
175 | // charge for prims creation | ||
176 | meshsfee += primCreationCost; | ||
177 | } | ||
178 | |||
179 | if (meshcostdata.physics_cost <= meshcostdata.model_streaming_cost) | ||
180 | meshcostdata.resource_cost = meshcostdata.model_streaming_cost; | ||
181 | else | ||
182 | meshcostdata.resource_cost = meshcostdata.physics_cost; | ||
183 | |||
184 | if (meshsfee < ModelMinCost) | ||
185 | meshsfee = ModelMinCost; | ||
186 | |||
187 | meshsfee *= ModelCostScale; | ||
188 | meshsfee += 0.5f; // rounding | ||
189 | |||
190 | totalcost += (int)meshsfee; | ||
191 | |||
192 | // breakdown prices | ||
193 | // don't seem to be in use so removed code for now | ||
194 | |||
195 | return true; | ||
196 | } | ||
197 | |||
198 | private bool MeshCost(byte[] data, ameshCostParam cost, out string error) | ||
199 | { | ||
200 | cost.highLODSize = 0; | ||
201 | cost.medLODSize = 0; | ||
202 | cost.lowLODSize = 0; | ||
203 | cost.lowestLODSize = 0; | ||
204 | cost.physicsCost = 0.0f; | ||
205 | cost.costFee = 0.0f; | ||
206 | |||
207 | error = string.Empty; | ||
208 | |||
209 | if (data == null || data.Length == 0) | ||
210 | { | ||
211 | error = "Unable to upload mesh model. missing information."; | ||
212 | return false; | ||
213 | } | ||
214 | |||
215 | OSD meshOsd = null; | ||
216 | int start = 0; | ||
217 | |||
218 | error = "Unable to upload mesh model. Invalid data"; | ||
219 | |||
220 | using (MemoryStream ms = new MemoryStream(data)) | ||
221 | { | ||
222 | try | ||
223 | { | ||
224 | OSD osd = OSDParser.DeserializeLLSDBinary(ms); | ||
225 | if (osd is OSDMap) | ||
226 | meshOsd = (OSDMap)osd; | ||
227 | else | ||
228 | return false; | ||
229 | } | ||
230 | catch (Exception e) | ||
231 | { | ||
232 | return false; | ||
233 | } | ||
234 | start = (int)ms.Position; | ||
235 | } | ||
236 | |||
237 | OSDMap map = (OSDMap)meshOsd; | ||
238 | OSDMap tmpmap; | ||
239 | |||
240 | int highlod_size = 0; | ||
241 | int medlod_size = 0; | ||
242 | int lowlod_size = 0; | ||
243 | int lowestlod_size = 0; | ||
244 | int skin_size = 0; | ||
245 | |||
246 | int hulls_size = 0; | ||
247 | int phys_nhulls; | ||
248 | int phys_hullsvertices = 0; | ||
249 | |||
250 | int physmesh_size = 0; | ||
251 | int phys_ntriangles = 0; | ||
252 | |||
253 | int submesh_offset = -1; | ||
254 | |||
255 | if (map.ContainsKey("physics_convex")) | ||
256 | { | ||
257 | tmpmap = (OSDMap)map["physics_convex"]; | ||
258 | if (tmpmap.ContainsKey("offset")) | ||
259 | submesh_offset = tmpmap["offset"].AsInteger() + start; | ||
260 | if (tmpmap.ContainsKey("size")) | ||
261 | hulls_size = tmpmap["size"].AsInteger(); | ||
262 | } | ||
263 | |||
264 | if (submesh_offset < 0 || hulls_size == 0) | ||
265 | { | ||
266 | error = "Unable to upload mesh model. missing physics_convex block"; | ||
267 | return false; | ||
268 | } | ||
269 | |||
270 | if (!hulls(data, submesh_offset, hulls_size, out phys_hullsvertices, out phys_nhulls)) | ||
271 | { | ||
272 | error = "Unable to upload mesh model. bad physics_convex block"; | ||
273 | return false; | ||
274 | } | ||
275 | |||
276 | submesh_offset = -1; | ||
277 | |||
278 | // only look for LOD meshs sizes | ||
279 | |||
280 | if (map.ContainsKey("high_lod")) | ||
281 | { | ||
282 | tmpmap = (OSDMap)map["high_lod"]; | ||
283 | // see at least if there is a offset for this one | ||
284 | if (tmpmap.ContainsKey("offset")) | ||
285 | submesh_offset = tmpmap["offset"].AsInteger() + start; | ||
286 | if (tmpmap.ContainsKey("size")) | ||
287 | highlod_size = tmpmap["size"].AsInteger(); | ||
288 | } | ||
289 | |||
290 | if (submesh_offset < 0 || highlod_size <= 0) | ||
291 | { | ||
292 | error = "Unable to upload mesh model. missing high_lod"; | ||
293 | return false; | ||
294 | } | ||
295 | |||
296 | bool haveprev = true; | ||
297 | |||
298 | if (map.ContainsKey("medium_lod")) | ||
299 | { | ||
300 | tmpmap = (OSDMap)map["medium_lod"]; | ||
301 | if (tmpmap.ContainsKey("size")) | ||
302 | medlod_size = tmpmap["size"].AsInteger(); | ||
303 | else | ||
304 | haveprev = false; | ||
305 | } | ||
306 | |||
307 | if (haveprev && map.ContainsKey("low_lod")) | ||
308 | { | ||
309 | tmpmap = (OSDMap)map["low_lod"]; | ||
310 | if (tmpmap.ContainsKey("size")) | ||
311 | lowlod_size = tmpmap["size"].AsInteger(); | ||
312 | else | ||
313 | haveprev = false; | ||
314 | } | ||
315 | |||
316 | if (haveprev && map.ContainsKey("lowest_lod")) | ||
317 | { | ||
318 | tmpmap = (OSDMap)map["lowest_lod"]; | ||
319 | if (tmpmap.ContainsKey("size")) | ||
320 | lowestlod_size = tmpmap["size"].AsInteger(); | ||
321 | } | ||
322 | |||
323 | if (map.ContainsKey("skin")) | ||
324 | { | ||
325 | tmpmap = (OSDMap)map["skin"]; | ||
326 | if (tmpmap.ContainsKey("size")) | ||
327 | skin_size = tmpmap["size"].AsInteger(); | ||
328 | } | ||
329 | |||
330 | cost.highLODSize = highlod_size; | ||
331 | cost.medLODSize = medlod_size; | ||
332 | cost.lowLODSize = lowlod_size; | ||
333 | cost.lowestLODSize = lowestlod_size; | ||
334 | |||
335 | submesh_offset = -1; | ||
336 | |||
337 | if (map.ContainsKey("physics_mesh")) | ||
338 | { | ||
339 | tmpmap = (OSDMap)map["physics_mesh"]; | ||
340 | if (tmpmap.ContainsKey("offset")) | ||
341 | submesh_offset = tmpmap["offset"].AsInteger() + start; | ||
342 | if (tmpmap.ContainsKey("size")) | ||
343 | physmesh_size = tmpmap["size"].AsInteger(); | ||
344 | |||
345 | if (submesh_offset >= 0 || physmesh_size > 0) | ||
346 | { | ||
347 | |||
348 | if (!submesh(data, submesh_offset, physmesh_size, out phys_ntriangles)) | ||
349 | { | ||
350 | error = "Unable to upload mesh model. parsing error"; | ||
351 | return false; | ||
352 | } | ||
353 | } | ||
354 | } | ||
355 | |||
356 | // upload is done in convex shape type so only one hull | ||
357 | phys_hullsvertices++; | ||
358 | cost.physicsCost = 0.04f * phys_hullsvertices; | ||
359 | |||
360 | float sfee; | ||
361 | |||
362 | sfee = data.Length; // start with total compressed data size | ||
363 | |||
364 | // penalize lod meshs that should be more builder optimized | ||
365 | sfee += medSizeWth * medlod_size; | ||
366 | sfee += lowSizeWth * lowlod_size; | ||
367 | sfee += lowestSizeWth * lowlod_size; | ||
368 | |||
369 | // physics | ||
370 | // favor potencial optimized meshs versus automatic decomposition | ||
371 | if (physmesh_size != 0) | ||
372 | sfee += physMeshSizeWth * (physmesh_size + hulls_size / 4); // reduce cost of mandatory convex hull | ||
373 | else | ||
374 | sfee += physHullSizeWth * hulls_size; | ||
375 | |||
376 | // bytes to money | ||
377 | sfee *= bytecost; | ||
378 | |||
379 | // fee compression | ||
380 | if (sfee > feeCompressionBase) | ||
381 | { | ||
382 | sfee -= feeCompressionBase; | ||
383 | sfee = feeCompressionScale * (float)Math.Log10((double)sfee); | ||
384 | sfee += feeCompressionBase; | ||
385 | } | ||
386 | |||
387 | |||
388 | |||
389 | cost.costFee = sfee; | ||
390 | return true; | ||
391 | } | ||
392 | |||
393 | private bool submesh(byte[] data, int offset, int size, out int ntriangles) | ||
394 | { | ||
395 | ntriangles = 0; | ||
396 | |||
397 | OSD decodedMeshOsd = new OSD(); | ||
398 | byte[] meshBytes = new byte[size]; | ||
399 | System.Buffer.BlockCopy(data, offset, meshBytes, 0, size); | ||
400 | try | ||
401 | { | ||
402 | using (MemoryStream inMs = new MemoryStream(meshBytes)) | ||
403 | { | ||
404 | using (MemoryStream outMs = new MemoryStream()) | ||
405 | { | ||
406 | using (ZOutputStream zOut = new ZOutputStream(outMs)) | ||
407 | { | ||
408 | byte[] readBuffer = new byte[4096]; | ||
409 | int readLen = 0; | ||
410 | while ((readLen = inMs.Read(readBuffer, 0, readBuffer.Length)) > 0) | ||
411 | { | ||
412 | zOut.Write(readBuffer, 0, readLen); | ||
413 | } | ||
414 | zOut.Flush(); | ||
415 | outMs.Seek(0, SeekOrigin.Begin); | ||
416 | |||
417 | byte[] decompressedBuf = outMs.GetBuffer(); | ||
418 | decodedMeshOsd = OSDParser.DeserializeLLSDBinary(decompressedBuf); | ||
419 | } | ||
420 | } | ||
421 | } | ||
422 | } | ||
423 | catch (Exception e) | ||
424 | { | ||
425 | return false; | ||
426 | } | ||
427 | |||
428 | OSDArray decodedMeshOsdArray = null; | ||
429 | if ((!decodedMeshOsd is OSDArray)) | ||
430 | return false; | ||
431 | |||
432 | byte[] dummy; | ||
433 | |||
434 | decodedMeshOsdArray = (OSDArray)decodedMeshOsd; | ||
435 | foreach (OSD subMeshOsd in decodedMeshOsdArray) | ||
436 | { | ||
437 | if (subMeshOsd is OSDMap) | ||
438 | { | ||
439 | OSDMap subtmpmap = (OSDMap)subMeshOsd; | ||
440 | if (subtmpmap.ContainsKey("NoGeometry") && ((OSDBoolean)subtmpmap["NoGeometry"])) | ||
441 | continue; | ||
442 | |||
443 | if (!subtmpmap.ContainsKey("Position")) | ||
444 | return false; | ||
445 | |||
446 | if (subtmpmap.ContainsKey("TriangleList")) | ||
447 | { | ||
448 | dummy = subtmpmap["TriangleList"].AsBinary(); | ||
449 | ntriangles += dummy.Length / bytesPerCoord; | ||
450 | } | ||
451 | else | ||
452 | return false; | ||
453 | } | ||
454 | } | ||
455 | |||
456 | return true; | ||
457 | } | ||
458 | |||
459 | private bool hulls(byte[] data, int offset, int size, out int nvertices, out int nhulls) | ||
460 | { | ||
461 | nvertices = 0; | ||
462 | nhulls = 1; | ||
463 | |||
464 | OSD decodedMeshOsd = new OSD(); | ||
465 | byte[] meshBytes = new byte[size]; | ||
466 | System.Buffer.BlockCopy(data, offset, meshBytes, 0, size); | ||
467 | try | ||
468 | { | ||
469 | using (MemoryStream inMs = new MemoryStream(meshBytes)) | ||
470 | { | ||
471 | using (MemoryStream outMs = new MemoryStream()) | ||
472 | { | ||
473 | using (ZOutputStream zOut = new ZOutputStream(outMs)) | ||
474 | { | ||
475 | byte[] readBuffer = new byte[4096]; | ||
476 | int readLen = 0; | ||
477 | while ((readLen = inMs.Read(readBuffer, 0, readBuffer.Length)) > 0) | ||
478 | { | ||
479 | zOut.Write(readBuffer, 0, readLen); | ||
480 | } | ||
481 | zOut.Flush(); | ||
482 | outMs.Seek(0, SeekOrigin.Begin); | ||
483 | |||
484 | byte[] decompressedBuf = outMs.GetBuffer(); | ||
485 | decodedMeshOsd = OSDParser.DeserializeLLSDBinary(decompressedBuf); | ||
486 | } | ||
487 | } | ||
488 | } | ||
489 | } | ||
490 | catch (Exception e) | ||
491 | { | ||
492 | return false; | ||
493 | } | ||
494 | |||
495 | OSDMap cmap = (OSDMap)decodedMeshOsd; | ||
496 | if (cmap == null) | ||
497 | return false; | ||
498 | |||
499 | byte[] dummy; | ||
500 | |||
501 | // must have one of this | ||
502 | if (cmap.ContainsKey("BoundingVerts")) | ||
503 | { | ||
504 | dummy = cmap["BoundingVerts"].AsBinary(); | ||
505 | nvertices = dummy.Length / bytesPerCoord; | ||
506 | } | ||
507 | else | ||
508 | return false; | ||
509 | |||
510 | /* upload is done with convex shape type | ||
511 | if (cmap.ContainsKey("HullList")) | ||
512 | { | ||
513 | dummy = cmap["HullList"].AsBinary(); | ||
514 | nhulls += dummy.Length; | ||
515 | } | ||
516 | |||
517 | |||
518 | if (cmap.ContainsKey("Positions")) | ||
519 | { | ||
520 | dummy = cmap["Positions"].AsBinary(); | ||
521 | nvertices = dummy.Length / bytesPerCoord; | ||
522 | } | ||
523 | */ | ||
524 | |||
525 | return true; | ||
526 | } | ||
527 | |||
528 | private float streamingCost(ameshCostParam curCost, float sqdiam) | ||
529 | { | ||
530 | // compute efective areas | ||
531 | float ma = 262144f; | ||
532 | |||
533 | float mh = sqdiam * highLodFactor; | ||
534 | if (mh > ma) | ||
535 | mh = ma; | ||
536 | float mm = sqdiam * midLodFactor; | ||
537 | if (mm > ma) | ||
538 | mm = ma; | ||
539 | |||
540 | float ml = sqdiam * lowLodFactor; | ||
541 | if (ml > ma) | ||
542 | ml = ma; | ||
543 | |||
544 | float mlst = ma; | ||
545 | |||
546 | mlst -= ml; | ||
547 | ml -= mm; | ||
548 | mm -= mh; | ||
549 | |||
550 | if (mlst < 1.0f) | ||
551 | mlst = 1.0f; | ||
552 | if (ml < 1.0f) | ||
553 | ml = 1.0f; | ||
554 | if (mm < 1.0f) | ||
555 | mm = 1.0f; | ||
556 | if (mh < 1.0f) | ||
557 | mh = 1.0f; | ||
558 | |||
559 | ma = mlst + ml + mm + mh; | ||
560 | |||
561 | // get LODs compressed sizes | ||
562 | // giving 384 bytes bonus | ||
563 | int lst = curCost.lowestLODSize - 384; | ||
564 | int l = curCost.lowLODSize - 384; | ||
565 | int m = curCost.medLODSize - 384; | ||
566 | int h = curCost.highLODSize - 384; | ||
567 | |||
568 | // use previus higher LOD size on missing ones | ||
569 | if (m <= 0) | ||
570 | m = h; | ||
571 | if (l <= 0) | ||
572 | l = m; | ||
573 | if (lst <= 0) | ||
574 | lst = l; | ||
575 | |||
576 | // force minumum sizes | ||
577 | if (lst < 16) | ||
578 | lst = 16; | ||
579 | if (l < 16) | ||
580 | l = 16; | ||
581 | if (m < 16) | ||
582 | m = 16; | ||
583 | if (h < 16) | ||
584 | h = 16; | ||
585 | |||
586 | // compute cost weighted by relative effective areas | ||
587 | |||
588 | float cost = (float)lst * mlst + (float)l * ml + (float)m * mm + (float)h * mh; | ||
589 | cost /= ma; | ||
590 | |||
591 | cost *= 0.004f; // overall tunning parameter | ||
592 | |||
593 | return cost; | ||
594 | } | ||
595 | } | ||
596 | } | ||