티스토리 뷰
**curve_u = 선을 따라 진행된 비율 (0~1)** ``` 출생지 ─────────────────────> 사망지 0.0 0.25 0.5 0.75 1.0
CSV 파일 구조 확인
row[0] = ID (고유 ID)
row[1] = Name (이름)
row[2] = birth_lat (출생지 위도)
row[3] = birth_lon (출생지 경도)
row[4] = death_lat (사망지 위도)
row[5] = death_lon (사망지 경도)
row[6] = uncertainty_birth (출생지 불확실성)
row[7] = uncertainty_death (사망지 불확실성)
row[8] = Date_Int (사망날짜)
Primitive 속성 (선에 저장):
- path_id (string): 고유 ID ⭐ - person_name (string): 이름 ⭐ - death_date (int): 사망날짜 ⭐
- `time`: 선을 따라 진행된 비율 (0.0 → 1.0) - 애니메이션용 - curve_u와 같은 값
- `death_date`: 실제 사망날짜 (예: 19500315) - 그룹핑/필터링용 - Unity에서 중요!
값매피
path_id = row[0] # 고유 ID
person_name = row[1] # 이름
birth_lat = float(row[2]) # 출생지 위도
birth_lon = float(row[3]) # 출생지 경도
death_lat = float(row[4]) # 사망지 위도
death_lon = float(row[5]) # 사망지 경도
uncert_birth = float(row[6]) # 출생지 불확실성
uncert_death = float(row[7]) # 사망지 불확실성
death_date = int(row[8]) # 사망날짜
프리미티브에저장
poly.setAttribValue("path_id", path_id) # 고유 ID
poly.setAttribValue("person_name", person_name) # 이름
poly.setAttribValue("death_date", death_date) # 사망날짜
# 좌표 데이터
poly.setAttribValue("birth_lat", birth_lat)
poly.setAttribValue("birth_lon", birth_lon)
poly.setAttribValue("death_lat", death_lat)
poly.setAttribValue("death_lon", death_lon)
# 그룹핑용 ID
poly.setAttribValue("death_grid_id", death_grid)
1. 속도값과 pscale의 의미
pscale (Point Scale)
- 의미: 포인트의 크기를 제어하는 속성
- 현재 코드: uncertainty * PSCALE_MULTIPLIER로 설정
- 목적: 위치 데이터의 불확실성을 시각적으로 표현 (불확실성이 클수록 포인트가 큼)
- 출생지/사망지 각각의 불확실성을 구분하여 표시
import hou
import csv
import os
import math
# =========================================================================
# 설정
# =========================================================================
# CSV 파일 경로
FILE_PATH = "/Users/l.smin/Documents/4_3_jeju/pj_code/1129/processed_paths.csv"
# 제주도 중심 좌표 (원점)
CENTER_LON = 126.53
CENTER_LAT = 33.50
# 위경도 → 거리 변환
LAT_KM_PER_DEGREE = 111.0 # 위도 1도 ≈ 111km
LON_KM_PER_DEGREE = 111.0 * math.cos(math.radians(CENTER_LAT)) # ≈ 92.4km
# Houdini 공간 스케일 (제주도 동서 73km → 10 units)
SCALE_FACTOR = 10.0 / 73.0 # 1km = 0.137 units
# pscale 배율 (시각화용)
PSCALE_MULTIPLIER = 0.01
# =========================================================================
# Houdini 초기화
# =========================================================================
node = hou.pwd()
geo = node.geometry()
geo.clear()
# -------------------------------------------------------------------------
# 속성 정의
# -------------------------------------------------------------------------
# Point 속성 (점에 저장)
geo.addAttrib(hou.attribType.Point, "pscale", 0.0) # 점 크기 (시각화)
geo.addAttrib(hou.attribType.Point, "curve_u", 0.0) # 선 진행 비율 (0→1)
geo.addAttrib(hou.attribType.Point, "uncertainty", 0.0) # 불확실성 원본값
# Primitive 속성 (선에 저장 - 각 경로의 메타데이터)
geo.addAttrib(hou.attribType.Prim, "path_id", "") # 고유 ID ⭐
geo.addAttrib(hou.attribType.Prim, "person_name", "") # 이름 ⭐
geo.addAttrib(hou.attribType.Prim, "death_date", 0) # 사망날짜 ⭐
# 좌표 데이터 (그룹핑/필터링용)
geo.addAttrib(hou.attribType.Prim, "birth_lat", 0.0)
geo.addAttrib(hou.attribType.Prim, "birth_lon", 0.0)
geo.addAttrib(hou.attribType.Prim, "death_lat", 0.0)
geo.addAttrib(hou.attribType.Prim, "death_lon", 0.0)
# 사망지 그리드 ID (같은 지역 그룹핑용)
geo.addAttrib(hou.attribType.Prim, "death_grid_id", "")
# -------------------------------------------------------------------------
# 좌표 변환 함수
# -------------------------------------------------------------------------
def latlon_to_xy(lat, lon):
"""
위경도를 Houdini XZ 좌표로 변환
- 제주도 중심을 (0, 0)으로
- 실제 거리 비율 유지
"""
delta_lon = lon - CENTER_LON
delta_lat = lat - CENTER_LAT
# km 단위 거리
x_km = delta_lon * LON_KM_PER_DEGREE
z_km = delta_lat * LAT_KM_PER_DEGREE
# Houdini 좌표 (Z축 반전으로 북쪽=위)
x = x_km * SCALE_FACTOR
z = -z_km * SCALE_FACTOR
return (x, z)
def create_death_grid_id(lat, lon, grid_size=0.01):
"""
사망지를 그리드로 분할해서 ID 생성
grid_size=0.01 → 약 1km 단위 그리드
grid_size=0.1 → 약 10km 단위 그리드
"""
precision = int(1.0 / grid_size)
grid_lat = int(lat * precision)
grid_lon = int(lon * precision)
return f"{grid_lat}_{grid_lon}"
# -------------------------------------------------------------------------
# CSV 파일 읽기 및 경로 생성
# -------------------------------------------------------------------------
if not os.path.exists(FILE_PATH):
print(f"❌ 파일을 찾을 수 없습니다: {FILE_PATH}")
print(" 경로를 확인해주세요!")
else:
with open(FILE_PATH, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
header = next(reader, None) # 헤더 스킵
count = 0
skipped = 0
for row in reader:
# 데이터 유효성 검사
if not row or len(row) < 9:
skipped += 1
continue
try:
# CSV 데이터 파싱
path_id = row[0] # 고유 ID
person_name = row[1] # 이름
birth_lat = float(row[2]) # 출생지 위도
birth_lon = float(row[3]) # 출생지 경도
death_lat = float(row[4]) # 사망지 위도
death_lon = float(row[5]) # 사망지 경도
uncert_birth = float(row[6]) # 출생지 불확실성
uncert_death = float(row[7]) # 사망지 불확실성
death_date = int(row[8]) # 사망날짜
# 유효성 검사: 사망지 좌표가 없는 경우 스킵
if death_lat == 0.0 and death_lon == 0.0:
skipped += 1
continue
# 좌표 변환
birth_x, birth_z = latlon_to_xy(birth_lat, birth_lon)
death_x, death_z = latlon_to_xy(death_lat, death_lon)
# 사망지 그리드 ID 생성
death_grid = create_death_grid_id(death_lat, death_lon, grid_size=0.01)
# =============================================================
# 지오메트리 생성: 선 (Polygon)
# =============================================================
poly = geo.createPolygon()
poly.setIsClosed(False) # 열린 선
# 시작점: 출생지
pt_birth = geo.createPoint()
pt_birth.setPosition((birth_x, 0.0, birth_z))
pt_birth.setAttribValue("curve_u", 0.0)
pt_birth.setAttribValue("pscale", uncert_birth * PSCALE_MULTIPLIER)
pt_birth.setAttribValue("uncertainty", uncert_birth)
poly.addVertex(pt_birth)
# 끝점: 사망지
pt_death = geo.createPoint()
pt_death.setPosition((death_x, 0.0, death_z))
pt_death.setAttribValue("curve_u", 1.0)
pt_death.setAttribValue("pscale", uncert_death * PSCALE_MULTIPLIER)
pt_death.setAttribValue("uncertainty", uncert_death)
poly.addVertex(pt_death)
# =============================================================
# Primitive 속성 설정 (선의 메타데이터)
# =============================================================
poly.setAttribValue("path_id", path_id) # 고유 ID
poly.setAttribValue("person_name", person_name) # 이름
poly.setAttribValue("death_date", death_date) # 사망날짜
# 좌표 데이터
poly.setAttribValue("birth_lat", birth_lat)
poly.setAttribValue("birth_lon", birth_lon)
poly.setAttribValue("death_lat", death_lat)
poly.setAttribValue("death_lon", death_lon)
# 그룹핑용 ID
poly.setAttribValue("death_grid_id", death_grid)
count += 1
except (ValueError, IndexError) as e:
print(f"⚠️ 데이터 오류 - ID: {row[0] if row else '?'}, 이름: {row[1] if len(row) > 1 else '?'}")
print(f" 사유: {e}")
skipped += 1
continue
# 완료 메시지
print("=" * 60)
print(f"✅ 경로 생성 완료!")
print(f" 생성: {count}개")
print(f" 스킵: {skipped}개")
print(f" 총계: {count + skipped}개")
print("=" * 60)
print(f"📏 스케일: 1km = {SCALE_FACTOR:.3f} Houdini units")
print(f"📐 제주도 크기: 동서 {73 * SCALE_FACTOR:.1f} units, 남북 {31 * SCALE_FACTOR:.1f} units")
print("=" * 60)
============================================================
Houdini 노드 체인 - 완전한 워크플로우
============================================================
1. Python 노드 (위의 코드)
↓
[출력] 400개 선 (Polygons)
Primitive 속성: path_id, person_name, death_date, death_grid_id, 좌표들
Point 속성: P, pscale, curve_u, uncertainty
============================================================
2. Resample SOP
↓
설정:
- Resample: Length
- Length: 0.2 (또는 0.1 - 점을 촘촘하게)
- Treat Polygons As: Subdivision Curves
[출력] 선마다 여러 점이 생김
- Point 속성 자동 보간 (pscale, curve_u, uncertainty)
- Primitive 속성 유지 (path_id, person_name, death_date)
============================================================
3. PolyFrame SOP
↓
설정:
- Entity: Points
- Style: First Edge
- Tangent Name: v
[출력] 각 점에 속도(v) 추가됨
Point 속성 추가: v (vector)
============================================================
4. Attribute Wrangle SOP (Point 모드) ⭐ 중요!
↓
VEX 코드:
// Primitive 속성 → Point 속성으로 복사
s@path_id = prim(0, "path_id", @primnum); // 사람 고유 ID
s@person_name = prim(0, "person_name", @primnum); // 이름
i@death_date = prim(0, "death_date", @primnum); // 사망날짜 (정수)
// 좌표 데이터 (원본 위경도 저장용)
f@birth_lat = prim(0, "birth_lat", @primnum);
f@birth_lon = prim(0, "birth_lon", @primnum);
f@death_lat = prim(0, "death_lat", @primnum);
f@death_lon = prim(0, "death_lon", @primnum);
// 그리드 ID (사망 위치 클러스터링용)
s@death_grid_id = prim(0, "death_grid_id", @primnum);
// 애니메이션용 time 속성
f@time = f@curve_u; // 0~1 범위
// 날짜 기반 색상 (1947~1958 범위를 0~1로 정규화)
float normalized_date = fit(i@death_date, 19470101, 19561201, 0, 1);
v@Cd = chramp("date_color", normalized_date); // Ramp 파라미터 필요
[출력] 모든 속성이 Point에 있음!
Point 속성: P, v, path_id, person_name, death_date, death_grid_id,
time, curve_u, uncertainty, pscale, 좌표들
============================================================
5. Delete SOP
↓
설정:
- Entity: Primitives
delete non select
- Keep Points: ✅ 체크!
[출력] Point만 남음 (선은 삭제됨)
- 모든 속성은 Point에 보존됨
============================================================
6. Labs Niagara ROP (또는 ROP Alembic Output)
↓
Labs Niagara ROP 설정:
Output Path:
$HOME/Desktop/migration_paths.hcsv
Keep Attributes:
P v path_id person_name death_date death_grid_id time pscale uncertainty
Cast Attributes:
0 (필요없음)
─────────────────────────────
여기서 실패
Labs Niagara ROP 대신:
- File SOP 추가 (Delete 뒤에)
- File → $HIP/test.bgeo
- File Mode → Write
- Save to Disk 클릭
.bgeo 파일이 생성되고 데이터가 있나요? ㅇㅇ
있으면 → Labs Niagara ROP 버그 없으면 → 노드 체인 문제
Labs Niagara ROP 버그
1. bgeo를 JSON으로 변환
delete 다음 - File 연결 뒤에 Python SOP 추가:
import json
node = hou.pwd()
geo = node.geometry()
# Point 데이터 추출
data = []
for pt in geo.points():
data.append({
"P": list(pt.position()),
"v": list(pt.attribValue("v")),
"path_id": pt.attribValue("path_id"),
"person_name": pt.attribValue("person_name"),
"death_date": pt.attribValue("death_date"),
"death_grid_id": pt.attribValue("death_grid_id"),
"time": pt.attribValue("time"),
"pscale": pt.attribValue("pscale"),
"uncertainty": pt.attribValue("uncertainty")
})
# JSON 저장
output_path = hou.expandString("$HIP/migration_data.json")
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
print(f"✅ JSON 저장 완료: {output_path}")
print(f"📊 포인트 개수: {len(data)}")
============================================================
최종 출력 데이터 구조:
============================================================
각 Point마다:
- P (vector): 위치
- v (vector): 속도/방향
- path_id (string): 고유 ID
- person_name (string): 이름
- death_date (int): 사망날짜 (YYYYMMDD)
- death_grid_id (string): 사망지 그리드
- time (float): 선 진행 비율 (0~1)
- pscale (float): 점 크기
- uncertainty (float): 불확실성
============================================================
Unity에서 사용:
============================================================
VFX Graph에서 사용하는 방법:
1️⃣ JSON → Texture로 변환 필요
VFX Graph는 JSON을 직접 못 읽습니다. Texture2D 또는 GraphicsBuffer로 변환해야 합니다.
2️⃣ Unity C# 스크립트로 데이터 로드
using UnityEngine;
using System.Collections.Generic;
[System.Serializable]
public class ParticleData
{
public float[] P; // Position [x,y,z]
public float[] v; // Velocity [x,y,z]
public string path_id;
public string person_name;
public int death_date;
public string death_grid_id;
public float time;
public float pscale;
public float uncertainty;
}
public class MigrationDataLoader : MonoBehaviour
{
public TextAsset jsonFile;
public List<ParticleData> particles;
void Start()
{
// JSON 파싱
particles = JsonUtility.FromJson<List<ParticleData>>(
"{\"items\":" + jsonFile.text + "}"
).items;
Debug.Log($"로드 완료: {particles.Count}개 파티클");
}
}
3️⃣ VFX Graph 사용 방법
Graphics Buffer 사용
- Compute Shader로 데이터 전달
- 더 복잡하지만 성능 좋음
'후디니' 카테고리의 다른 글
| Houdini to Unity : 벡터 필드(Vector Field)/SDF(Signed Distance Field) (1) | 2025.12.01 |
|---|---|
| 후디니 위도경도 지도 map data 연결 (0) | 2025.11.30 |
| Houdini(Data Visualization) _ GEO : .shp 데이터 불러오기 / .geoJson (0) | 2025.11.06 |
| houdini - stable diffusion (0) | 2024.02.25 |
| houdini_기초 (1) | 2024.02.25 |
- Total
- Today
- Yesterday
- houdini
- Midjourney
- Arduino
- krea
- TouchDesigner
- DeepLeaning
- sequelize
- Python
- VFXgraph
- ai film
- MQTT
- Java
- VR
- docker
- 라즈베리파이
- RNN
- Express
- Unity
- MCP
- CNC
- 유니티
- opticalflow
- 4d guassian splatting
- node.js
- colab
- 4dgs
- opencv
- 후디니
- three.js
- AI
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |

