public static IEnumerable <VFXPropertyWithValue> GetInputProperties(uint meshCount, VFXOutputUpdate.Features features)
        {
            for (int i = 0; i < meshCount; ++i)
            {
                string id = GetId(meshCount, i);

                yield return(new VFXPropertyWithValue(new VFXProperty(typeof(Mesh), meshName + id, new TooltipAttribute("Specifies the mesh" + id + " used to render the particle.")), VFXResources.defaultResources.mesh));

                yield return(new VFXPropertyWithValue(new VFXProperty(typeof(uint), maskName + id, new TooltipAttribute("Defines a bitmask to control which submeshes are rendered for mesh" + id + "."), new BitFieldAttribute()), 0xffffffff));
            }

            if (VFXOutputUpdate.HasFeature(features, VFXOutputUpdate.Features.LOD))
            {
                yield return(new VFXPropertyWithValue(new VFXProperty(typeof(Vector4), lodName, new TooltipAttribute("Specifies the minimum screen ratio for a LOD mesh to be used (e.g. a value of 25 means the mesh has to occupy 25% of the screen on 1 dimension).")), lodFactors));
            }

            if (VFXOutputUpdate.HasFeature(features, VFXOutputUpdate.Features.LOD) ||
                VFXOutputUpdate.HasFeature(features, VFXOutputUpdate.Features.FrustumCulling))
            {
                yield return(new VFXPropertyWithValue(new VFXProperty(typeof(float), "radiusScale", new MinAttribute(0.0f), new TooltipAttribute("Specifies a scale to apply to the radius of the bounding sphere used for LOD and frustum culling. By default the bounding sphere is encompassing a mesh bounding box of side 1.")), 1.0f));
            }
        }
Example #2
0
        public override void FillDescs(
            VFXCompileErrorReporter reporter,
            List <VFXGPUBufferDesc> outBufferDescs,
            List <VFXTemporaryGPUBufferDesc> outTemporaryBufferDescs,
            List <VFXEditorSystemDesc> outSystemDescs,
            VFXExpressionGraph expressionGraph,
            Dictionary <VFXContext, VFXContextCompiledData> contextToCompiledData,
            Dictionary <VFXContext, int> contextSpawnToBufferIndex,
            VFXDependentBuffersData dependentBuffers,
            Dictionary <VFXContext, List <VFXContextLink>[]> effectiveFlowInputLinks,
            VFXSystemNames systemNames = null)
        {
            bool hasKill = IsAttributeStored(VFXAttribute.Alive);

            var deadListBufferIndex = -1;
            var deadListCountIndex  = -1;

            var systemBufferMappings = new List <VFXMapping>();
            var systemValueMappings  = new List <VFXMapping>();

            var attributeBufferIndex = dependentBuffers.attributeBuffers[this];

            int attributeSourceBufferIndex = -1;
            int eventGPUFrom = -1;

            var stripDataIndex    = -1;
            var boundsBufferIndex = -1;

            if (m_DependenciesIn.Any())
            {
                if (m_DependenciesIn.Count != 1)
                {
                    throw new InvalidOperationException("Unexpected multiple input dependency for GPU event");
                }
                attributeSourceBufferIndex = dependentBuffers.attributeBuffers[m_DependenciesIn.FirstOrDefault()];
                eventGPUFrom = dependentBuffers.eventBuffers[this];
            }

            if (attributeBufferIndex != -1)
            {
                systemBufferMappings.Add(new VFXMapping("attributeBuffer", attributeBufferIndex));
            }

            if (m_ownAttributeSourceBuffer)
            {
                if (attributeSourceBufferIndex != -1)
                {
                    throw new InvalidOperationException("Unexpected source while filling description of data particle");
                }

                attributeSourceBufferIndex = outBufferDescs.Count;
                outBufferDescs.Add(m_layoutAttributeSource.GetBufferDesc(staticSourceCount));
            }

            if (attributeSourceBufferIndex != -1)
            {
                systemBufferMappings.Add(new VFXMapping("sourceAttributeBuffer", attributeSourceBufferIndex));
            }

            var systemFlag = VFXSystemFlag.SystemDefault;

            if (eventGPUFrom != -1)
            {
                systemFlag |= VFXSystemFlag.SystemReceivedEventGPU;
                systemBufferMappings.Add(new VFXMapping("eventList", eventGPUFrom));
            }

            if (hasKill)
            {
                systemFlag |= VFXSystemFlag.SystemHasKill;

                deadListBufferIndex = outBufferDescs.Count;
                outBufferDescs.Add(new VFXGPUBufferDesc()
                {
                    type = ComputeBufferType.Counter, size = capacity, stride = 4
                });
                systemBufferMappings.Add(new VFXMapping("deadList", deadListBufferIndex));

                deadListCountIndex = outBufferDescs.Count;
                outBufferDescs.Add(new VFXGPUBufferDesc()
                {
                    type = ComputeBufferType.Raw, size = 1, stride = 4
                });
                systemBufferMappings.Add(new VFXMapping("deadListCount", deadListCountIndex));
            }

            if (hasStrip)
            {
                systemFlag |= VFXSystemFlag.SystemHasStrips;

                systemValueMappings.Add(new VFXMapping("stripCount", (int)stripCapacity));
                systemValueMappings.Add(new VFXMapping("particlePerStripCount", (int)particlePerStripCount));

                stripDataIndex = dependentBuffers.stripBuffers[this];
                systemBufferMappings.Add(new VFXMapping("stripDataBuffer", stripDataIndex));
            }

            if (hasDynamicSourceCount)
            {
                systemFlag |= VFXSystemFlag.SystemHasDirectLink;
            }

            if (needsComputeBounds || boundsSettingMode == BoundsSettingMode.Automatic)
            {
                systemFlag |= VFXSystemFlag.SystemNeedsComputeBounds;

                boundsBufferIndex = dependentBuffers.boundsBuffers[this];
                systemBufferMappings.Add(new VFXMapping("boundsBuffer", boundsBufferIndex));
            }

            if (boundsSettingMode == BoundsSettingMode.Automatic)
            {
                systemFlag |= VFXSystemFlag.SystemAutomaticBounds;
            }

            if (space == VFXCoordinateSpace.World)
            {
                systemFlag |= VFXSystemFlag.SystemInWorldSpace;
            }

            var initContext = m_Contexts.FirstOrDefault(o => o.contextType == VFXContextType.Init);

            if (initContext != null)
            {
                systemBufferMappings.AddRange(effectiveFlowInputLinks[initContext].SelectMany(t => t.Select(u => u.context)).Where(o => o.contextType == VFXContextType.Spawner).Select(o => new VFXMapping("spawner_input", contextSpawnToBufferIndex[o])));
            }
            if (m_Contexts.Count() > 0 && m_Contexts.First().contextType == VFXContextType.Init) // TODO This test can be removed once we ensure priorly the system is valid
            {
                var mapper = contextToCompiledData[m_Contexts.First()].cpuMapper;

                var boundsCenterExp  = mapper.FromNameAndId("bounds_center", -1);
                var boundsSizeExp    = mapper.FromNameAndId("bounds_size", -1);
                var boundsPaddingExp = mapper.FromNameAndId("boundsPadding", -1);

                int boundsCenterIndex = boundsCenterExp != null?expressionGraph.GetFlattenedIndex(boundsCenterExp) : -1;

                int boundsSizeIndex = boundsSizeExp != null?expressionGraph.GetFlattenedIndex(boundsSizeExp) : -1;

                int boundsPaddingIndex = boundsPaddingExp != null?expressionGraph.GetFlattenedIndex(boundsPaddingExp) : -1;

                if (boundsCenterIndex != -1 && boundsSizeIndex != -1)
                {
                    systemValueMappings.Add(new VFXMapping("bounds_center", boundsCenterIndex));
                    systemValueMappings.Add(new VFXMapping("bounds_size", boundsSizeIndex));
                }
                if (boundsPaddingIndex != -1)
                {
                    systemValueMappings.Add(new VFXMapping("boundsPadding", boundsPaddingIndex));
                }
            }

            Dictionary <VFXContext, VFXOutputUpdate> indirectOutputToCuller = null;
            bool needsIndirectBuffer       = NeedsIndirectBuffer();
            int  globalIndirectBufferIndex = -1;
            bool needsGlobalIndirectBuffer = false;

            if (needsIndirectBuffer)
            {
                indirectOutputToCuller = new Dictionary <VFXContext, VFXOutputUpdate>();
                foreach (var cullCompute in m_Contexts.OfType <VFXOutputUpdate>())
                {
                    if (cullCompute.HasFeature(VFXOutputUpdate.Features.IndirectDraw))
                    {
                        indirectOutputToCuller.Add(cullCompute.output, cullCompute);
                    }
                }

                var allIndirectOutputs = owners.OfType <VFXAbstractParticleOutput>().Where(o => o.HasIndirectDraw());

                needsGlobalIndirectBuffer = NeedsGlobalIndirectBuffer();
                if (needsGlobalIndirectBuffer)
                {
                    globalIndirectBufferIndex = outBufferDescs.Count;
                    systemBufferMappings.Add(new VFXMapping("indirectBuffer0", outBufferDescs.Count));
                    outBufferDescs.Add(new VFXGPUBufferDesc()
                    {
                        type = ComputeBufferType.Counter, size = capacity, stride = 4
                    });
                }

                int currentIndirectBufferIndex = globalIndirectBufferIndex == -1 ? 0 : 1;
                foreach (var indirectOutput in allIndirectOutputs)
                {
                    if (indirectOutputToCuller.ContainsKey(indirectOutput))
                    {
                        VFXOutputUpdate culler      = indirectOutputToCuller[indirectOutput];
                        uint            bufferCount = culler.bufferCount;
                        culler.bufferIndex = outBufferDescs.Count;
                        bool perCamera    = culler.isPerCamera;
                        uint bufferStride = culler.HasFeature(VFXOutputUpdate.Features.Sort) ? 8u : 4u;
                        for (uint i = 0; i < bufferCount; ++i)
                        {
                            string bufferName = "indirectBuffer" + currentIndirectBufferIndex++;
                            if (perCamera)
                            {
                                bufferName += "PerCamera";
                            }
                            systemBufferMappings.Add(new VFXMapping(bufferName, outBufferDescs.Count));
                            outBufferDescs.Add(new VFXGPUBufferDesc()
                            {
                                type = ComputeBufferType.Counter, size = capacity, stride = bufferStride
                            });
                        }

                        if (culler.HasFeature(VFXOutputUpdate.Features.Sort))
                        {
                            culler.sortedBufferIndex = outBufferDescs.Count;
                            for (uint i = 0; i < bufferCount; ++i)
                            {
                                outBufferDescs.Add(new VFXGPUBufferDesc()
                                {
                                    type = ComputeBufferType.Default, size = capacity, stride = 4
                                });
                            }
                        }
                        else
                        {
                            culler.sortedBufferIndex = culler.bufferIndex;
                        }
                    }
                }
            }

            // sort buffers
            int  sortBufferAIndex = -1;
            int  sortBufferBIndex = -1;
            bool needsSort        = NeedsGlobalSort();

            if (needsSort)
            {
                sortBufferAIndex = outBufferDescs.Count;
                sortBufferBIndex = sortBufferAIndex + 1;

                outBufferDescs.Add(new VFXGPUBufferDesc()
                {
                    type = ComputeBufferType.Default, size = capacity, stride = 8
                });
                systemBufferMappings.Add(new VFXMapping("sortBufferA", sortBufferAIndex));

                outBufferDescs.Add(new VFXGPUBufferDesc()
                {
                    type = ComputeBufferType.Default, size = capacity, stride = 8
                });
                systemBufferMappings.Add(new VFXMapping("sortBufferB", sortBufferBIndex));
            }

            var elementToVFXBufferMotionVector = new Dictionary <VFXContext, int>();

            foreach (VFXOutputUpdate context in m_Contexts.OfType <VFXOutputUpdate>())
            {
                if (context.HasFeature(VFXOutputUpdate.Features.MotionVector))
                {
                    uint sizePerElement = 12U * 4U;
                    if (context.output.SupportsMotionVectorPerVertex(out uint vertsCount))
                    {
                        // 2 floats per vertex
                        sizePerElement = vertsCount * 2U * 4U;
                    }
                    // add previous frame index
                    sizePerElement += 4U;
                    int currentElementToVFXBufferMotionVector = outTemporaryBufferDescs.Count;
                    outTemporaryBufferDescs.Add(new VFXTemporaryGPUBufferDesc()
                    {
                        frameCount = 2u, desc = new VFXGPUBufferDesc {
                            type = ComputeBufferType.Raw, size = capacity * sizePerElement, stride = 4
                        }
                    });
                    elementToVFXBufferMotionVector.Add(context.output, currentElementToVFXBufferMotionVector);
                }
            }

            var taskDescs            = new List <VFXEditorTaskDesc>();
            var bufferMappings       = new List <VFXMapping>();
            var uniformMappings      = new List <VFXMapping>();
            var additionalParameters = new List <VFXMapping>();

            for (int i = 0; i < m_Contexts.Count; ++i)
            {
                var temporaryBufferMappings = new List <VFXMappingTemporary>();

                var context = m_Contexts[i];
                if (!contextToCompiledData.TryGetValue(context, out var contextData))
                {
                    throw new InvalidOperationException("Unexpected context which hasn't been compiled : " + context);
                }

                var taskDesc = new VFXEditorTaskDesc();
                taskDesc.type = (UnityEngine.VFX.VFXTaskType)context.taskType;

                bufferMappings.Clear();
                additionalParameters.Clear();

                if (context is VFXOutputUpdate)
                {
                    var update = (VFXOutputUpdate)context;
                    if (update.HasFeature(VFXOutputUpdate.Features.MotionVector))
                    {
                        var currentIndex = elementToVFXBufferMotionVector[update.output];
                        temporaryBufferMappings.Add(new VFXMappingTemporary()
                        {
                            pastFrameIndex = 0u, perCameraBuffer = true, mapping = new VFXMapping("elementToVFXBuffer", currentIndex)
                        });
                    }
                }
                else if (context.contextType == VFXContextType.Output && (context is IVFXSubRenderer) && (context as IVFXSubRenderer).hasMotionVector)
                {
                    var currentIndex = elementToVFXBufferMotionVector[context];
                    temporaryBufferMappings.Add(new VFXMappingTemporary()
                    {
                        pastFrameIndex = 1u, perCameraBuffer = true, mapping = new VFXMapping("elementToVFXBufferPrevious", currentIndex)
                    });
                }

                if (attributeBufferIndex != -1)
                {
                    bufferMappings.Add(new VFXMapping("attributeBuffer", attributeBufferIndex));
                }

                if (eventGPUFrom != -1 && context.contextType == VFXContextType.Init)
                {
                    bufferMappings.Add(new VFXMapping("eventList", eventGPUFrom));
                }

                if (deadListBufferIndex != -1 && (context.taskType == VFXTaskType.Initialize || context.taskType == VFXTaskType.Update))
                {
                    bufferMappings.Add(new VFXMapping(context.contextType == VFXContextType.Update ? "deadListOut" : "deadListIn", deadListBufferIndex));
                }

                if (deadListCountIndex != -1 && context.contextType == VFXContextType.Init)
                {
                    bufferMappings.Add(new VFXMapping("deadListCount", deadListCountIndex));
                }

                if (attributeSourceBufferIndex != -1 && context.contextType == VFXContextType.Init)
                {
                    bufferMappings.Add(new VFXMapping("sourceAttributeBuffer", attributeSourceBufferIndex));
                }

                if (stripDataIndex != -1 && context.ownedType == VFXDataType.ParticleStrip)
                {
                    bufferMappings.Add(new VFXMapping("stripDataBuffer", stripDataIndex));
                }

                bool hasAttachedStrip = IsAttributeStored(VFXAttribute.StripAlive);
                if (hasAttachedStrip)
                {
                    var stripData = dependenciesOut.First(d => ((VFXDataParticle)d).hasStrip); // TODO Handle several strip attached
                    bufferMappings.Add(new VFXMapping("attachedStripDataBuffer", dependentBuffers.stripBuffers[stripData]));
                }

                if (needsIndirectBuffer)
                {
                    systemFlag |= VFXSystemFlag.SystemHasIndirectBuffer;

                    if (context.contextType == VFXContextType.Output && (context as VFXAbstractParticleOutput).HasIndirectDraw())
                    {
                        bool hasCuller = indirectOutputToCuller.ContainsKey(context);
                        additionalParameters.Add(new VFXMapping("indirectIndex", hasCuller ? indirectOutputToCuller[context].bufferIndex : globalIndirectBufferIndex));
                        bufferMappings.Add(new VFXMapping("indirectBuffer", hasCuller ? indirectOutputToCuller[context].sortedBufferIndex : globalIndirectBufferIndex));
                    }

                    if (context.contextType == VFXContextType.Update)
                    {
                        if (context.taskType == VFXTaskType.Update && needsGlobalIndirectBuffer)
                        {
                            bufferMappings.Add(new VFXMapping("indirectBuffer", globalIndirectBufferIndex));
                        }
                    }

                    if (context.contextType == VFXContextType.Filter)
                    {
                        if (context.taskType == VFXTaskType.CameraSort && needsGlobalIndirectBuffer)
                        {
                            bufferMappings.Add(new VFXMapping("inputBuffer", globalIndirectBufferIndex));
                        }
                        else if (context is VFXOutputUpdate)
                        {
                            var  outputUpdate = (VFXOutputUpdate)context;
                            int  startIndex   = outputUpdate.bufferIndex;
                            uint bufferCount  = outputUpdate.bufferCount;
                            for (int j = 0; j < bufferCount; ++j)
                            {
                                bufferMappings.Add(new VFXMapping("outputBuffer" + j, startIndex + j));
                            }
                        }
                    }
                }

                if (deadListBufferIndex != -1 && context.contextType == VFXContextType.Output && (context as VFXAbstractParticleOutput).NeedsDeadListCount())
                {
                    bufferMappings.Add(new VFXMapping("deadListCount", deadListCountIndex));
                }

                if (context.taskType == VFXTaskType.CameraSort)
                {
                    bufferMappings.Add(new VFXMapping("outputBuffer", sortBufferAIndex));
                    if (deadListCountIndex != -1)
                    {
                        bufferMappings.Add(new VFXMapping("deadListCount", deadListCountIndex));
                    }
                }

                var gpuTarget = context.allLinkedOutputSlot.SelectMany(o => (o.owner as VFXContext).outputContexts)
                                .Where(c => c.CanBeCompiled())
                                .Select(o => dependentBuffers.eventBuffers[o.GetData()])
                                .ToArray();
                for (uint indexTarget = 0; indexTarget < (uint)gpuTarget.Length; ++indexTarget)
                {
                    var prefix = VFXCodeGeneratorHelper.GeneratePrefix(indexTarget);
                    bufferMappings.Add(new VFXMapping(string.Format("eventListOut_{0}", prefix), gpuTarget[indexTarget]));
                }

                uniformMappings.Clear();

                foreach (var uniform in contextData.uniformMapper.uniforms)
                {
                    uniformMappings.Add(new VFXMapping(contextData.uniformMapper.GetName(uniform), expressionGraph.GetFlattenedIndex(uniform)));
                }
                foreach (var buffer in contextData.uniformMapper.buffers)
                {
                    uniformMappings.Add(new VFXMapping(contextData.uniformMapper.GetName(buffer), expressionGraph.GetFlattenedIndex(buffer)));
                }
                foreach (var texture in contextData.uniformMapper.textures)
                {
                    // TODO At the moment issue all names sharing the same texture as different texture slots. This is not optimized as it required more texture binding than necessary
                    foreach (var name in contextData.uniformMapper.GetNames(texture))
                    {
                        uniformMappings.Add(new VFXMapping(name, expressionGraph.GetFlattenedIndex(texture)));
                    }
                }

                // Retrieve all cpu mappings at context level (-1)
                var cpuMappings = contextData.cpuMapper.CollectExpression(-1).Select(exp => new VFXMapping(exp.name, expressionGraph.GetFlattenedIndex(exp.exp))).ToArray();

                //Check potential issue with invalid operation on CPU
                foreach (var mapping in cpuMappings)
                {
                    if (mapping.index < 0)
                    {
                        reporter?.RegisterError(context.GetSlotByPath(true, mapping.name), "GPUNodeLinkedTOCPUSlot", VFXErrorType.Error, "Can not link a GPU operator to a system wide (CPU) input.");;
                        throw new InvalidOperationException("Unable to compute CPU expression for mapping : " + mapping.name);
                    }
                }

                taskDesc.buffers           = bufferMappings.ToArray();
                taskDesc.temporaryBuffers  = temporaryBufferMappings.ToArray();
                taskDesc.values            = uniformMappings.ToArray();
                taskDesc.parameters        = cpuMappings.Concat(contextData.parameters).Concat(additionalParameters).ToArray();
                taskDesc.shaderSourceIndex = contextToCompiledData[context].indexInShaderSource;
                taskDesc.model             = context;

                if (context is IVFXMultiMeshOutput) // If the context is a multi mesh output, split and patch task desc into several tasks
                {
                    var multiMeshOutput = (IVFXMultiMeshOutput)context;
                    for (int j = (int)multiMeshOutput.meshCount - 1; j >= 0; --j) // Back to front to be consistent with LOD and alpha
                    {
                        VFXEditorTaskDesc singleMeshTaskDesc = taskDesc;
                        singleMeshTaskDesc.parameters = VFXMultiMeshHelper.PatchCPUMapping(taskDesc.parameters, multiMeshOutput.meshCount, j).ToArray();
                        singleMeshTaskDesc.buffers    = VFXMultiMeshHelper.PatchBufferMapping(taskDesc.buffers, j).ToArray();
                        taskDescs.Add(singleMeshTaskDesc);
                    }
                }
                else
                {
                    taskDescs.Add(taskDesc);
                }

                // if task is a per camera update with sorting, add sort tasks
                if (context is VFXOutputUpdate)
                {
                    var update = (VFXOutputUpdate)context;

                    if (update.HasFeature(VFXOutputUpdate.Features.Sort))
                    {
                        for (int j = 0; j < update.bufferCount; ++j)
                        {
                            VFXEditorTaskDesc sortTaskDesc = new VFXEditorTaskDesc();
                            sortTaskDesc.type = UnityEngine.VFX.VFXTaskType.PerCameraSort;
                            sortTaskDesc.externalProcessor = null;
                            sortTaskDesc.model             = context;

                            sortTaskDesc.buffers    = new VFXMapping[3];
                            sortTaskDesc.buffers[0] = new VFXMapping("srcBuffer", update.bufferIndex + j);
                            if (capacity > 4096) // Add scratch buffer
                            {
                                sortTaskDesc.buffers[1] = new VFXMapping("scratchBuffer", outBufferDescs.Count);
                                outBufferDescs.Add(new VFXGPUBufferDesc()
                                {
                                    type = ComputeBufferType.Default, size = capacity, stride = 8
                                });
                            }
                            else
                            {
                                sortTaskDesc.buffers[1] = new VFXMapping("scratchBuffer", -1); // No scratchBuffer needed
                            }
                            sortTaskDesc.buffers[2] = new VFXMapping("dstBuffer", update.sortedBufferIndex + j);

                            sortTaskDesc.parameters    = new VFXMapping[1];
                            sortTaskDesc.parameters[0] = new VFXMapping("globalSort", 0);

                            taskDescs.Add(sortTaskDesc);
                        }
                    }
                }
            }

            string nativeName = string.Empty;

            if (systemNames != null)
            {
                nativeName = systemNames.GetUniqueSystemName(this);
            }
            else
            {
                throw new InvalidOperationException("system names manager cannot be null");
            }

            outSystemDescs.Add(new VFXEditorSystemDesc()
            {
                flags    = systemFlag,
                tasks    = taskDescs.ToArray(),
                capacity = capacity,
                name     = nativeName,
                buffers  = systemBufferMappings.ToArray(),
                values   = systemValueMappings.ToArray(),
                type     = VFXSystemType.Particle,
                layer    = m_Layer
            });
        }
Example #3
0
 public bool NeedsGlobalSort()
 {
     return(compilableOwners.OfType <VFXAbstractParticleOutput>().Any(o => o.CanBeCompiled() && o.HasSorting() && !VFXOutputUpdate.HasFeature(o.outputUpdateFeatures, VFXOutputUpdate.Features.IndirectDraw)));
 }
Example #4
0
 public bool NeedsGlobalIndirectBuffer()
 {
     return(compilableOwners.OfType <VFXAbstractParticleOutput>().Any(o => o.HasIndirectDraw() && !VFXOutputUpdate.HasFeature(o.outputUpdateFeatures, VFXOutputUpdate.Features.IndirectDraw)));
 }
Example #5
0
 public bool HasIndirectDraw()
 {
     return((indirectDraw || HasSorting() || VFXOutputUpdate.HasFeature(outputUpdateFeatures, VFXOutputUpdate.Features.IndirectDraw)) && !HasStrips(true));
 }