티스토리 뷰
728x90
반응형
1. FinalAtlasTool로 조형물과 데이터(인덱스, UV)를 한 번에 생성하고, WorldEngine이 런타임에 이 모든 것을 관장하는 구조
- 창조자 (FinalAtlasTool): 이미지들을 아틀라스 텍스처 하나로 합치고, 각 이미지를 3D 공간의 '조각(Shard)'으로 배치합니다. 이 과정에서 가장 중요한 **atlasIndex**라는 고유 번호를 각 조각에 부여합니다.
- 데이터 (SculptureAtlasData, ShardData): 생성된 조형물과 조각들은 각자 자신의 정보(사용할 머티리얼, UV 좌표, 고유 인덱스)를 스스로 들고 있습니다.
- 두뇌 (WorldEngine): 플레이어 주변에서 가장 가까운 조각을 찾아, 그 조각의 atlasIndex를 알아냅니다. 그리고 이 인덱스를 이용해 조각에 해당하는 아틀라스 텍스처와 UV 영역(Rect)을 다른 시스템에 제공합니다.
이미지 시퀀스 절차적 조형 만들기.
각 플레인은 이미지 인덱스를 가지고있다.(uv 정보 순서정보)
2. 이미지 시퀀스를 따라 현재의 이미지 프레임 정보를 불러와 full화면으로 보여주며, 지나간 경로의 이미지시퀀스를 아카이빙한다.
코드 정리
1.
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Linq;
using System.Collections.Generic;
public class FinalAtlasTool : EditorWindow
{
private string sourceFolderPath = "Assets/";
private string sculptureName = "New_Sculpture";
private enum PathType { Line, Arc, Curve, Ellipse }
private PathType pathToCreate = PathType.Ellipse;
private float shardSize = 1f;
private Vector3 shardLocalRotation = new Vector3(90, 0, 0);
private float pathLength = 20f;
private float arcAngle = 360f;
private Vector3 startPoint = new Vector3(-10, 0, 0);
private Vector3 endPoint = new Vector3(10, 0, 0);
private Vector3 startTangent = new Vector3(0, 10, 0);
private Vector3 endTangent = new Vector3(0, 10, 0);
private Vector2 ellipseRadius = new Vector2(10f, 10f);
private Vector3 ellipseCenter = Vector3.zero;
private int atlasSize = 4096;
private int padding = 2;
private string rhythmPattern = "1";
private static int totalImageCounter = 0;
private int shardLayer = 0;
[MenuItem("Tools/Final Atlas Tool")]
public static void ShowWindow()
{
GetWindow<FinalAtlasTool>("Final Atlas Tool");
}
void OnGUI()
{
EditorGUILayout.LabelField("Final Atlas Tool", EditorStyles.boldLabel);
if (GUILayout.Button("이미지 시퀀스 폴더 선택..."))
{
string path = EditorUtility.OpenFolderPanel("이미지 폴더 선택", Application.dataPath, "");
if (!string.IsNullOrEmpty(path) && path.StartsWith(Application.dataPath))
{
sourceFolderPath = "Assets" + path.Substring(Application.dataPath.Length);
}
}
EditorGUILayout.SelectableLabel($"선택된 폴더: {sourceFolderPath}");
sculptureName = EditorGUILayout.TextField("조형물 이름", sculptureName);
shardSize = EditorGUILayout.FloatField("개별 조각 크기", shardSize);
pathToCreate = (PathType)EditorGUILayout.EnumPopup("경로 형태", pathToCreate);
switch (pathToCreate)
{
case PathType.Line:
pathLength = EditorGUILayout.FloatField("직선 길이", pathLength);
break;
case PathType.Arc:
pathLength = EditorGUILayout.FloatField("호(Arc) 지름", pathLength);
arcAngle = EditorGUILayout.Slider("호(Arc) 각도", arcAngle, 1, 360);
break;
case PathType.Curve:
startPoint = EditorGUILayout.Vector3Field("시작점", startPoint);
endPoint = EditorGUILayout.Vector3Field("도착점", endPoint);
startTangent = EditorGUILayout.Vector3Field("시작 탄젠트", startTangent);
endTangent = EditorGUILayout.Vector3Field("도착 탄젠트", endTangent);
break;
case PathType.Ellipse:
ellipseRadius = EditorGUILayout.Vector2Field("타원 반지름 (너비, 높이)", ellipseRadius);
arcAngle = EditorGUILayout.Slider("생성할 호(Arc) 각도", arcAngle, 1, 360);
ellipseCenter = EditorGUILayout.Vector3Field("중심 위치", ellipseCenter);
break;
}
shardLocalRotation = EditorGUILayout.Vector3Field("조각 로컬 회전", shardLocalRotation);
atlasSize = EditorGUILayout.IntField("아틀라스 크기", atlasSize);
padding = EditorGUILayout.IntField("이미지 간격 (Padding)", padding);
rhythmPattern = EditorGUILayout.TextField("리듬 패턴", rhythmPattern);
shardLayer = EditorGUILayout.LayerField("조각 레이어 (Shard Layer)", shardLayer);
if (GUILayout.Button("!!! 조형물 생성 실행 !!!", GUILayout.Height(40)))
{
Execute();
}
if (GUILayout.Button("전체 이미지 카운터 리셋"))
{
totalImageCounter = 0;
}
EditorGUILayout.LabelField($"현재까지 생성된 총 이미지 수: {totalImageCounter}");
}
private void Execute()
{
Shader finalShader = Shader.Find("Unlit/AtlasShader_TwoSided");
if (finalShader == null)
{
EditorUtility.DisplayDialog("오류", "쉐이더 'Unlit/AtlasShader_TwoSided'를 찾을 수 없습니다.", "확인");
return;
}
string fullFolderPath = Application.dataPath + sourceFolderPath.Substring("Assets".Length);
var imagePaths = Directory.GetFiles(fullFolderPath, "*.*").Where(s => s.EndsWith(".png") || s.EndsWith(".jpg")).OrderBy(p => p).ToList();
if (imagePaths.Count == 0)
{
EditorUtility.DisplayDialog("오류", "선택된 폴더에 이미지 파일이 없습니다.", "확인");
return;
}
List<Texture2D> texturesToPack = new List<Texture2D>();
foreach (var path in imagePaths)
{
byte[] fileData = File.ReadAllBytes(path);
Texture2D tex = new Texture2D(2, 2);
tex.LoadImage(fileData);
tex.name = Path.GetFileNameWithoutExtension(path);
texturesToPack.Add(tex);
}
string outputFolder = $"Assets/Generated/{sculptureName}";
if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder);
}
Texture2D atlas = new Texture2D(atlasSize, atlasSize);
Rect[] uvRects = atlas.PackTextures(texturesToPack.ToArray(), padding, atlasSize, false);
if (uvRects == null)
{
EditorUtility.DisplayDialog("오류", "아틀라스 생성에 실패했습니다. 아틀라스 크기가 너무 작거나 이미지 개수가 너무 많을 수 있습니다.", "확인");
return;
}
File.WriteAllBytes($"{outputFolder}/{sculptureName}_Atlas.png", atlas.EncodeToPNG());
AssetDatabase.ImportAsset($"{outputFolder}/{sculptureName}_Atlas.png", ImportAssetOptions.ForceUpdate);
Material finalMaterial = new Material(finalShader);
finalMaterial.mainTexture = AssetDatabase.LoadAssetAtPath<Texture2D>($"{outputFolder}/{sculptureName}_Atlas.png");
AssetDatabase.CreateAsset(finalMaterial, $"{outputFolder}/{sculptureName}_Material.mat");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
GameObject sculptureRoot = GameObject.Find(sculptureName);
if (sculptureRoot != null)
{
DestroyImmediate(sculptureRoot);
}
sculptureRoot = new GameObject(sculptureName);
var atlasData = sculptureRoot.AddComponent<SculptureAtlasData>();
atlasData.atlasMaterial = finalMaterial;
atlasData.uvRects = uvRects;
atlasData.imageCount = texturesToPack.Count;
atlasData.startIndex = totalImageCounter;
EditorUtility.SetDirty(atlasData);
List<float> rhythmValues = rhythmPattern.Split(',').Select(s => float.Parse(s.Trim())).ToList();
if (rhythmValues.Count == 0)
{
rhythmValues.Add(1f);
}
List<float> progressPositions = new List<float>();
float currentPosition = 0f;
progressPositions.Add(currentPosition);
for (int i = 0; i < texturesToPack.Count - 1; i++)
{
currentPosition += rhythmValues[i % rhythmValues.Count];
progressPositions.Add(currentPosition);
}
float totalWeight = progressPositions.Last();
if (totalWeight == 0)
{
totalWeight = 1;
}
for (int i = 0; i < texturesToPack.Count; i++)
{
GameObject shardGO = GameObject.CreatePrimitive(PrimitiveType.Plane);
DestroyImmediate(shardGO.GetComponent<MeshCollider>());
var boxCollider = shardGO.AddComponent<BoxCollider>();
boxCollider.isTrigger = true;
shardGO.layer = shardLayer;
shardGO.name = texturesToPack[i].name;
shardGO.transform.SetParent(sculptureRoot.transform);
var renderer = shardGO.GetComponent<MeshRenderer>();
renderer.sharedMaterial = finalMaterial;
MeshFilter meshFilter = shardGO.GetComponent<MeshFilter>();
Mesh mesh = Instantiate(meshFilter.sharedMesh);
mesh.name = meshFilter.sharedMesh.name + "_" + i;
meshFilter.mesh = mesh;
Vector2[] originalUVs = mesh.uv;
Vector2[] newUVs = new Vector2[originalUVs.Length];
Rect uvRect = uvRects[i];
for (int j = 0; j < originalUVs.Length; j++)
{
newUVs[j] = new Vector2(uvRect.x + originalUVs[j].x * uvRect.width, uvRect.y + originalUVs[j].y * uvRect.height);
}
mesh.uv = newUVs;
var data = shardGO.AddComponent<ShardData>();
data.atlasIndex = totalImageCounter + i;
EditorUtility.SetDirty(data);
float progress = progressPositions[i] / totalWeight;
GetTransformAtProgress(progress, out var pos, out var rot);
shardGO.transform.position = pos;
shardGO.transform.rotation = rot;
shardGO.transform.localScale = Vector3.one * shardSize;
}
totalImageCounter += texturesToPack.Count;
EditorUtility.DisplayDialog("성공", $"'{sculptureName}' 조형물이 성공적으로 생성되었습니다. 현재까지 총 {totalImageCounter}개의 이미지가 처리되었습니다.", "확인");
}
void GetTransformAtProgress(float progress, out Vector3 position, out Quaternion rotation)
{
Vector3 tangent;
switch (pathToCreate)
{
case PathType.Line:
position = Vector3.right * (progress - 0.5f) * pathLength;
tangent = Vector3.right;
break;
case PathType.Arc:
float currentAngleRadArc = progress * arcAngle * Mathf.Deg2Rad;
float radius = pathLength / 2f;
position = new Vector3(Mathf.Cos(currentAngleRadArc) * radius, 0, Mathf.Sin(currentAngleRadArc) * radius);
tangent = new Vector3(-Mathf.Sin(currentAngleRadArc) * radius, 0, Mathf.Cos(currentAngleRadArc) * radius);
break;
case PathType.Curve:
position = GetBezierPoint(startPoint, startPoint + startTangent, endPoint + endTangent, endPoint, progress);
tangent = GetBezierFirstDerivative(startPoint, startPoint + startTangent, endPoint + endTangent, endPoint, progress);
break;
case PathType.Ellipse:
float currentAngleRadEllipse = progress * arcAngle * Mathf.Deg2Rad;
float xPos = Mathf.Cos(currentAngleRadEllipse) * ellipseRadius.x;
float zPos = Mathf.Sin(currentAngleRadEllipse) * ellipseRadius.y;
position = ellipseCenter + new Vector3(xPos, 0, zPos);
tangent = new Vector3(-Mathf.Sin(currentAngleRadEllipse) * ellipseRadius.x, 0, Mathf.Cos(currentAngleRadEllipse) * ellipseRadius.y);
break;
default:
position = Vector3.zero;
tangent = Vector3.forward;
break;
}
if (tangent == Vector3.zero)
{
tangent = Vector3.forward;
}
rotation = Quaternion.LookRotation(tangent.normalized) * Quaternion.Euler(shardLocalRotation);
}
Vector3 GetBezierPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;
return uuu * p0 + 3 * uu * t * p1 + 3 * u * tt * p2 + ttt * p3;
}
Vector3 GetBezierFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1 - t;
float uu = u * u;
float tt = t * t;
return 3 * uu * (p1 - p0) + 6 * u * t * (p2 - p1) + 3 * tt * (p3 - p2);
}
}
#endif
using UnityEngine;
// 이 컴포넌트는 WallDisplayManager가 생성하는 각 아카이브 조각(Shard) 프리팹에
// 런타임에 동적으로 추가되어, 자신의 고유한 정보를 기억하는 역할을 합니다.
public class ArchiveShardData : MonoBehaviour
{
public int assignedFrameIndex = -1;
public Texture2D assignedTexture = null;
}
using UnityEngine;
/// <summary>
/// 3D 조형물을 구성하는 각 이미지 조각(Shard)에 부착되는 데이터 컨테이너입니다.
/// </summary>
public class ShardData : MonoBehaviour
{
/// <summary>
/// 이 조각이 전체 이미지 시퀀스 중 몇 번째 이미지에 해당하는지에 대한 인덱스입니다.
/// FinalAtlasTool에 의해 자동으로 설정됩니다.
/// </summary>
public int atlasIndex;
}
using UnityEngine;
/// <summary>
/// FinalAtlasTool로 생성된 각 조형물(Sculpture)의 루트 오브젝트에 부착되는 데이터 컨테이너입니다.
/// 이 조형물이 사용하는 아틀라스, 머티리얼, UV 좌표 배열 등의 정보를 가지고 있습니다.
/// </summary>
public class SculptureAtlasData : MonoBehaviour
{
[Header("Atlas Information")]
public Material atlasMaterial;
public int imageCount;
public int startIndex;
public Rect[] uvRects;
}
2.
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class WallDisplayManager : MonoBehaviour
{
[Header("Required Components")]
public WorldEngine worldEngine;
public RawImage mainDisplayRawImage;
public GameObject archiveShardPrefab;
public Transform archiveShardsParent;
[Header("Archive Rendering")]
public RawImage archiveDisplayRawImage;
public Camera archiveCamera;
public RenderTexture archiveRenderTexture;
[Header("Archive Visuals")]
public Color highlightColor = Color.yellow;
public float highlightThickness = 0.01f;
public Vector2 archiveImageWorldSize = new Vector2(1.0f, 1.0f);
public float archiveSpacingWorld = 0.1f;
public int maxArchiveImages = 20; // 순환 버퍼의 크기
public int maxArchiveImagesPerRow = 20;
// --- 데이터 구조 변경 ---
private class ArchivedFrame { public Texture2D staticTexture; public int originalIndex; }
private HashSet<int> recordedIndices = new HashSet<int>(); // 이미 기록된 인덱스인지 빠르게 확인
private ArchivedFrame[] displaySlots; // 화면에 표시될 고정된 슬롯들
private GameObject[] archiveShardObjects; // 슬롯에 해당하는 게임 오브젝트들
private int nextSlotIndex = 0; // 다음에 채워질 슬롯의 인덱스
private int currentMainDisplayIndex = -1;
private Shader staticArchiveShader;
private MaterialPropertyBlock propBlock;
private int mainTexId, applyHighlightId, highlightColorId, highlightThicknessId;
void Awake()
{
propBlock = new MaterialPropertyBlock();
mainTexId = Shader.PropertyToID("_MainTex");
applyHighlightId = Shader.PropertyToID("_ApplyHighlight");
highlightColorId = Shader.PropertyToID("_HighlightColor");
highlightThicknessId = Shader.PropertyToID("_HighlightThickness");
}
void Start()
{
staticArchiveShader = Shader.Find("UI/ArchiveShardShader");
if (staticArchiveShader == null) this.enabled = false;
if (archiveDisplayRawImage == null || archiveCamera == null || archiveRenderTexture == null)
{
this.enabled = false;
return;
}
archiveDisplayRawImage.texture = archiveRenderTexture;
archiveCamera.targetTexture = archiveRenderTexture;
// ★★★ [핵심 변경] 시작할 때 모든 UI 슬롯을 미리 생성합니다 ★★★
InitializeArchiveSlots();
}
void OnDestroy()
{
// 생성된 텍스처와 오브젝트 모두 파괴
if(displaySlots != null)
{
foreach (var frame in displaySlots) { if (frame != null && frame.staticTexture != null) Destroy(frame.staticTexture); }
}
if(archiveShardObjects != null)
{
foreach (var shard in archiveShardObjects) { if (shard != null) Destroy(shard); }
}
}
/// <summary>
/// 시작 시점에 아카이브 슬롯들을 미리 만들어 준비합니다.
/// </summary>
void InitializeArchiveSlots()
{
displaySlots = new ArchivedFrame[maxArchiveImages];
archiveShardObjects = new GameObject[maxArchiveImages];
for (int i = 0; i < maxArchiveImages; i++)
{
GameObject newShard = Instantiate(archiveShardPrefab, archiveShardsParent);
int row = i / maxArchiveImagesPerRow;
int col = i % maxArchiveImagesPerRow;
newShard.transform.localPosition = new Vector3(col * (archiveImageWorldSize.x + archiveSpacingWorld), 0, -row * (archiveImageWorldSize.y + archiveSpacingWorld));
newShard.transform.localScale = new Vector3(archiveImageWorldSize.x, 1, archiveImageWorldSize.y);
newShard.AddComponent<ArchiveShardData>();
newShard.SetActive(false); // 처음에는 비활성화 상태로 둡니다.
archiveShardObjects[i] = newShard;
}
}
void Update()
{
UpdateMainDisplay();
}
private void UpdateMainDisplay()
{
int newIndex = worldEngine.GetCurrentShardIndex();
if (newIndex == -1 || newIndex == currentMainDisplayIndex) return;
int previousIndex = currentMainDisplayIndex;
currentMainDisplayIndex = newIndex;
Texture currentAtlasTexture = worldEngine.GetTextureAtIndex(newIndex);
if (currentAtlasTexture == null) return;
mainDisplayRawImage.texture = currentAtlasTexture;
mainDisplayRawImage.uvRect = worldEngine.GetUVRectForIndex(newIndex);
// 한 번도 본 적 없는 새로운 인덱스일 경우에만 스냅샷을 찍고 슬롯에 추가합니다.
if (!recordedIndices.Contains(newIndex))
{
StartCoroutine(CaptureAndPlaceInSlot(currentAtlasTexture, mainDisplayRawImage.uvRect, newIndex));
}
else
{
// 이미 본 이미지라면 하이라이트만 업데이트합니다.
UpdateHighlight(previousIndex, currentMainDisplayIndex);
}
}
private IEnumerator CaptureAndPlaceInSlot(Texture sourceAtlas, Rect uvRect, int frameIndex)
{
yield return new WaitForEndOfFrame();
Texture2D staticSnapshot = CreatePermanentSnapshot(sourceAtlas, uvRect);
if (staticSnapshot == null) yield break;
// 1. 새 프레임 데이터 생성 및 기록
var newFrame = new ArchivedFrame { staticTexture = staticSnapshot, originalIndex = frameIndex };
recordedIndices.Add(frameIndex);
// 2. 현재 슬롯에 이미 데이터가 있다면, 이전 텍스처를 파괴하여 메모리 누수 방지
if(displaySlots[nextSlotIndex] != null && displaySlots[nextSlotIndex].staticTexture != null)
{
recordedIndices.Remove(displaySlots[nextSlotIndex].originalIndex);
Destroy(displaySlots[nextSlotIndex].staticTexture);
}
// 3. '다음 슬롯'에 새 데이터 덮어쓰기
displaySlots[nextSlotIndex] = newFrame;
// 4. 해당 슬롯의 UI 업데이트
GameObject targetShard = archiveShardObjects[nextSlotIndex];
targetShard.SetActive(true);
targetShard.GetComponent<ArchiveShardData>().assignedFrameIndex = frameIndex;
var rend = targetShard.GetComponent<Renderer>();
rend.GetPropertyBlock(propBlock);
propBlock.SetTexture(mainTexId, staticSnapshot);
rend.SetPropertyBlock(propBlock);
// 5. 다음 슬롯 인덱스를 순환
nextSlotIndex = (nextSlotIndex + 1) % maxArchiveImages;
// 6. 하이라이트 업데이트 및 최종 렌더링
UpdateHighlight(-1, currentMainDisplayIndex);
}
/// <summary>
/// 하이라이트를 효율적으로 업데이트합니다.
/// </summary>
void UpdateHighlight(int previousIndexToTurnOff, int currentIndexToTurnOn)
{
for (int i = 0; i < maxArchiveImages; i++)
{
if (displaySlots[i] == null) continue;
int frameOriginalIndex = displaySlots[i].originalIndex;
// 하이라이트를 켜거나 꺼야 하는 대상인지 확인
bool needsUpdate = (frameOriginalIndex == previousIndexToTurnOff || frameOriginalIndex == currentIndexToTurnOn);
if(needsUpdate)
{
var rend = archiveShardObjects[i].GetComponent<Renderer>();
rend.GetPropertyBlock(propBlock);
bool isCurrent = (frameOriginalIndex == currentIndexToTurnOn);
propBlock.SetFloat(applyHighlightId, isCurrent ? 1.0f : 0.0f);
rend.SetPropertyBlock(propBlock);
}
}
archiveCamera.Render();
}
private Texture2D CreatePermanentSnapshot(Texture sourceAtlas, Rect uvRect)
{
int snapshotWidth = 256;
int snapshotHeight = 256;
RenderTexture tempRT = RenderTexture.GetTemporary(snapshotWidth, snapshotHeight, 0, RenderTextureFormat.Default);
Graphics.Blit(sourceAtlas, tempRT, uvRect.size, uvRect.position);
RenderTexture.active = tempRT;
Texture2D snapshot = new Texture2D(snapshotWidth, snapshotHeight, TextureFormat.RGBA32, false);
snapshot.ReadPixels(new Rect(0, 0, snapshotWidth, snapshotHeight), 0, 0);
snapshot.Apply(false);
snapshot.Compress(true);
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(tempRT);
return snapshot;
}
private class ArchiveShardData : MonoBehaviour
{
public int assignedFrameIndex;
}
}
이건 플레이어에
using UnityEngine;
public class PixelSampler : MonoBehaviour
{
public WorldEngine worldEngine;
private Material viewMaterial;
private int currentImageIndex = -1;
void Start()
{
Renderer renderer = GetComponent<Renderer>();
if (renderer == null || renderer.sharedMaterial == null)
{
Debug.LogError("PixelSampler에 Renderer 또는 Material이 없습니다.");
this.enabled = false;
return;
}
viewMaterial = renderer.material; // 고유 머티리얼 인스턴스 생성
}
void Update()
{
if (worldEngine == null) return;
int newImageIndex = worldEngine.GetCurrentShardIndex();
if (newImageIndex == -1)
{
viewMaterial.SetTexture("_MainTex", Texture2D.blackTexture);
return;
}
if (newImageIndex != currentImageIndex)
{
currentImageIndex = newImageIndex;
Texture currentTexture = worldEngine.GetTextureAtIndex(currentImageIndex);
if (currentTexture != null)
{
viewMaterial.SetTexture("_MainTex", currentTexture);
}
}
}
}
이게 총괄
using UnityEngine;
using System.Linq;
public class WorldEngine : MonoBehaviour
{
[Header("Core Components")]
public Transform player;
public Transform[] mapRoots;
[Header("Optimization Settings")]
[Tooltip("플레이어 주변에서 조각을 탐색할 반경입니다.")]
public float searchRadius = 5f;
[Tooltip("조각(Shard) 오브젝트들이 속한 레이어를 지정해야 합니다.")]
public LayerMask shardLayer;
private SculptureAtlasData[] sculptureData;
void Awake()
{
sculptureData = mapRoots.Select(root => root.GetComponent<SculptureAtlasData>()).ToArray();
if(sculptureData.Length > 0)
{
Debug.Log($"WorldEngine: {sculptureData.Length}개의 조형물 루트를 성공적으로 로드했습니다.");
}
}
/// <summary>
/// [최적화 & 진단 기능 추가] 플레이어 주변에서 가장 가까운 조각의 인덱스를 반환합니다.
/// </summary>
public int GetCurrentShardIndex()
{
if (player == null)
{
Debug.LogError("[WorldEngine] 'Player'가 할당되지 않았습니다!");
return -1;
}
// 1. 플레이어 주변의 콜라이더를 탐색합니다.
Collider[] nearbyColliders = Physics.OverlapSphere(player.position, searchRadius, shardLayer);
// 2. ★★★ 진단 코드 ★★★
// 만약 주변에서 콜라이더를 하나도 찾지 못했다면, 경고 로그를 출력합니다.
if (nearbyColliders.Length == 0)
{
Debug.LogWarning($"[WorldEngine 진단] 플레이어 주변({searchRadius}m)에서 'Shard' 레이어를 가진 콜라이더를 찾지 못했습니다! 아래 사항을 확인하세요:\n1. 조각(Shard) 오브젝트들의 Layer가 'Shard Layer' 필드에 지정된 레이어와 일치하는가?\n2. 'Search Radius' 값이 너무 작지 않은가?\n3. 조각 오브젝트에 Box Collider가 제대로 붙어있는가?");
return -1;
}
// 3. 찾은 콜라이더들 중에서 가장 가까운 것을 찾습니다.
float minDistance = float.MaxValue;
int closestIndex = -1;
foreach (var col in nearbyColliders)
{
// ShardData 컴포넌트가 없는 경우는 건너뜁니다.
ShardData shardData = col.GetComponent<ShardData>();
if (shardData == null) continue;
float distance = Vector3.Distance(player.position, col.transform.position);
if (distance < minDistance)
{
minDistance = distance;
closestIndex = shardData.atlasIndex;
}
}
if (closestIndex == -1)
{
Debug.LogWarning("[WorldEngine 진단] 주변에서 콜라이더는 찾았지만, ShardData 컴포넌트를 가진 오브젝트가 없습니다.");
}
return closestIndex;
}
// 이하 GetTextureAtIndex, GetUVRectForIndex 함수는 동일합니다.
public Texture GetTextureAtIndex(int index)
{
if (index < 0) return null;
foreach (var data in sculptureData)
{
if (data != null && index >= data.startIndex && index < data.startIndex + data.imageCount)
return data.atlasMaterial.mainTexture;
}
return null;
}
public Rect GetUVRectForIndex(int index)
{
if (index < 0) return new Rect(0, 0, 1, 1);
foreach (var data in sculptureData)
{
if (data != null && index >= data.startIndex && index < data.startIndex + data.imageCount)
{
int localIndex = index - data.startIndex;
if (localIndex >= 0 && localIndex < data.uvRects.Length)
return data.uvRects[localIndex];
}
}
return new Rect(0, 0, 1, 1);
}
}
728x90
반응형
'Unity' 카테고리의 다른 글
Unity Procedural interactiv 3D 종이접기 시스템 (3) | 2025.07.18 |
---|---|
Unity ML _ ai 학습 프로젝트 (2) | 2025.07.14 |
Unity_VR_2024ver_Oculus Quest 3_Meta SDK 패키지 (0) | 2024.08.02 |
Unity_real time _ audio thread : audioBuffer (0) | 2023.11.06 |
Unity_wav파일 Envelopes (0) | 2023.10.12 |
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 4dgs
- MCP
- colab
- 4d guassian splatting
- Java
- krea
- three.js
- TouchDesigner
- houdini
- RNN
- VFXgraph
- opticalflow
- docker
- DeepLeaning
- node.js
- 유니티
- Midjourney
- sequelize
- Express
- Python
- AI
- Unity
- MQTT
- opencv
- Arduino
- CNC
- 라즈베리파이
- ai film
- VR
- 후디니
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
글 보관함
반응형