Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96c24da077 | ||
|
|
ee9e70c53a | ||
|
|
bfb061d88a | ||
|
|
e85f78ece2 | ||
|
|
e6f7d1efb3 | ||
|
|
4a45c4dd43 | ||
|
|
ca4bb5c34b | ||
|
|
42cd8265f5 | ||
|
|
fd25180989 | ||
|
|
b55d4b9600 | ||
|
|
463b4724c9 | ||
|
|
5e851505bb | ||
|
|
5b9ca62976 | ||
|
|
346de14e00 | ||
|
|
8d1f34604e | ||
|
|
3f4dcfa0a7 |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 139 KiB |
@@ -17,6 +17,7 @@ package com.khronos.vulkan_samples
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -103,7 +104,8 @@ class FaceLandmarkerHelper(
|
|||||||
.setMinTrackingConfidence(minFaceTrackingConfidence)
|
.setMinTrackingConfidence(minFaceTrackingConfidence)
|
||||||
.setMinFacePresenceConfidence(minFacePresenceConfidence)
|
.setMinFacePresenceConfidence(minFacePresenceConfidence)
|
||||||
.setNumFaces(maxNumFaces)
|
.setNumFaces(maxNumFaces)
|
||||||
.setOutputFaceBlendshapes(true)
|
.setOutputFaceBlendshapes(false)
|
||||||
|
.setOutputFacialTransformationMatrixes(false)
|
||||||
.setRunningMode(runningMode)
|
.setRunningMode(runningMode)
|
||||||
|
|
||||||
// The ResultListener and ErrorListener only use for LIVE_STREAM mode.
|
// The ResultListener and ErrorListener only use for LIVE_STREAM mode.
|
||||||
@@ -138,6 +140,51 @@ class FaceLandmarkerHelper(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var frameId: Long = 0
|
||||||
|
// 简化的延时跟踪
|
||||||
|
private val frameTimings = mutableMapOf<Long, FrameTiming>()
|
||||||
|
private var lastFpsUpdateTime: Long = 0
|
||||||
|
private var frameCount: Int = 0
|
||||||
|
private var currentFps: Double = 0.0
|
||||||
|
|
||||||
|
// 内部数据类用于存储时间信息
|
||||||
|
private data class FrameTiming(
|
||||||
|
var captureTime: Long = 0,
|
||||||
|
var preprocessStartTime: Long = 0,
|
||||||
|
var preprocessEndTime: Long = 0,
|
||||||
|
var detectionStartTime: Long = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// 辅助方法:记录帧时间信息
|
||||||
|
private fun recordFrameTiming(frameId: Long, block: (FrameTiming) -> Unit) {
|
||||||
|
val timing = frameTimings.getOrPut(frameId) { FrameTiming() }
|
||||||
|
block(timing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:更新FPS计算
|
||||||
|
private fun updateFps() {
|
||||||
|
frameCount++
|
||||||
|
val currentTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
if (lastFpsUpdateTime == 0L) {
|
||||||
|
lastFpsUpdateTime = currentTime
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeDiff = currentTime - lastFpsUpdateTime
|
||||||
|
if (timeDiff >= 1000) { // 每秒更新一次FPS
|
||||||
|
currentFps = frameCount * 1000.0 / timeDiff
|
||||||
|
frameCount = 0
|
||||||
|
lastFpsUpdateTime = currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:清理旧的时间记录
|
||||||
|
private fun cleanupOldTimings(currentFrameId: Long) {
|
||||||
|
val framesToRemove = frameTimings.keys.filter { it < currentFrameId - 10 }
|
||||||
|
framesToRemove.forEach { frameTimings.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the ImageProxy to MP Image and feed it to FacelandmakerHelper.
|
// Convert the ImageProxy to MP Image and feed it to FacelandmakerHelper.
|
||||||
fun detectLiveStream(
|
fun detectLiveStream(
|
||||||
imageProxy: ImageProxy,
|
imageProxy: ImageProxy,
|
||||||
@@ -151,6 +198,15 @@ class FaceLandmarkerHelper(
|
|||||||
}
|
}
|
||||||
val frameTime = SystemClock.uptimeMillis()
|
val frameTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
val currentFrameId = frameId++
|
||||||
|
val captureTime = SystemClock.uptimeMillis()
|
||||||
|
|
||||||
|
// 记录捕获时间
|
||||||
|
recordFrameTiming(currentFrameId) {
|
||||||
|
it.captureTime = captureTime
|
||||||
|
it.preprocessStartTime = SystemClock.uptimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
// Copy out RGB bits from the frame to a bitmap buffer
|
// Copy out RGB bits from the frame to a bitmap buffer
|
||||||
val bitmapBuffer =
|
val bitmapBuffer =
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(
|
||||||
@@ -180,6 +236,9 @@ class FaceLandmarkerHelper(
|
|||||||
matrix, true
|
matrix, true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//val bitmap = BitmapFactory.decodeFile("/sdcard/Android/data/com.khronos.vulkan_samples/files/assets/face480.png")
|
||||||
|
//val mpImage = BitmapImageBuilder(bitmap).build()
|
||||||
|
|
||||||
// Convert the input Bitmap object to an MPImage object to run inference
|
// Convert the input Bitmap object to an MPImage object to run inference
|
||||||
val mpImage = BitmapImageBuilder(rotatedBitmap).build()
|
val mpImage = BitmapImageBuilder(rotatedBitmap).build()
|
||||||
|
|
||||||
@@ -194,144 +253,48 @@ class FaceLandmarkerHelper(
|
|||||||
// be returned in returnLivestreamResult function
|
// be returned in returnLivestreamResult function
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accepts the URI for a video file loaded from the user's gallery and attempts to run
|
|
||||||
// face landmarker inference on the video. This process will evaluate every
|
|
||||||
// frame in the video and attach the results to a bundle that will be
|
|
||||||
// returned.
|
|
||||||
fun detectVideoFile(
|
|
||||||
videoUri: Uri,
|
|
||||||
inferenceIntervalMs: Long
|
|
||||||
): VideoResultBundle? {
|
|
||||||
if (runningMode != RunningMode.VIDEO) {
|
|
||||||
throw IllegalArgumentException(
|
|
||||||
"Attempting to call detectVideoFile" +
|
|
||||||
" while not using RunningMode.VIDEO"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inference time is the difference between the system time at the start and finish of the
|
|
||||||
// process
|
|
||||||
val startTime = SystemClock.uptimeMillis()
|
|
||||||
|
|
||||||
var didErrorOccurred = false
|
|
||||||
|
|
||||||
// Load frames from the video and run the face landmarker.
|
|
||||||
val retriever = MediaMetadataRetriever()
|
|
||||||
retriever.setDataSource(context, videoUri)
|
|
||||||
val videoLengthMs =
|
|
||||||
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
||||||
?.toLong()
|
|
||||||
|
|
||||||
// Note: We need to read width/height from frame instead of getting the width/height
|
|
||||||
// of the video directly because MediaRetriever returns frames that are smaller than the
|
|
||||||
// actual dimension of the video file.
|
|
||||||
val firstFrame = retriever.getFrameAtTime(0)
|
|
||||||
val width = firstFrame?.width
|
|
||||||
val height = firstFrame?.height
|
|
||||||
|
|
||||||
// If the video is invalid, returns a null detection result
|
|
||||||
if ((videoLengthMs == null) || (width == null) || (height == null)) return null
|
|
||||||
|
|
||||||
// Next, we'll get one frame every frameInterval ms, then run detection on these frames.
|
|
||||||
val resultList = mutableListOf<FaceLandmarkerResult>()
|
|
||||||
val numberOfFrameToRead = videoLengthMs.div(inferenceIntervalMs)
|
|
||||||
|
|
||||||
for (i in 0..numberOfFrameToRead) {
|
|
||||||
val timestampMs = i * inferenceIntervalMs // ms
|
|
||||||
|
|
||||||
retriever
|
|
||||||
.getFrameAtTime(
|
|
||||||
timestampMs * 1000, // convert from ms to micro-s
|
|
||||||
MediaMetadataRetriever.OPTION_CLOSEST
|
|
||||||
)
|
|
||||||
?.let { frame ->
|
|
||||||
// Convert the video frame to ARGB_8888 which is required by the MediaPipe
|
|
||||||
val argb8888Frame =
|
|
||||||
if (frame.config == Bitmap.Config.ARGB_8888) frame
|
|
||||||
else frame.copy(Bitmap.Config.ARGB_8888, false)
|
|
||||||
|
|
||||||
// Convert the input Bitmap object to an MPImage object to run inference
|
|
||||||
val mpImage = BitmapImageBuilder(argb8888Frame).build()
|
|
||||||
|
|
||||||
// Run face landmarker using MediaPipe Face Landmarker API
|
|
||||||
faceLandmarker?.detectForVideo(mpImage, timestampMs)
|
|
||||||
?.let { detectionResult ->
|
|
||||||
resultList.add(detectionResult)
|
|
||||||
} ?: {
|
|
||||||
didErrorOccurred = true
|
|
||||||
faceLandmarkerHelperListener?.onError(
|
|
||||||
"ResultBundle could not be returned" +
|
|
||||||
" in detectVideoFile"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
didErrorOccurred = true
|
|
||||||
faceLandmarkerHelperListener?.onError(
|
|
||||||
"Frame at specified time could not be" +
|
|
||||||
" retrieved when detecting in video."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retriever.release()
|
|
||||||
|
|
||||||
val inferenceTimePerFrameMs =
|
|
||||||
(SystemClock.uptimeMillis() - startTime).div(numberOfFrameToRead)
|
|
||||||
|
|
||||||
return if (didErrorOccurred) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
VideoResultBundle(resultList, inferenceTimePerFrameMs, height, width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accepted a Bitmap and runs face landmarker inference on it to return
|
|
||||||
// results back to the caller
|
|
||||||
fun detectImage(image: Bitmap): ResultBundle? {
|
|
||||||
if (runningMode != RunningMode.IMAGE) {
|
|
||||||
throw IllegalArgumentException(
|
|
||||||
"Attempting to call detectImage" +
|
|
||||||
" while not using RunningMode.IMAGE"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Inference time is the difference between the system time at the
|
|
||||||
// start and finish of the process
|
|
||||||
val startTime = SystemClock.uptimeMillis()
|
|
||||||
|
|
||||||
// Convert the input Bitmap object to an MPImage object to run inference
|
|
||||||
val mpImage = BitmapImageBuilder(image).build()
|
|
||||||
|
|
||||||
// Run face landmarker using MediaPipe Face Landmarker API
|
|
||||||
faceLandmarker?.detect(mpImage)?.also { landmarkResult ->
|
|
||||||
val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
|
|
||||||
return ResultBundle(
|
|
||||||
landmarkResult,
|
|
||||||
inferenceTimeMs,
|
|
||||||
image.height,
|
|
||||||
image.width
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If faceLandmarker?.detect() returns null, this is likely an error. Returning null
|
|
||||||
// to indicate this.
|
|
||||||
faceLandmarkerHelperListener?.onError(
|
|
||||||
"Face Landmarker failed to detect."
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the landmark result to this FaceLandmarkerHelper's caller
|
// Return the landmark result to this FaceLandmarkerHelper's caller
|
||||||
private fun returnLivestreamResult(
|
private fun returnLivestreamResult(
|
||||||
result: FaceLandmarkerResult,
|
result: FaceLandmarkerResult,
|
||||||
input: MPImage
|
input: MPImage
|
||||||
) {
|
) {
|
||||||
|
val resultTime = SystemClock.uptimeMillis()
|
||||||
|
val frameId = result.timestampMs()
|
||||||
if( result.faceLandmarks().size > 0 ) {
|
if( result.faceLandmarks().size > 0 ) {
|
||||||
|
// 计算各种延时
|
||||||
|
val timings = frameTimings[frameId]
|
||||||
|
if (timings != null) {
|
||||||
|
val totalLatency = resultTime - timings.captureTime
|
||||||
|
val preprocessLatency = timings.preprocessEndTime - timings.preprocessStartTime
|
||||||
|
val detectionLatency = resultTime - timings.detectionStartTime
|
||||||
|
val captureToDetectionLatency = timings.detectionStartTime - timings.captureTime
|
||||||
|
|
||||||
|
Log.i("TimeLatency",
|
||||||
|
"总延时: ${totalLatency}ms | " +
|
||||||
|
"预处理: ${preprocessLatency}ms | " +
|
||||||
|
"检测: ${detectionLatency}ms | " +
|
||||||
|
"捕获到检测: ${captureToDetectionLatency}ms | " +
|
||||||
|
"FPS: ${String.format("%.1f", currentFps)}"
|
||||||
|
)
|
||||||
|
|
||||||
val finishTimeMs = SystemClock.uptimeMillis()
|
val finishTimeMs = SystemClock.uptimeMillis()
|
||||||
val inferenceTime = finishTimeMs - result.timestampMs()
|
val inferenceTime = finishTimeMs - result.timestampMs()
|
||||||
|
faceLandmarkerHelperListener?.onResults(
|
||||||
|
ResultBundle(
|
||||||
|
result,
|
||||||
|
inferenceTime, // 主要是检测时间
|
||||||
|
input.height,
|
||||||
|
input.width,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val finishTimeMs = SystemClock.uptimeMillis()
|
||||||
|
val inferenceTime = finishTimeMs - result.timestampMs()
|
||||||
|
Log.i("TimeLatency",
|
||||||
|
"总延时: ${inferenceTime}ms | "
|
||||||
|
)
|
||||||
faceLandmarkerHelperListener?.onResults(
|
faceLandmarkerHelperListener?.onResults(
|
||||||
ResultBundle(
|
ResultBundle(
|
||||||
result,
|
result,
|
||||||
@@ -341,6 +304,7 @@ class FaceLandmarkerHelper(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
faceLandmarkerHelperListener?.onEmpty()
|
faceLandmarkerHelperListener?.onEmpty()
|
||||||
}
|
}
|
||||||
@@ -375,13 +339,6 @@ class FaceLandmarkerHelper(
|
|||||||
val inputImageWidth: Int,
|
val inputImageWidth: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class VideoResultBundle(
|
|
||||||
val results: List<FaceLandmarkerResult>,
|
|
||||||
val inferenceTime: Long,
|
|
||||||
val inputImageHeight: Int,
|
|
||||||
val inputImageWidth: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface LandmarkerListener {
|
interface LandmarkerListener {
|
||||||
fun onError(error: String, errorCode: Int = OTHER_ERROR)
|
fun onError(error: String, errorCode: Int = OTHER_ERROR)
|
||||||
fun onResults(resultBundle: ResultBundle)
|
fun onResults(resultBundle: ResultBundle)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@@ -36,6 +37,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class MainActivity extends GameActivity implements FaceLandmarkerHelper.LandmarkerListener, CameraDataFetcher.CameraDataCallback {
|
public class MainActivity extends GameActivity implements FaceLandmarkerHelper.LandmarkerListener, CameraDataFetcher.CameraDataCallback {
|
||||||
private ProcessCameraProvider cameraProvider;
|
private ProcessCameraProvider cameraProvider;
|
||||||
@@ -66,8 +68,8 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
AssetsCopyUtil.copyAssetsToAppFiles(this, "assets");
|
//AssetsCopyUtil.copyAssetsToAppFiles(this, "assets");
|
||||||
AssetsCopyUtil.copyAssetsToAppFiles(this, "shaders");
|
//AssetsCopyUtil.copyAssetsToAppFiles(this, "shaders");
|
||||||
|
|
||||||
|
|
||||||
String native_lib_name = getResources().getString(R.string.native_lib_name);
|
String native_lib_name = getResources().getString(R.string.native_lib_name);
|
||||||
@@ -105,16 +107,16 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 初始化Runnable
|
// 初始化Runnable
|
||||||
cameraDataRunnable = new Runnable() {
|
// cameraDataRunnable = new Runnable() {
|
||||||
@Override
|
// @Override
|
||||||
public void run() {
|
// public void run() {
|
||||||
fetchCameraData();
|
// fetchCameraData();
|
||||||
handler.postDelayed(this, INTERVAL); // 再次调度
|
// handler.postDelayed(this, INTERVAL); // 再次调度
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 开始周期性调用
|
// 开始周期性调用
|
||||||
startPeriodicFetching();
|
// startPeriodicFetching();
|
||||||
|
|
||||||
|
|
||||||
Log.i("MainActivity", "onCreate: ");
|
Log.i("MainActivity", "onCreate: ");
|
||||||
@@ -178,13 +180,45 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
|
|
||||||
|
|
||||||
imageAnalyzer.setAnalyzer(backgroundExecutor, new ImageAnalysis.Analyzer() {
|
imageAnalyzer.setAnalyzer(backgroundExecutor, new ImageAnalysis.Analyzer() {
|
||||||
|
private long lastCaptureTime = 0;
|
||||||
|
private int frameCount = 0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void analyze(@NonNull ImageProxy image) {
|
public void analyze(@NonNull ImageProxy image) {
|
||||||
|
frameCount++;
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
//Log.d("Camera", "Received image: " + image.getWidth() + "x" + image.getHeight());
|
long captureTimeNs = image.getImageInfo().getTimestamp();
|
||||||
|
long analyzeTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// 简单直接的计算方法
|
||||||
|
long captureTimeMs = TimeUnit.NANOSECONDS.toMillis(captureTimeNs);
|
||||||
|
long latency = analyzeTime - captureTimeMs;
|
||||||
|
|
||||||
|
// 帧率计算
|
||||||
|
if (lastCaptureTime > 0) {
|
||||||
|
long frameInterval = captureTimeMs - lastCaptureTime;
|
||||||
|
double fps = 1000.0 / frameInterval;
|
||||||
|
|
||||||
|
if (frameCount % 10 == 0) { // 每30帧打印一次
|
||||||
|
Log.i("TimeLatency", String.format(
|
||||||
|
"Frame %d - Latency: %d ms, FPS: %.1f",
|
||||||
|
frameCount, latency, fps
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCaptureTime = captureTimeMs;
|
||||||
|
|
||||||
processImageForVulkan(image);
|
processImageForVulkan(image);
|
||||||
|
if(frameCount %2 == 0)
|
||||||
|
{
|
||||||
detectFace(image);
|
detectFace(image);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
image.close();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
//image.close(); // 确保在这里关闭
|
//image.close(); // 确保在这里关闭
|
||||||
//Log.d("Camera", "Image closed, ready for next frame");
|
//Log.d("Camera", "Image closed, ready for next frame");
|
||||||
@@ -315,7 +349,7 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
nativeBuffer.order(ByteOrder.nativeOrder());
|
nativeBuffer.order(ByteOrder.nativeOrder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long start_time = System.currentTimeMillis();
|
||||||
|
|
||||||
// ArrayList<Vector> vertexs = new ArrayList<>();
|
// ArrayList<Vector> vertexs = new ArrayList<>();
|
||||||
// ArrayList<Integer> triangle_index = new ArrayList<>();
|
// ArrayList<Integer> triangle_index = new ArrayList<>();
|
||||||
@@ -325,8 +359,8 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
// StringBuilder sb = new StringBuilder();
|
//StringBuilder sb = new StringBuilder();
|
||||||
// sb.append("[");
|
//sb.append("[");
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
// Iterate through each detected face
|
// Iterate through each detected face
|
||||||
@@ -337,7 +371,6 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
for (NormalizedLandmark p : faceLandmarks)
|
for (NormalizedLandmark p : faceLandmarks)
|
||||||
{
|
{
|
||||||
//sb.append("[");
|
//sb.append("[");
|
||||||
//Log.i(TAG, "Index" + index + " x:"+p.x() + " y:"+p.y() + " z:"+p.z());
|
|
||||||
points[arrayIndex++] = p.x();
|
points[arrayIndex++] = p.x();
|
||||||
//sb.append(""+p.x()+",");
|
//sb.append(""+p.x()+",");
|
||||||
points[arrayIndex++] = p.y();
|
points[arrayIndex++] = p.y();
|
||||||
@@ -359,7 +392,10 @@ public class MainActivity extends GameActivity implements FaceLandmarkerHelper.L
|
|||||||
FloatBuffer floatBuffer = nativeBuffer.asFloatBuffer();
|
FloatBuffer floatBuffer = nativeBuffer.asFloatBuffer();
|
||||||
floatBuffer.put(points);
|
floatBuffer.put(points);
|
||||||
floatBuffer.position(0);
|
floatBuffer.position(0);
|
||||||
|
long cur_time = System.currentTimeMillis();
|
||||||
|
Log.i("TimeLatency","Result Data ProcessTime:" + (cur_time-start_time));
|
||||||
passDataToNative(nativeBuffer, index, width, height);
|
passDataToNative(nativeBuffer, index, width, height);
|
||||||
|
Log.i("TimeLatency","passDataToNative ProcessTime:" + (System.currentTimeMillis() - cur_time));
|
||||||
}
|
}
|
||||||
|
|
||||||
public float z_rate = -1.0f;
|
public float z_rate = -1.0f;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 139 KiB |
@@ -0,0 +1,161 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def read_obj_vertices(obj_file_path):
|
||||||
|
"""读取OBJ文件中的顶点数据"""
|
||||||
|
vertices = []
|
||||||
|
with open(obj_file_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith('v ') and not line.startswith('vt ') and not line.startswith('vn '):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
# 提取顶点坐标,转换为浮点数
|
||||||
|
vertex = [float(parts[1]), float(parts[2]), float(parts[3])]
|
||||||
|
vertices.append(vertex)
|
||||||
|
return np.array(vertices)
|
||||||
|
|
||||||
|
def find_vertex_mapping(original_vertices, modified_vertices, tolerance=1e-4):
|
||||||
|
"""
|
||||||
|
找到修改后顶点在原始顶点中的索引映射
|
||||||
|
使用严格的数值匹配,匹配失败或重复匹配都会报错
|
||||||
|
"""
|
||||||
|
mapping = []
|
||||||
|
used_indices = set()
|
||||||
|
|
||||||
|
for i, mod_vertex in enumerate(modified_vertices):
|
||||||
|
matches = []
|
||||||
|
|
||||||
|
# 查找所有在容差范围内的匹配顶点
|
||||||
|
for j, orig_vertex in enumerate(original_vertices):
|
||||||
|
if np.allclose(mod_vertex, orig_vertex, atol=tolerance):
|
||||||
|
matches.append(j)
|
||||||
|
|
||||||
|
# 检查匹配结果
|
||||||
|
if len(matches) == 0:
|
||||||
|
# 没有找到匹配
|
||||||
|
print(f"错误: 无法为修改后顶点 {i} ({mod_vertex}) 找到匹配的原始顶点")
|
||||||
|
print(f" 最近的原始顶点:")
|
||||||
|
# 找到最近的几个顶点供参考
|
||||||
|
distances = []
|
||||||
|
for j, orig_vertex in enumerate(original_vertices):
|
||||||
|
dist = np.linalg.norm(mod_vertex - orig_vertex)
|
||||||
|
distances.append((dist, j, orig_vertex))
|
||||||
|
distances.sort()
|
||||||
|
for dist, j, vertex in distances[:3]: # 显示最近的3个
|
||||||
|
print(f" 索引 {j}: {vertex}, 距离: {dist:.6f}")
|
||||||
|
raise ValueError(f"顶点 {i} 匹配失败")
|
||||||
|
|
||||||
|
elif len(matches) > 1:
|
||||||
|
# 找到多个匹配
|
||||||
|
print(f"错误: 为修改后顶点 {i} ({mod_vertex}) 找到多个匹配的原始顶点:")
|
||||||
|
for match_idx in matches:
|
||||||
|
print(f" 原始顶点索引 {match_idx}: {original_vertices[match_idx]}")
|
||||||
|
raise ValueError(f"顶点 {i} 匹配到多个原始顶点")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 找到一个匹配
|
||||||
|
match_idx = matches[0]
|
||||||
|
if match_idx in used_indices:
|
||||||
|
print(f"错误: 原始顶点 {match_idx} 已经被多个修改后顶点匹配")
|
||||||
|
print(f" 当前修改后顶点 {i}: {mod_vertex}")
|
||||||
|
# 找出哪个修改后顶点已经匹配了这个原始顶点
|
||||||
|
for k, mapped_idx in enumerate(mapping):
|
||||||
|
if mapped_idx == match_idx:
|
||||||
|
print(f" 已经被修改后顶点 {k}: {modified_vertices[k]} 匹配")
|
||||||
|
raise ValueError(f"原始顶点 {match_idx} 被重复匹配")
|
||||||
|
|
||||||
|
mapping.append(match_idx)
|
||||||
|
used_indices.add(match_idx)
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
def validate_mapping_completeness(mapping, original_vertices, modified_vertices):
|
||||||
|
"""验证映射的完整性"""
|
||||||
|
print("\n验证映射完整性...")
|
||||||
|
|
||||||
|
# 检查是否有未匹配的原始顶点
|
||||||
|
all_original_indices = set(range(len(original_vertices)))
|
||||||
|
used_original_indices = set(mapping)
|
||||||
|
unused_original_indices = all_original_indices - used_original_indices
|
||||||
|
|
||||||
|
if unused_original_indices:
|
||||||
|
print(f"警告: 有 {len(unused_original_indices)} 个原始顶点未被使用:")
|
||||||
|
for idx in list(unused_original_indices)[:5]: # 只显示前5个
|
||||||
|
print(f" 原始顶点 {idx}: {original_vertices[idx]}")
|
||||||
|
if len(unused_original_indices) > 5:
|
||||||
|
print(f" ... 还有 {len(unused_original_indices) - 5} 个")
|
||||||
|
|
||||||
|
# 检查顶点数量是否一致
|
||||||
|
if len(modified_vertices) != len(original_vertices):
|
||||||
|
print(f"警告: 顶点数量不一致 - 原始: {len(original_vertices)}, 修改后: {len(modified_vertices)}")
|
||||||
|
|
||||||
|
return len(unused_original_indices) == 0
|
||||||
|
|
||||||
|
def generate_header_file(mapping, output_file="map.h"):
|
||||||
|
"""生成C++头文件"""
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.write("#ifndef VERTEX_MAP_H\n")
|
||||||
|
f.write("#define VERTEX_MAP_H\n\n")
|
||||||
|
f.write("// 顶点索引映射表\n")
|
||||||
|
f.write("// 用于将face_picture_3dmax.obj的顶点索引映射到face_picture.obj的顶点索引\n")
|
||||||
|
f.write("// 生成说明: modified_vertex_index -> original_vertex_index\n")
|
||||||
|
f.write("// 注意: 这个映射是严格的一对一匹配,匹配容差为1e-4\n")
|
||||||
|
f.write(f"const int indexMap[{len(mapping)}] = {{\n")
|
||||||
|
|
||||||
|
# 每行输出10个元素
|
||||||
|
for i in range(0, len(mapping), 10):
|
||||||
|
line_indices = mapping[i:i+10]
|
||||||
|
line = " " + ", ".join(f"{idx:3d}" for idx in line_indices)
|
||||||
|
if i + 10 < len(mapping):
|
||||||
|
line += ","
|
||||||
|
f.write(line + "\n")
|
||||||
|
|
||||||
|
f.write("};\n\n")
|
||||||
|
f.write("#endif // VERTEX_MAP_H\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 文件路径
|
||||||
|
original_obj = "../assets/face_picture.obj"
|
||||||
|
modified_obj = "../assets/face_picture_3dmax.obj"
|
||||||
|
output_header = "map.h"
|
||||||
|
|
||||||
|
# 匹配容差 - 根据你的数据精度调整这个值
|
||||||
|
tolerance = 1e-4
|
||||||
|
|
||||||
|
print("正在读取OBJ文件...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取顶点数据
|
||||||
|
original_vertices = read_obj_vertices(original_obj)
|
||||||
|
modified_vertices = read_obj_vertices(modified_obj)
|
||||||
|
|
||||||
|
print(f"原始文件顶点数: {len(original_vertices)}")
|
||||||
|
print(f"修改文件顶点数: {len(modified_vertices)}")
|
||||||
|
print(f"匹配容差: {tolerance}")
|
||||||
|
|
||||||
|
# 找到顶点映射
|
||||||
|
print("正在严格匹配顶点...")
|
||||||
|
mapping = find_vertex_mapping(original_vertices, modified_vertices, tolerance)
|
||||||
|
|
||||||
|
# 验证完整性
|
||||||
|
is_complete = validate_mapping_completeness(mapping, original_vertices, modified_vertices)
|
||||||
|
|
||||||
|
if is_complete:
|
||||||
|
print("✓ 所有原始顶点都被正确使用")
|
||||||
|
else:
|
||||||
|
print("⚠ 部分原始顶点未被使用")
|
||||||
|
|
||||||
|
print(f"✓ 成功匹配所有 {len(mapping)} 个顶点")
|
||||||
|
|
||||||
|
# 生成头文件
|
||||||
|
print(f"生成头文件: {output_header}")
|
||||||
|
generate_header_file(mapping, output_header)
|
||||||
|
|
||||||
|
print("\n完成!映射文件已生成。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 错误: {e}")
|
||||||
|
print("映射生成失败,请检查数据或调整匹配容差")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
def create_triangular_obj_from_json(vertices_file, indices_file, output_file):
|
||||||
|
# 读取顶点数据
|
||||||
|
with open(vertices_file, 'r') as f:
|
||||||
|
vertices = json.load(f)
|
||||||
|
|
||||||
|
# 读取线索引数据
|
||||||
|
with open(indices_file, 'r') as f:
|
||||||
|
edges = json.load(f)
|
||||||
|
|
||||||
|
print(f"读取到 {len(vertices)} 个顶点")
|
||||||
|
print(f"读取到 {len(edges)} 条边")
|
||||||
|
|
||||||
|
# 根据边的连接关系自动识别三角形面
|
||||||
|
triangles = edges_to_triangles(edges)
|
||||||
|
|
||||||
|
|
||||||
|
print(f"识别到 {len(triangles)} 个三角形面")
|
||||||
|
|
||||||
|
# 创建OBJ文件
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
# 写入文件头
|
||||||
|
f.write("# OBJ文件 - 三角形面网格\n")
|
||||||
|
f.write(f"# 顶点数: {len(vertices)}, 面数: {len(triangles)}\n\n")
|
||||||
|
|
||||||
|
# 写入顶点信息 (v x y z)
|
||||||
|
for i, vertex in enumerate(vertices):
|
||||||
|
#f.write(f"# 顶点 {i}\n")
|
||||||
|
f.write(f"v {vertex[0]:.6f} {vertex[1]:.6f} {vertex[2]:.6f}\n")
|
||||||
|
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# 写入面信息 (f v1 v2 v3)
|
||||||
|
for triangle in triangles:
|
||||||
|
# OBJ文件索引从1开始,所以需要+1
|
||||||
|
#f.write(f"# 面 {triangle[0]} {triangle[1]} {triangle[2]}\n")
|
||||||
|
f.write(f"f {triangle[0]+1} {triangle[1]+1} {triangle[2]+1}\n")
|
||||||
|
|
||||||
|
def edges_to_triangles(edges):
|
||||||
|
"""
|
||||||
|
将edges数组转换为triangles数组
|
||||||
|
每三个元素的第一值组成一个三角形
|
||||||
|
|
||||||
|
Args:
|
||||||
|
edges: 从JSON读取的边数组
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
triangles: 三角形数组,每个元素包含三个点的索引
|
||||||
|
"""
|
||||||
|
triangles = []
|
||||||
|
|
||||||
|
# 确保边的数量是3的倍数(每个三角形由3条边组成)
|
||||||
|
if len(edges) % 3 != 0:
|
||||||
|
print(f"警告: 边的数量({len(edges)})不是3的倍数")
|
||||||
|
|
||||||
|
# 每3条边组成一个三角形,取每条边的第一个点
|
||||||
|
for i in range(0, len(edges), 3):
|
||||||
|
if i + 2 < len(edges): # 确保有足够的边
|
||||||
|
triangle = [
|
||||||
|
edges[i][0], # 第一条边的第一个点
|
||||||
|
edges[i+1][0], # 第二条边的第一个点
|
||||||
|
edges[i+2][0] # 第三条边的第一个点
|
||||||
|
]
|
||||||
|
triangles.append(triangle)
|
||||||
|
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 输入文件
|
||||||
|
vertices_file = "face_picture_point.json" # 顶点数据文件
|
||||||
|
indices_file = "point_connection.json" # 线索引数据文件
|
||||||
|
|
||||||
|
# 输出文件
|
||||||
|
output_file = "face_picture.obj" # 生成的OBJ文件
|
||||||
|
|
||||||
|
# 生成OBJ文件
|
||||||
|
create_triangular_obj_from_json(vertices_file, indices_file, output_file)
|
||||||
|
print(f"OBJ文件已生成: {output_file}")
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#ifndef VERTEX_MAP_H
|
||||||
|
#define VERTEX_MAP_H
|
||||||
|
|
||||||
|
// 顶点索引映射表
|
||||||
|
// 用于将face_picture_3dmax.obj的顶点索引映射到face_picture.obj的顶点索引
|
||||||
|
// 生成说明: modified_vertex_index -> original_vertex_index
|
||||||
|
// 注意: 这个映射是严格的一对一匹配,匹配容差为1e-4
|
||||||
|
const int indexMap[468] = {
|
||||||
|
127, 34, 139, 11, 0, 37, 232, 231, 120, 72,
|
||||||
|
39, 128, 121, 47, 104, 69, 67, 175, 171, 148,
|
||||||
|
118, 50, 101, 73, 40, 9, 151, 108, 48, 115,
|
||||||
|
131, 194, 204, 211, 74, 185, 80, 42, 183, 92,
|
||||||
|
186, 230, 229, 202, 212, 214, 83, 18, 17, 76,
|
||||||
|
61, 146, 160, 29, 30, 56, 157, 173, 106, 135,
|
||||||
|
192, 203, 165, 98, 21, 71, 68, 51, 45, 4,
|
||||||
|
144, 24, 23, 77, 91, 205, 187, 201, 200, 182,
|
||||||
|
90, 181, 85, 84, 206, 36, 140, 193, 189, 244,
|
||||||
|
159, 158, 28, 247, 246, 161, 236, 3, 196, 54,
|
||||||
|
168, 8, 117, 228, 31, 55, 97, 99, 126, 100,
|
||||||
|
166, 79, 218, 155, 154, 26, 209, 49, 136, 150,
|
||||||
|
217, 223, 52, 53, 134, 170, 43, 119, 226, 130,
|
||||||
|
63, 238, 20, 242, 46, 70, 156, 78, 62, 96,
|
||||||
|
143, 227, 123, 111, 44, 125, 19, 216, 153, 22,
|
||||||
|
167, 208, 142, 57, 60, 35, 113, 27, 210, 225,
|
||||||
|
137, 116, 41, 38, 129, 64, 240, 102, 207, 184,
|
||||||
|
169, 149, 176, 105, 66, 122, 6, 147, 65, 107,
|
||||||
|
89, 180, 93, 15, 86, 14, 87, 145, 88, 179,
|
||||||
|
95, 138, 172, 215, 58, 219, 81, 195, 199, 82,
|
||||||
|
163, 110, 234, 109, 235, 191, 222, 141, 221, 197,
|
||||||
|
25, 7, 33, 220, 237, 245, 162, 188, 174, 2,
|
||||||
|
241, 164, 12, 13, 198, 133, 112, 243, 239, 190,
|
||||||
|
32, 178, 132, 177, 1, 213, 59, 94, 75, 224,
|
||||||
|
233, 114, 124, 356, 389, 368, 302, 267, 452, 350,
|
||||||
|
349, 303, 269, 357, 343, 277, 453, 333, 332, 297,
|
||||||
|
152, 377, 347, 348, 330, 304, 270, 336, 337, 278,
|
||||||
|
279, 360, 418, 262, 431, 408, 409, 310, 415, 407,
|
||||||
|
410, 450, 422, 430, 434, 313, 314, 306, 307, 375,
|
||||||
|
387, 388, 260, 286, 414, 398, 335, 406, 364, 367,
|
||||||
|
416, 423, 358, 327, 251, 284, 298, 281, 5, 373,
|
||||||
|
374, 253, 320, 321, 425, 427, 411, 421, 405, 404,
|
||||||
|
315, 16, 426, 266, 400, 369, 322, 391, 417, 465,
|
||||||
|
464, 386, 257, 258, 466, 456, 399, 419, 285, 346,
|
||||||
|
340, 261, 413, 441, 460, 328, 355, 371, 329, 392,
|
||||||
|
439, 438, 382, 341, 256, 429, 420, 394, 379, 437,
|
||||||
|
443, 444, 283, 275, 440, 363, 338, 273, 451, 446,
|
||||||
|
342, 467, 293, 334, 282, 458, 461, 462, 276, 353,
|
||||||
|
383, 308, 324, 325, 300, 372, 345, 447, 352, 274,
|
||||||
|
248, 436, 381, 252, 393, 428, 287, 250, 384, 265,
|
||||||
|
259, 424, 292, 366, 271, 294, 455, 272, 432, 395,
|
||||||
|
299, 351, 280, 319, 295, 296, 403, 323, 454, 316,
|
||||||
|
380, 318, 402, 365, 435, 397, 344, 311, 291, 396,
|
||||||
|
268, 445, 254, 339, 449, 264, 10, 442, 370, 263,
|
||||||
|
255, 359, 412, 301, 378, 326, 457, 362, 459, 463,
|
||||||
|
354, 401, 361, 309, 376, 433, 289, 305, 448, 290,
|
||||||
|
288, 249, 103, 385, 331, 317, 312, 390
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // VERTEX_MAP_H
|
||||||
@@ -49,7 +49,7 @@ public:
|
|||||||
uint32_t mip_levels;
|
uint32_t mip_levels;
|
||||||
};
|
};
|
||||||
|
|
||||||
Texture texture = { 0 };
|
Texture tex_demo0 = { 0 };
|
||||||
Texture tex_demo1 = { 0 };
|
Texture tex_demo1 = { 0 };
|
||||||
Texture tex_demo2 = { 0 };
|
Texture tex_demo2 = { 0 };
|
||||||
Texture tex_demo3 = { 0 };
|
Texture tex_demo3 = { 0 };
|
||||||
@@ -183,8 +183,8 @@ public:
|
|||||||
virtual void view_changed() override;
|
virtual void view_changed() override;
|
||||||
virtual void on_update_ui_overlay(vkb::Drawer& drawer) override;
|
virtual void on_update_ui_overlay(vkb::Drawer& drawer) override;
|
||||||
|
|
||||||
void processWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& out_texture);
|
void processWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& out_texture, bool srgb);
|
||||||
void createTexture(VkDevice device, VkPhysicalDevice physicalDevice, int width, int height, Texture& texture);
|
void createTexture(VkDevice device, VkPhysicalDevice physicalDevice, int width, int height, Texture& texture, bool srgb);
|
||||||
void updateTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue queue, uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& texture);
|
void updateTexture(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue queue, uint8_t* data, int width, int height, int rowStride, size_t dataSize, Texture& texture);
|
||||||
uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties);
|
uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter, VkMemoryPropertyFlags properties);
|
||||||
VkCommandBuffer beginSingleTimeCommands(VkDevice device, VkCommandPool commandPool);
|
VkCommandBuffer beginSingleTimeCommands(VkDevice device, VkCommandPool commandPool);
|
||||||
@@ -206,6 +206,7 @@ private:
|
|||||||
std::vector<TextureLoadingVertexStructure> obj_vertices;
|
std::vector<TextureLoadingVertexStructure> obj_vertices;
|
||||||
std::vector<uint32_t> obj_indices;
|
std::vector<uint32_t> obj_indices;
|
||||||
std::map<int, int> obj_vertices_map;
|
std::map<int, int> obj_vertices_map;
|
||||||
|
std::map<int, int> vertices_map_3dmax;
|
||||||
std::thread workerThread;
|
std::thread workerThread;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#version 450
|
#version 450
|
||||||
|
|
||||||
layout (binding = 1) uniform sampler2D samplerColor;
|
layout (binding = 1) uniform sampler2D samplerColor;
|
||||||
|
layout (binding = 2) uniform sampler2D samplerColor_ex;
|
||||||
|
|
||||||
layout (location = 0) in vec2 inUV;
|
layout (location = 0) in vec2 inUV;
|
||||||
layout (location = 1) in vec3 inNormal;
|
layout (location = 1) in vec3 inNormal;
|
||||||
@@ -11,16 +12,27 @@ layout (location = 0) out vec4 outFragColor;
|
|||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec4 color = texture(samplerColor, inUV);
|
vec4 color = texture(samplerColor, inUV);
|
||||||
|
vec4 color_ex = texture(samplerColor_ex, inUV);
|
||||||
|
|
||||||
vec3 N = normalize(inNormal);
|
vec3 N = normalize(inNormal);
|
||||||
|
|
||||||
vec4 textureColor = color;
|
vec4 textureColor = color;
|
||||||
if (textureColor.a < 0.1) {
|
|
||||||
//textureColor = vec4(0.2, 0.2, 0.2, 0.5);
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
|
|
||||||
outFragColor = textureColor;
|
//if (textureColor.a < 0.1)
|
||||||
|
//{
|
||||||
|
//textureColor = vec4(0.2, 0.2, 0.2, 0.5);
|
||||||
|
// discard;
|
||||||
|
//}
|
||||||
|
|
||||||
|
// ¼ÆËãÓë (0,0,1) µÄµã³Ë
|
||||||
|
float ndot = dot(N, vec3(0.0, 0.0, -1.0));
|
||||||
|
|
||||||
|
// ±£Ö¤½á¹û >= 0
|
||||||
|
ndot = max(ndot, 0.0);
|
||||||
|
|
||||||
|
vec4 blendedColor = mix(color_ex, color, ndot);
|
||||||
|
|
||||||
|
outFragColor = blendedColor;
|
||||||
|
|
||||||
// outFragColor = vec4(1, 0, 0, 1);
|
// outFragColor = vec4(1, 0, 0, 1);
|
||||||
}
|
}
|
||||||