Arquitectura del Sistema
La solución se estructura en cinco capas fundamentales que gestionan el ciclo de vida del rascado:
- Capa de Interacción:
InputHandler.cs- Gestiona los eventos de toque y ratón. - Capa de Datos:
CoordinateMapper.csyBarycentricCalculator.cs- Convierte coordenadas de pantalla a coordenadas UV precisas. - Capa de Renderizado:
MaskDrawer.cs- Dibuja las marcas de borrado en unRenderTexture. - Capa de Visualización:
ScratchCardManager.csy el Shader - Utiliza la textura dinámica como máscara para revelar el contenido oculto. - Capa de Utilidades:
ProgressTracker.cs- Calcula el porcentaje de área descubierta.
Fundamentos del Renderizado
El núcleo del plugin no altera las coordenadas UV originales del Sprite. En su lugar, transforma las coordenadas de pantalla a coordenadas locales y finalmente a UV exactas. Las marcas de rascado se dibujan directamente sobre un RenderTexture, el cual actúa como una máscara dinámica controlada por un Shader para gestionar la visibilidad de la capa superior.
Análisis de los Componentes Principales
Gestor Principle (ScratchCardManager)
Este script actúa como el cerebro del sistema, inicializando la textura dinámica y delegando las interacciones.
private void InitializeMaskTexture()
{
dynamicMask = new RenderTexture(maskWidth, maskHeight, depth, renderFormat);
overlayMaterial.SetTexture("_EraseMask", dynamicMask);
}
private void ProcessInteraction(Vector2 screenPos, float intensity)
{
textureDrawer.DrawEraseMark(screenPos, intensity);
}
Dibujador de Máscara (MaskDrawer)
Utiliza un CommandBuffer para renderizar cuadriláteros (las marcas del rascado) directamente en la textura dinámica.
public void DrawEraseMark(Vector2 inputPos, float intensity = 1f)
{
Rect drawArea = CalculateDrawArea(inputPos, intensity);
Vector3[] quadVertices = {
new Vector3(drawArea.xMin, drawArea.yMax, 0f),
new Vector3(drawArea.xMax, drawArea.yMax, 0f),
new Vector3(drawArea.xMax, drawArea.yMin, 0f),
new Vector3(drawArea.xMin, drawArea.yMin, 0f)
};
brushMesh.vertices = quadVertices;
drawCommand.SetRenderTarget(targetMask);
drawCommand.DrawMesh(brushMesh, Matrix4x4.identity, brushShader);
Graphics.ExecuteCommandBuffer(drawCommand);
}
Manejador de Entradas (InputHandler)
Procesa las entradas táctiles o de ratón, filtrando las coordenadsa y emitiendo eventos en el espacio de la textura.
private void EvaluateTouches()
{
for (int touchIndex = 0; touchIndex < activeTouches.Length; touchIndex++)
{
if (activeTouches[touchIndex])
{
InteractionTriggered?.Invoke(touchData[touchIndex].Coordinate, touchData[touchIndex].Force);
}
}
}
Mapeo de Coordenadas y Cálculo Baricéntrico
El algoritmo central para convertir posiciones de pantala a UV utiliza raycasting y coordenadas baricéntricas, permitiendo que el rascado funcione en Sprites de formas arbitrarias.
public virtual Vector2 CalculateTextureCoordinate(Vector2 screenPoint)
{
Ray interactionRay = mainCamera.ScreenPointToRay(screenPoint);
if (surfacePlane.Raycast(interactionRay, out float hitDistance))
{
Vector3 worldHit = interactionRay.GetPoint(hitDistance);
Vector3 localHit = transform.InverseTransformPoint(worldHit);
Vector2 calculatedUV = BarycentricCalculator.ComputeBarycentricUV(localHit);
return Vector2.Scale(textureDimensions, calculatedUV);
}
return Vector2.zero;
}
El cálculo baricéntrico asegura que la UV sea precisa independientemente de la geometría del mesh:
public Vector2 ComputeBarycentricUV(Vector3 targetPoint)
{
Vector3 edge1 = vertexB - vertexA;
Vector3 edge2 = vertexC - vertexA;
Vector3 normal = Vector3.Cross(edge1, edge2);
float totalArea = normal.magnitude;
Vector3 subEdge1 = vertexC - vertexB;
Vector3 subEdge2 = targetPoint - vertexB;
float weightA = Vector3.Cross(subEdge1, subEdge2).magnitude / totalArea * orientationSign;
// Los pesos B y C se calculan de manera análoga
return uvA * weightA + uvB * weightB + uvC * weightC;
}
Rastreador de Progreso (ProgressTracker)
Lee los píxeles de la RenderTexture de forma asíncrona para determinar el porcentaje de área revelada.
private IEnumerator EvaluateErasePercentage()
{
AsyncGPUReadbackRequest readRequest = AsyncGPUReadback.RequestIntoNativeArray(ref pixelDataArray, dynamicMask);
yield return new WaitUntil(() => readRequest.done);
float erasedAmount = 0f;
int totalPixels = pixelDataArray.Length / bytesPerPixel;
for (int i = 0; i < pixelDataArray.Length; i += bytesPerPixel)
{
erasedAmount += pixelDataArray[i] / 255f;
}
currentProgress = erasedAmount / totalPixels;
}
Manejo de Formas Irregulares y Recortes
Una de las ventajas clave de esta arquitectura es su capacidad para manejar Sprites con formas complejas (como recortes de papel, corazones o círculos) sin que el rascado se desborde.
Esto es posible porque el sistema no depende de colisionadores rectangulares. En su lugar:
- El mapeo UV basado en coordenadas baricéntricas restringe las coordenadas válidas exclusivamente al área definida por la textura.
- El Shader multiplica el canal alfa de la máscara dinámica con el canal alfa original del Sprite, garantizando que las áreas transparentes originales no se vean afectadas.
Flujo de Ejecución del Algoritmo de Borrado
La interacción completa sigue una cadena de procesamiento estricta para garantizar precisión y rendimiento.
Fase 1: Captura de Entradas
Se interceptan los eventos táctiles y se realiza un raycast para descartar toques fuera del área visual del Sprite.
private void ProcessRawInput(int fingerId, Vector2 position, float pressure = 1f)
{
if (enableRaycastFiltering && raycastController.IsOutOfBounds(position))
return;
Vector2 textureSpacePos = ConvertToTextureSpace(position);
InteractionTriggered?.Invoke(textureSpacePos, pressure);
}
Fase 2: Mapeo de Coordenadas a UV
Como se detalló anteriormente, las coordenadas de pantalla se transforman a coordenadas locales y luego a UV mediante el algoritmo baricéntrico, asegurando que el borrado solo ocurra dentro de los límites del Sprite.
Fase 3: Renderizado en la Textura Dinámica
Las marcas de borrado se dibujan como cuadriláteros blancos sobre un fondo negro en la RenderTexture utilizando CommandBuffer para minimizar el impacto en el rendimiento de la CPU.
Fase 4: Composición en el Shader
El Shader combina la capa superior (el recorte) con la máscara dinámica. La fórmula de transparencia resultante es:
TransparenciaFinal = AlfaSpriteOriginal * AlfaMascaraDinamica
Donde el blanco en la máscara dinámica (áreas rascadas) resulta en una transparencia total, revelando la capa inferior.
Fase 5: Cálculo del Progreso de Borrado
Mediante AsyncGPUReadback, se extraen los datos de la textura dinámica en un NativeArray para contar los píxeles blancos, proporcionando un porcentaje de progreso preciso sin bloquear el hilo principal.