private void FadeIn(float timeFromBirth) { // fadeDelta 변수 - 페이드아웃이 몇 % 진행되었는지 델타값, SetVertsPos()에서도 사용하기때문에 private 처리. fadeDelta = timeFromBirth / appearTime; //Color newColor = Color.Lerp(Color.white, soxLtn.fadeOutColor, delta); switch (appearColorType) { case SoxLtn.AnimCurveType.CONSTANT: break; case SoxLtn.AnimCurveType.LINEAR: ColorStreak(appearColor, mainColor, fadeDelta); break; case SoxLtn.AnimCurveType.EASEIN: ColorStreak(appearColor, mainColor, SoxLtn.EaseIn(0f, 1f, fadeDelta)); break; case SoxLtn.AnimCurveType.EASEOUT: ColorStreak(appearColor, mainColor, SoxLtn.EaseOut(0f, 1f, fadeDelta)); break; default: break; } }
private static void CreateLtn(MenuCommand sel) { GameObject newLtn = new GameObject("LightningEmitter"); SoxLtn soxLtn = newLtn.AddComponent <SoxLtn>(); SoxLtnUtilEditor.CheckNodes(soxLtn); soxLtn.version = soxLtn.versionNow; GameObject selObj = (GameObject)sel.context; if (selObj != null) { newLtn.transform.SetParent(selObj.transform); newLtn.transform.localPosition = Vector3.zero; newLtn.transform.localEulerAngles = Vector3.zero; newLtn.transform.localScale = Vector3.one; } if (Selection.gameObjects.Length <= 1) { Selection.activeGameObject = newLtn; } Undo.RegisterCreatedObjectUndo(newLtn, "Create Lightning Emitter"); }
private void FadeOut(float timeFromBirth) { // fadeDelta 변수 - 페이드아웃이 몇 % 진행되었는지 델타값, SetVertsPos()에서도 사용하기때문에 private 처리. fadeDelta = 1f - (timeFromBirth - fadeTimeRest) / fadeTime; // 다이나믹 타입은 SetVertsPos() 에서 폭을 조절한다. if (animType == SoxLtn.AnimType.STATIC) { switch (fadeOutThinType) { case SoxLtn.AnimCurveType.CONSTANT: break; case SoxLtn.AnimCurveType.LINEAR: ThinStreak(fadeDelta); break; case SoxLtn.AnimCurveType.EASEIN: ThinStreak(SoxLtn.EaseIn(0f, 1f, fadeDelta)); break; case SoxLtn.AnimCurveType.EASEOUT: ThinStreak(SoxLtn.EaseOut(0f, 1f, fadeDelta)); break; default: break; } } switch (fadeOutColorType) { case SoxLtn.AnimCurveType.CONSTANT: if (fadeState != fadeStateBefore) { // CONSTANT 라도 한 번은 세팅해준다. ColorStreak(fadeOutColor, mainColor, fadeDelta); fadeStateBefore = fadeState; } break; case SoxLtn.AnimCurveType.LINEAR: ColorStreak(fadeOutColor, mainColor, fadeDelta); break; case SoxLtn.AnimCurveType.EASEIN: ColorStreak(fadeOutColor, mainColor, SoxLtn.EaseOut(0f, 1f, fadeDelta)); break; case SoxLtn.AnimCurveType.EASEOUT: ColorStreak(fadeOutColor, mainColor, SoxLtn.EaseIn(0f, 1f, fadeDelta)); break; default: break; } }
// Position Offset Animation을 위해 lifeDelta, scaleVar, 보간 애니메이션 타입을 고려한 벡터를 리턴 private Vector3 GetPosOffsetAnimPos() { Vector3 returnVec = Vector3.zero; switch (posOffsetAnimType) { case SoxLtn.AnimCurveType.EASEOUT: returnVec = posOffsetAnim * SoxLtn.EaseOut(0f, 1f, lifeDelta) * scaleVar; break; case SoxLtn.AnimCurveType.EASEIN: returnVec = posOffsetAnim * SoxLtn.EaseIn(0f, 1f, lifeDelta) * scaleVar; break; default: //CONSTANT, LINEAR returnVec = posOffsetAnim * lifeDelta * scaleVar; break; } return(returnVec); }
// 버택스 위치 선정, 애니메이션 등이 모두 이 함수에서 수행된다. // 버택스 위치가 달라질 때 UV도 연동되려면 UV 값도 이곳에서 처리되어야 한다. 그러나 퍼포먼스를 위해 UV 연동은 생략 public void SetVertsPos() { // soxLtn이 삭제되면 SetVertsPos() 작동을 멈춘다. // soxLtn이 삭제되어도 이펙트가 살아있도록 하려면 노드 트랜스폼에 대한 매트릭스 연산방식으로 로직을 변경해야하는데 현재는 노드 트랜스폼으로부터 직접 포인트 연산을 하는 방식이라서 이펙트가 살아있는 처리는 나중에... if (!soxLtn || !soxLtnNodes[0]) { return; } // SetVertsPos() 함수는 DYNAMIC 타입에서 계속 불리기때문에 distance all을 계속 참조해야한다. nodeDistanceAll = soxLtn.nodeDistanceAll; // 스케일 애니메이션이 되는 경우도 있으므로 실시간으로 스케일값을 업데이트 scaleVar = ( Mathf.Abs(soxLtn.transform.lossyScale.x) + Mathf.Abs(soxLtn.transform.lossyScale.y) + Mathf.Abs(soxLtn.transform.lossyScale.z) ) / 3f; if (scaleVar == 0f) { // scaleVar 로 나누는 경우가 종종 있어여 0이 되는 상황을 강제로 막아준다. scaleVar = 1f; } // Position Offset Animation 을 사용할 경우 임시로 Node 위치를 이동시켜둔다. (soxLtn을 옮기면 자식도 한방에 옮길 수 있지만 여러 계층구조가 얽힌 라이트닝도 고려해야해서 node들을 하나씩 옮김) // for 루프가 끝날 때 다시 원위치 해줘야한다. Vector3 tempPosOffsetAnim = Vector3.one; if (ifUsePosOffsetAnim) { tempPosOffsetAnim = GetPosOffsetAnimPos(); for (int i = 0; i < soxLtnNodes.Length; i++) { soxLtnNodes[i].transform.position += tempPosOffsetAnim; } } Matrix4x4 tempZRotMatrix = Matrix4x4.identity; float nodeDistanceSum = 0f; Vector3 waveVector; meMatrix = meTrans.localToWorldMatrix; meMatrixInverse = meMatrix.inverse; // 노드 매트릭스를 미리 계산해둔다. (for 문 안에서 nodeMatrixs[i], nodeMatrixs[i -1] 등으로 쓰일 일이 있음) for (int i = 0; i < soxLtnNodes.Length; i++) { nodeMatrixs[i] = soxLtnNodes[i].transform.localToWorldMatrix; } for (int i = 0; i < soxLtnNodes.Length; i++) { // i 카운트는 노드를 의미함 // 노드 위치 버택스들 먼저 처리 nodeVerts[i] = new Vector3(0, 0, 0); // Expand용 tDirVec 방향 벡터 세팅 Vector3 tDirVec; if (soxLtnNodes[i].nodeType == SoxLtnNode.NodeType.CIRCLE) { tDirVec = expDirCircle; } else { tDirVec = expDirSphere; } // zTwists 만큼 회전 매트릭스 세팅 (tempRotMatrix) (zTwists 는 각 노드마다 비틀림 값을 지정할 수 있는 기능) tempZRotMatrix.SetTRS(Vector3.zero, Quaternion.Euler(0, 0, zTwists[i]), Vector3.one); tDirVec = tempZRotMatrix.MultiplyPoint3x4(tDirVec); // circle 혹은 shpere 방향으로 확장 벡터를 더해준다. if (soxLtnNodes[i].fillType == SoxLtnNode.FillType.INSIDE) { // INSIDE nodeVerts[i] += tDirVec * expDist * soxLtnNodes[i].circleArea; // 확장 거리 변수에 시간 변화에 따른 누적 값 반영 expDist += expandAnimSpd * anyDeltaTime; } else { //SURFACE nodeVerts[i] += tDirVec * expDistSurf * soxLtnNodes[i].circleArea; // 확장 거리 변수에 시간 변화에 따른 누적 값 반영 expDistSurf += expandAnimSpd * anyDeltaTime; } // halfWidth 이펙트 두께 설정 (두께의 절반) switch (fadeState) { case FadeState.FADEIN: switch (appearThinType) { case SoxLtn.AnimCurveType.CONSTANT: if (fadeState != fadeStateBefore) { halfWidth = new Vector3(-streakWidthHalf, 0, 0); fadeStateBefore = fadeState; } break; case SoxLtn.AnimCurveType.LINEAR: halfWidth = new Vector3(-streakWidthHalf * fadeDelta, 0, 0); break; case SoxLtn.AnimCurveType.EASEIN: halfWidth = new Vector3(-streakWidthHalf * SoxLtn.EaseIn(0f, 1f, fadeDelta), 0, 0); break; case SoxLtn.AnimCurveType.EASEOUT: halfWidth = new Vector3(-streakWidthHalf * SoxLtn.EaseOut(0f, 1f, fadeDelta), 0, 0); break; default: break; } break; case FadeState.MAIN: halfWidth = new Vector3(-streakWidthHalf, 0, 0); break; case FadeState.FADEOUT: switch (fadeOutThinType) { case SoxLtn.AnimCurveType.CONSTANT: halfWidth = new Vector3(-streakWidthHalf, 0, 0); break; case SoxLtn.AnimCurveType.LINEAR: halfWidth = new Vector3(-streakWidthHalf * fadeDelta, 0, 0); break; case SoxLtn.AnimCurveType.EASEIN: halfWidth = new Vector3(-streakWidthHalf * SoxLtn.EaseOut(0f, 1f, fadeDelta), 0, 0); break; case SoxLtn.AnimCurveType.EASEOUT: halfWidth = new Vector3(-streakWidthHalf * SoxLtn.EaseIn(0f, 1f, fadeDelta), 0, 0); break; default: break; } break; } // 현재 노드까지의 누적 길이 합산 nodeDistanceSum += soxLtnNodes[i].distance; // Cone 두께 halfWidthCone 설정 float tempCone = 1f; Vector3 halfWidthCone; if (ifUseCone) { tempCone = GetCone(nodeDistanceSum); halfWidthCone = halfWidth * tempCone; } else { halfWidthCone = halfWidth; } int nodeVertIndex = (subStep + 1) * (i * 2); Vector3 finalWorldPos1; Vector3 finalWorldPos2; if (ifUseWave && soxLtn.waves != null) { // Wave를 사용하는 경우 // waveVector는 노드의 로컬방향이 반영되도록 하기 위해서 노드 트랜스폼을 기준으로 적용한다. (이후 서브 버택스의 웨이브는 매트릭스와 함께 연산) waveVector = GetWave(nodeDistanceSum) * tempCone; finalWorldPos1 = nodeMatrixs[i].MultiplyPoint3x4(nodeVerts[i] + halfWidthCone + waveVector); finalWorldPos2 = nodeMatrixs[i].MultiplyPoint3x4(nodeVerts[i] - halfWidthCone + waveVector); } else { // Wave를 사용하지 않는 경우 finalWorldPos1 = nodeMatrixs[i].MultiplyPoint3x4(nodeVerts[i] + halfWidthCone); finalWorldPos2 = nodeMatrixs[i].MultiplyPoint3x4(nodeVerts[i] - halfWidthCone); } vertices[nodeVertIndex] = meMatrixInverse.MultiplyPoint3x4(finalWorldPos1); //노드의 로컬을 월드로 변환한 뒤 월드를 이펙트의 로컬로 다시 변환 vertices[nodeVertIndex + 1] = meMatrixInverse.MultiplyPoint3x4(finalWorldPos2); //노드의 로컬을 월드로 변환한 뒤 월드를 이펙트의 로컬로 다시 변환 // PlayLinkedParticles()을 SetVertsPos()에서 해주는 이유는, 노드를 범위로 지정할 경우 라이트닝이 생겨날 위치 계산이 복잡해서 버택스 처리하면서 같이 처리해주기 위함 if (Application.isPlaying && !ifPlayParticle) { // ifPlayParticle에 의해 딱 한번만 플레이 한다. PlayLinkedParticles(meMatrix.MultiplyPoint3x4((vertices[nodeVertIndex] + vertices[nodeVertIndex + 1]) * 0.5f), i); } // 여기부터 서브스텝 버택스들 처리 if (i != 0) { // 첫 번째 노드는 서브스텝이 없어서 i가 0일 때에는 처리하지 않는다 // 서브스텝 버택스들 처리 float tempNoiseType = 1f; if (subStepNoiseAmpType == SoxLtn.ConstantRelated.DISTANCE_RELATED) { tempNoiseType = soxLtnNodes[i].distance; } for (int j = 1; j <= subStep; j++) { // j 카운트는 서브스텝 버택스 순서를 의미함 // bias 변수는 두 노드 사이에 서브스텝에 의해 간격이 어느 위치에 있어야하는지 비율 값이다. 주로 Lerp 의 세 번째 매개변수로 사용 float bias = ((1f / (float)(subStep + 1)) * j); // biasForWave? bias 값은 부드러운 서브스텝을 위해 중간에 변형되므로 wave의 distance 기준이 될 수 있는 순수한 bias 값이 필요함 float biasForWave = bias; // shiftStart와 shiftEnd 값은 노드 로컬 방향을 기준으로 z 방향으로 이동한 서브스텝 포인트의 위치. 이 쉬프트 덕분에 substep이 부드러운 곡선을 그린다. Vector3 shiftStart = new Vector3(0, 0, soxLtnNodes[i].distance * bias); Vector3 shiftEnd = new Vector3(0, 0, soxLtnNodes[i].distance * (1f - bias) * -1f); // lossy 스케일 반영 (마이너스 스케일이 있을 수 있음) shiftStart와 shiftEnd는 z값만 존재 // distance 는 distance 값인데, 스케일이 커지면 역으로 작아져야한다. 그 이유는, TransformPoint (매트릭스) 연산은 동일한 입력 값에 대해서 로컬 스케일이 커지면 더 큰 값을 도출하기때문에 distance는 작아져야 같은 형상을 유지한다. shiftStart.z /= soxLtnNodes[i].transform.lossyScale.z; shiftEnd.z /= soxLtnNodes[i].transform.lossyScale.z; // !! 순서 중요 // 부드러운 곡선을 위해 bias에 약간의 특수 처리. shiftStart와 shiftEnd 값을 먼저 세팅한 뒤에 bias에 변화를 주는 순서가 중요함 if (i == 1 || i == (soxLtnNodes.Length - 1)) { // 서브스텝이 시작이거나 끝 노드쪽일 경우 if (i == 1) { bias = SoxLtn.EaseOut(0f, 1f, bias); } if (i == (soxLtnNodes.Length - 1)) { bias = SoxLtn.EaseIn(0f, 1f, bias); } } else { // 서브스텝이 중간 노드쪽일 경우 if (bias < 0.5f) { bias -= ((0.5f - bias) * (bias / 0.5f)); } else { bias += ((bias - 0.5f) * (1f - bias) / 0.5f); } } int substepIndex = (subStep + 1) * (i - 1) * 2 + (j * 2); int substepNoiseIndex = (i - 1) * subStep + j - 1; // shiftLerp 위치는 두께 halfWidth를 고려하지 않은 서브스텝 위치 Vector3 shiftLerp = Vector3.Lerp ( // 라이트닝 노드 기준으로 로컬 포지션을 월드 포지션으로 변환한 뒤 Lerp nodeMatrixs[i - 1].MultiplyPoint3x4(nodeVerts[i - 1] + shiftStart), nodeMatrixs[i].MultiplyPoint3x4(nodeVerts[i] + shiftEnd), bias ); Vector3 noise = subStepNoises[substepNoiseIndex] * subStepNoiseAmp * tempNoiseType * scaleVar; // 서브스텝 위치의 zTwist 로테이션 매트릭스 (두께를 적용하려면 두께 벡터를 노드 방향으로 이동시켜야함, 노드는 카메라 룩앳) tempZRotMatrix.SetTRS(Vector3.zero, Quaternion.Slerp(soxLtnNodes[i - 1].transform.rotation, soxLtnNodes[i].transform.rotation, bias), Vector3.one); float subDistanceSum = 0f; if (ifUseWave || ifUseCone) { // 서브스텝 버택스의 distance subDistanceSum = nodeDistanceSum + soxLtnNodes[i].distance * biasForWave - soxLtnNodes[i].distance; } if (ifUseCone) { tempCone = GetCone(subDistanceSum); halfWidthCone = halfWidth * tempCone; } else { halfWidthCone = halfWidth; } // lossy 스케일 반영, 마이너스 스케일이 있을 수 있음 halfWidthCone.x *= soxLtnNodes[i].transform.lossyScale.x; // 월드 포지션을 다시 meTrans 의 로컬 포지션으로 변환 if (ifUseWave && soxLtn.waves != null) { // waveVector는 노드의 로컬방향이 반영되도록 하기 위해서 매트릭스 연산 안에 넣어준다. waveVector = GetWave(subDistanceSum) * tempCone; vertices[substepIndex] = meMatrixInverse.MultiplyPoint3x4(shiftLerp + tempZRotMatrix.MultiplyPoint3x4(halfWidthCone + waveVector)) + noise; vertices[substepIndex + 1] = meMatrixInverse.MultiplyPoint3x4(shiftLerp + tempZRotMatrix.MultiplyPoint3x4(halfWidthCone * -1f + waveVector)) + noise; } else { vertices[substepIndex] = meMatrixInverse.MultiplyPoint3x4(shiftLerp + tempZRotMatrix.MultiplyPoint3x4(halfWidthCone)) + noise; vertices[substepIndex + 1] = meMatrixInverse.MultiplyPoint3x4(shiftLerp + tempZRotMatrix.MultiplyPoint3x4(halfWidthCone * -1f)) + noise; } } // for j end } // if end } // for i end ifPlayParticle = true; // for문 밖에서 처리해야함. // Position Offset Animation 을 사용할 경우 임시로 Node 위치를 이동시켜두었던 것을 // for 루프가 끝났으니 때 다시 원위치. if (ifUsePosOffsetAnim) { for (int i = 0; i < soxLtnNodes.Length; i++) { soxLtnNodes[i].transform.position -= tempPosOffsetAnim; } } mesh.vertices = vertices; mesh.RecalculateBounds(); if (recalculateNormals) { mesh.RecalculateNormals(); } if (recalculateTangents) { mesh.RecalculateTangents(); } } // end of SetVertsPos()