Projection.java
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.video.spherical;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.media3.common.C;
import androidx.media3.common.C.StereoMode;
import androidx.media3.common.util.Assertions;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** The projection mesh used with 360/VR videos. */
/* package */ final class Projection {
/** Enforces allowed (sub) mesh draw modes. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({DRAW_MODE_TRIANGLES, DRAW_MODE_TRIANGLES_STRIP, DRAW_MODE_TRIANGLES_FAN})
public @interface DrawMode {}
/** Triangle draw mode. */
public static final int DRAW_MODE_TRIANGLES = 0;
/** Triangle strip draw mode. */
public static final int DRAW_MODE_TRIANGLES_STRIP = 1;
/** Triangle fan draw mode. */
public static final int DRAW_MODE_TRIANGLES_FAN = 2;
/** Number of position coordinates per vertex. */
public static final int TEXTURE_COORDS_PER_VERTEX = 2;
/** Number of texture coordinates per vertex. */
public static final int POSITION_COORDS_PER_VERTEX = 3;
/**
* Generates a complete sphere equirectangular projection.
*
* @param stereoMode A {@link C.StereoMode} value.
*/
public static Projection createEquirectangular(@C.StereoMode int stereoMode) {
return createEquirectangular(
/* radius= */ 50, // Should be large enough that there are no stereo artifacts.
/* latitudes= */ 36, // Should be large enough to prevent videos looking wavy.
/* longitudes= */ 72, // Should be large enough to prevent videos looking wavy.
/* verticalFovDegrees= */ 180,
/* horizontalFovDegrees= */ 360,
stereoMode);
}
/**
* Generates an equirectangular projection.
*
* @param radius Size of the sphere. Must be > 0.
* @param latitudes Number of rows that make up the sphere. Must be >= 1.
* @param longitudes Number of columns that make up the sphere. Must be >= 1.
* @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in
* (0, 180].
* @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be
* in (0, 360].
* @param stereoMode A {@link C.StereoMode} value.
* @return an equirectangular projection.
*/
public static Projection createEquirectangular(
float radius,
int latitudes,
int longitudes,
float verticalFovDegrees,
float horizontalFovDegrees,
@C.StereoMode int stereoMode) {
Assertions.checkArgument(radius > 0);
Assertions.checkArgument(latitudes >= 1);
Assertions.checkArgument(longitudes >= 1);
Assertions.checkArgument(verticalFovDegrees > 0 && verticalFovDegrees <= 180);
Assertions.checkArgument(horizontalFovDegrees > 0 && horizontalFovDegrees <= 360);
// Compute angular size in radians of each UV quad.
float verticalFovRads = (float) Math.toRadians(verticalFovDegrees);
float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees);
float quadHeightRads = verticalFovRads / latitudes;
float quadWidthRads = horizontalFovRads / longitudes;
// Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices.
int vertexCount = (2 * (longitudes + 1) + 2) * latitudes;
// Buffer to return.
float[] vertexData = new float[vertexCount * POSITION_COORDS_PER_VERTEX];
float[] textureData = new float[vertexCount * TEXTURE_COORDS_PER_VERTEX];
// Generate the data for the sphere which is a set of triangle strips representing each
// latitude band.
int vOffset = 0; // Offset into the vertexData array.
int tOffset = 0; // Offset into the textureData array.
// (i, j) represents a quad in the equirectangular sphere.
for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip.
// Each latitude band lies between the two phi values. Each vertical edge on a band lies on
// a theta value.
float phiLow = quadHeightRads * j - verticalFovRads / 2;
float phiHigh = quadHeightRads * (j + 1) - verticalFovRads / 2;
for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band.
for (int k = 0; k < 2; ++k) { // For low and high points on an edge.
// For each point, determine its position in polar coordinates.
float phi = k == 0 ? phiLow : phiHigh;
float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2;
// Set vertex position data as Cartesian coordinates.
vertexData[vOffset++] = -(float) (radius * Math.sin(theta) * Math.cos(phi));
vertexData[vOffset++] = (float) (radius * Math.sin(phi));
vertexData[vOffset++] = (float) (radius * Math.cos(theta) * Math.cos(phi));
textureData[tOffset++] = i * quadWidthRads / horizontalFovRads;
textureData[tOffset++] = (j + k) * quadHeightRads / verticalFovRads;
// Break up the triangle strip with degenerate vertices by copying first and last points.
if ((i == 0 && k == 0) || (i == longitudes && k == 1)) {
System.arraycopy(
vertexData,
vOffset - POSITION_COORDS_PER_VERTEX,
vertexData,
vOffset,
POSITION_COORDS_PER_VERTEX);
vOffset += POSITION_COORDS_PER_VERTEX;
System.arraycopy(
textureData,
tOffset - TEXTURE_COORDS_PER_VERTEX,
textureData,
tOffset,
TEXTURE_COORDS_PER_VERTEX);
tOffset += TEXTURE_COORDS_PER_VERTEX;
}
}
// Move on to the next vertical edge in the triangle strip.
}
// Move on to the next triangle strip.
}
SubMesh subMesh =
new SubMesh(SubMesh.VIDEO_TEXTURE_ID, vertexData, textureData, DRAW_MODE_TRIANGLES_STRIP);
return new Projection(new Mesh(subMesh), stereoMode);
}
/** The Mesh corresponding to the left eye. */
public final Mesh leftMesh;
/**
* The Mesh corresponding to the right eye. If {@code singleMesh} is true then this mesh is
* identical to {@link #leftMesh}.
*/
public final Mesh rightMesh;
/** The stereo mode. */
public final @StereoMode int stereoMode;
/** Whether the left and right mesh are identical. */
public final boolean singleMesh;
/**
* Creates a Projection with single mesh.
*
* @param mesh the Mesh for both eyes.
* @param stereoMode A {@link StereoMode} value.
*/
public Projection(Mesh mesh, int stereoMode) {
this(mesh, mesh, stereoMode);
}
/**
* Creates a Projection with dual mesh. Use {@link #Projection(Mesh, int)} if there is single mesh
* for both eyes.
*
* @param leftMesh the Mesh corresponding to the left eye.
* @param rightMesh the Mesh corresponding to the right eye.
* @param stereoMode A {@link C.StereoMode} value.
*/
public Projection(Mesh leftMesh, Mesh rightMesh, int stereoMode) {
this.leftMesh = leftMesh;
this.rightMesh = rightMesh;
this.stereoMode = stereoMode;
this.singleMesh = leftMesh == rightMesh;
}
/** The sub mesh associated with the {@link Mesh}. */
public static final class SubMesh {
/** Texture ID for video frames. */
public static final int VIDEO_TEXTURE_ID = 0;
/** Texture ID. */
public final int textureId;
/** The drawing mode. One of {@link DrawMode}. */
public final @DrawMode int mode;
/** The SubMesh vertices. */
public final float[] vertices;
/** The SubMesh texture coordinates. */
public final float[] textureCoords;
public SubMesh(int textureId, float[] vertices, float[] textureCoords, @DrawMode int mode) {
this.textureId = textureId;
Assertions.checkArgument(
vertices.length * (long) TEXTURE_COORDS_PER_VERTEX
== textureCoords.length * (long) POSITION_COORDS_PER_VERTEX);
this.vertices = vertices;
this.textureCoords = textureCoords;
this.mode = mode;
}
/** Returns the SubMesh vertex count. */
public int getVertexCount() {
return vertices.length / POSITION_COORDS_PER_VERTEX;
}
}
/** A Mesh associated with the projection scene. */
public static final class Mesh {
private final SubMesh[] subMeshes;
public Mesh(SubMesh... subMeshes) {
this.subMeshes = subMeshes;
}
/** Returns the number of sub meshes. */
public int getSubMeshCount() {
return subMeshes.length;
}
/** Returns the SubMesh for the given index. */
public SubMesh getSubMesh(int index) {
return subMeshes[index];
}
}
}