blob: 3debb89138453bf56ff8411fa681d70eeaab83ec [file] [log] [blame]
class Uniform {
constructor(float32Array) {
if (float32Array && float32Array.length != 64) {
console.log("Incorrect backing store for Uniform");
return;
}
this.array = float32Array || new Float32Array(64);
}
// Layout is
// 0-15 = model_view_projection_matrix (float4x4)
// 16-31 = normal matrix (float4x4)
// 32-35 = ambient color (float4)
// 36 = multiplier (int)
get buffer() {
return this.array;
}
get mvp() {
return this.array.subarray(0, 16);
}
get normal() {
return this.array.subarray(16, 32);
}
get ambientColor() {
return this.array.subarray(32, 36);
}
get multiplier() {
return this.array[40];
}
set mvp(value) {
this._copyMatrix(value, 0);
}
set normal(value) {
this._copyMatrix(value, 16);
}
set ambientColor(value) {
this.array[32] = value[0];
this.array[33] = value[1];
this.array[34] = value[2];
this.array[35] = value[3];
}
set multiplier(value) {
this.array[36] = value;
}
_copyMatrix(matrix, offset) {
for (var i = 0; i < 16; i++) {
this.array[offset + i] = matrix[i];
}
}
}
const smoothstep = (min, max, value) => {
let x = Math.max(0, Math.min(1, (value - min) / (max - min)));
return x * x * (3 - 2 * x);
};
const inTimeRange = (now, start, end, period) => {
let offset = now % period;
return offset >= start && offset <= end;
};
const timeRangeOffset = (now, start, end, period) => {
if (!inTimeRange(now, start, end, period)) {
return 0;
}
let offset = now % period;
return Math.min(1, Math.max(0, (offset - start) / (end - start)));
};
const middlePeakTimeRangeOffset = (now, start, end, period) => {
let offset = timeRangeOffset(now, start, end, period);
return 1 - Math.abs(0.5 - offset) * 2;
};
const kNumActiveUniformBuffers = 3;
const kNumberOfBoxesPerAxis = 6;
const kNumberOfBoxes = kNumberOfBoxesPerAxis * kNumberOfBoxesPerAxis;
const kBoxBaseAmbientColor = [0.2, 0.2, 0.2, 1.0];
const kFOVY = 45.0 / 180 * Math.PI;
const kEye = vec3.fromValues(0.0, 2.75, -2);
const kCenter = vec3.fromValues(0.0, 1.5, 1.0);
const kUp = vec3.fromValues(0.0, 1.0, 0.0);
const kEdge = 1.5 / kNumberOfBoxesPerAxis ;
const kCubeVertexData = new Float32Array(
[
kEdge, -kEdge, kEdge, 0.0, -1.0, 0.0,
-kEdge, -kEdge, kEdge, 0.0, -1.0, 0.0,
-kEdge, -kEdge, -kEdge, 0.0, -1.0, 0.0,
kEdge, -kEdge, -kEdge, 0.0, -1.0, 0.0,
kEdge, -kEdge, kEdge, 0.0, -1.0, 0.0,
-kEdge, -kEdge, -kEdge, 0.0, -1.0, 0.0,
kEdge, kEdge, kEdge, 1.0, 0.0, 0.0,
kEdge, -kEdge, kEdge, 1.0, 0.0, 0.0,
kEdge, -kEdge, -kEdge, 1.0, 0.0, 0.0,
kEdge, kEdge, -kEdge, 1.0, 0.0, 0.0,
kEdge, kEdge, kEdge, 1.0, 0.0, 0.0,
kEdge, -kEdge, -kEdge, 1.0, 0.0, 0.0,
-kEdge, kEdge, kEdge, 0.0, 1.0, 0.0,
kEdge, kEdge, kEdge, 0.0, 1.0, 0.0,
kEdge, kEdge, -kEdge, 0.0, 1.0, 0.0,
-kEdge, kEdge, -kEdge, 0.0, 1.0, 0.0,
-kEdge, kEdge, kEdge, 0.0, 1.0, 0.0,
kEdge, kEdge, -kEdge, 0.0, 1.0, 0.0,
-kEdge, -kEdge, kEdge, -1.0, 0.0, 0.0,
-kEdge, kEdge, kEdge, -1.0, 0.0, 0.0,
-kEdge, kEdge, -kEdge, -1.0, 0.0, 0.0,
-kEdge, -kEdge, -kEdge, -1.0, 0.0, 0.0,
-kEdge, -kEdge, kEdge, -1.0, 0.0, 0.0,
-kEdge, kEdge, -kEdge, -1.0, 0.0, 0.0,
kEdge, kEdge, kEdge, 0.0, 0.0, 1.0,
-kEdge, kEdge, kEdge, 0.0, 0.0, 1.0,
-kEdge, -kEdge, kEdge, 0.0, 0.0, 1.0,
-kEdge, -kEdge, kEdge, 0.0, 0.0, 1.0,
kEdge, -kEdge, kEdge, 0.0, 0.0, 1.0,
kEdge, kEdge, kEdge, 0.0, 0.0, 1.0,
kEdge, -kEdge, -kEdge, 0.0, 0.0, -1.0,
-kEdge, -kEdge, -kEdge, 0.0, 0.0, -1.0,
-kEdge, kEdge, -kEdge, 0.0, 0.0, -1.0,
kEdge, kEdge, -kEdge, 0.0, 0.0, -1.0,
kEdge, -kEdge, -kEdge, 0.0, 0.0, -1.0,
-kEdge, kEdge, -kEdge, 0.0, 0.0, -1.0
]);
let gpu;
let commandQueue;
let renderPassDescriptor;
let renderPipelineState;
let depthStencilState;
let projectionMatrix = mat4.create();
let viewMatrix = mat4.create();
var startTime;
var elapsedTime;
let cameraRotation = 0;
let cameraAltitude = 0;
const uniformBuffers = new Array(kNumActiveUniformBuffers);
var currentUniformBufferIndex = 0;
window.addEventListener("load", init, false);
function init() {
if (!checkForWebMetal()) {
return;
}
let canvas = document.querySelector("canvas");
let canvasSize = canvas.getBoundingClientRect();
canvas.width = canvasSize.width;
canvas.height = canvasSize.height;
let aspect = Math.abs(canvasSize.width / canvasSize.height);
mat4.perspective(projectionMatrix, kFOVY, aspect, 0.1, 100.0);
mat4.lookAt(viewMatrix, kEye, kCenter, kUp);
gpu = canvas.getContext("webmetal");
commandQueue = gpu.createCommandQueue();
let library = gpu.createLibrary(document.getElementById("library").text);
let vertexFunction = library.functionWithName("vertex_function");
let fragmentFunction = library.functionWithName("fragment_function");
if (!library || !fragmentFunction || !vertexFunction) {
return;
}
var pipelineDescriptor = new WebMetalRenderPipelineDescriptor();
pipelineDescriptor.vertexFunction = vertexFunction;
pipelineDescriptor.fragmentFunction = fragmentFunction;
// NOTE: Our API proposal has these values as enums, not constant numbers.
// We haven't got around to implementing the enums yet.
pipelineDescriptor.colorAttachments[0].pixelFormat = gpu.PixelFormatBGRA8Unorm;
pipelineDescriptor.depthAttachmentPixelFormat = gpu.PixelFormatDepth32Float;
renderPipelineState = gpu.createRenderPipelineState(pipelineDescriptor);
let depthStencilDescriptor = new WebMetalDepthStencilDescriptor();
depthStencilDescriptor.depthWriteEnabled = true;
depthStencilDescriptor.depthCompareFunction = "less";
depthStencilState = gpu.createDepthStencilState(depthStencilDescriptor);
for (let i = 0; i < kNumActiveUniformBuffers; i++) {
uniformBuffers[i] = [];
for (let j = 0; j < kNumberOfBoxes; j++) {
let uniform = new Uniform();
uniform.multiplier = (j % 2) ? -1 : 1;
uniform.ambientColor = kBoxBaseAmbientColor;
uniformBuffers[i].push(gpu.createBuffer(uniform.buffer));
}
}
let depthTextureDescriptor = new WebMetalTextureDescriptor(gpu.PixelFormatDepth32Float, canvasSize.width, canvasSize.height, false);
// NOTE: Our API proposal has some of these values as enums, not constant numbers.
// We haven't got around to implementing the enums yet.
depthTextureDescriptor.textureType = gpu.TextureType2D;
depthTextureDescriptor.sampleCount = 1;
depthTextureDescriptor.usage = gpu.TextureUsageUnknown;
depthTextureDescriptor.storageMode = gpu.StorageModePrivate;
let depthTexture = gpu.createTexture(depthTextureDescriptor);
renderPassDescriptor = new WebMetalRenderPassDescriptor();
// NOTE: Our API proposal has some of these values as enums, not constant numbers.
// We haven't got around to implementing the enums yet.
renderPassDescriptor.colorAttachments[0].loadAction = gpu.LoadActionClear;
renderPassDescriptor.colorAttachments[0].storeAction = gpu.StoreActionStore;
renderPassDescriptor.colorAttachments[0].clearColor = [0.35, 0.65, 0.85, 1.0];
renderPassDescriptor.depthAttachment.loadAction = gpu.LoadActionClear;
renderPassDescriptor.depthAttachment.storeAction = gpu.StoreActionDontCare;
renderPassDescriptor.depthAttachment.clearDepth = 1.0;
renderPassDescriptor.depthAttachment.texture = depthTexture;
vertexBuffer = gpu.createBuffer(kCubeVertexData);
startTime = Date.now();
render();
}
function render() {
elapsedTime = (Date.now() - startTime) / 1000;
cameraRotation = Math.PI * 2 * (1 - timeRangeOffset(elapsedTime, 0, 11, 11));
cameraAltitude = Math.sin(elapsedTime / 6 * Math.PI) + 1;
let eye = vec3.create();
vec3.add(eye, kEye, vec3.fromValues(0, cameraAltitude, 0));
mat4.lookAt(viewMatrix, eye, kCenter, kUp);
updateUniformData(currentUniformBufferIndex);
let commandBuffer = commandQueue.createCommandBuffer();
let drawable = gpu.nextDrawable();
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
let commandEncoder = commandBuffer.createRenderCommandEncoderWithDescriptor(renderPassDescriptor);
commandEncoder.setDepthStencilState(depthStencilState);
commandEncoder.setRenderPipelineState(renderPipelineState);
commandEncoder.setVertexBuffer(vertexBuffer, 0, 0);
for (let geometryBuffer of uniformBuffers[currentUniformBufferIndex]) {
commandEncoder.setVertexBuffer(geometryBuffer, 0, 1);
// NOTE: Our API proposal uses the enum value "triangle" here. We haven't got around to implementing the enums yet.
commandEncoder.drawPrimitives(gpu.PrimitiveTypeTriangle, 0, kCubeVertexData.length);
}
commandEncoder.endEncoding();
commandBuffer.presentDrawable(drawable);
commandBuffer.commit();
currentUniformBufferIndex = (currentUniformBufferIndex + 1) % kNumActiveUniformBuffers;
requestAnimationFrame(render);
}
function updateUniformData(index) {
let baseModelViewMatrix = mat4.create();
mat4.translate(baseModelViewMatrix, baseModelViewMatrix, vec3.fromValues(0, -1 * cameraAltitude, 5));
mat4.rotate(baseModelViewMatrix, baseModelViewMatrix, cameraRotation, vec3.fromValues(0, 1, 0));
mat4.multiply(baseModelViewMatrix, viewMatrix, baseModelViewMatrix);
for (let i = 0; i < kNumberOfBoxesPerAxis; i++) {
for (let j = 0; j < kNumberOfBoxesPerAxis; j++) {
let boxIndex = i * kNumberOfBoxesPerAxis + j;
let modelViewMatrix = mat4.create();
// Position the cube in X,Y.
let translationMatrix = mat4.create();
if (kNumberOfBoxesPerAxis > 1) {
let step = 4 / (kNumberOfBoxesPerAxis - 1);
mat4.translate(translationMatrix, translationMatrix, vec3.fromValues(j * step - 2, 0, i * step - 2));
} else {
mat4.translate(translationMatrix, translationMatrix, vec3.fromValues(0, 0, 0));
}
let translateElapsedOffset = elapsedTime + boxIndex * 0.6;
if (inTimeRange(translateElapsedOffset, 10, 12, 23)) {
let translate = smoothstep(0, 1, middlePeakTimeRangeOffset(translateElapsedOffset, 10, 12, 23)) * 0.4;
mat4.translate(translationMatrix, translationMatrix, vec3.fromValues(0, translate, 0));
}
let scaleMatrix = mat4.create();
let scaleElapsedOffset = elapsedTime + boxIndex * 0.1;
if (inTimeRange(scaleElapsedOffset, 2, 6, 19)) {
let scale = smoothstep(0, 1, middlePeakTimeRangeOffset(scaleElapsedOffset, 2, 6, 19)) * 0.5 + 1;
mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));
}
mat4.multiply(modelViewMatrix, translationMatrix, scaleMatrix);
// Rotate the cube.
let rotationMatrix = mat4.create();
let rotationElapsedOffset = elapsedTime + (kNumberOfBoxes - boxIndex) * 0.1;
if (inTimeRange(rotationElapsedOffset, 3, 7, 11)) {
var rotation = smoothstep(0, 1, timeRangeOffset(rotationElapsedOffset, 3, 7, 11)) * Math.PI * 2;
mat4.rotate(rotationMatrix, rotationMatrix, rotation, vec3.fromValues(0.5, 1, 1));
}
mat4.multiply(modelViewMatrix, modelViewMatrix, rotationMatrix);
mat4.multiply(modelViewMatrix, baseModelViewMatrix, modelViewMatrix);
let normalMatrix = mat4.clone(modelViewMatrix);
mat4.transpose(normalMatrix, normalMatrix);
mat4.invert(normalMatrix, normalMatrix);
let uniform = new Uniform(new Float32Array(uniformBuffers[index][i * kNumberOfBoxesPerAxis + j].contents));
uniform.normal = normalMatrix;
let modelViewProjectionMatrix = mat4.create();
mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelViewMatrix);
uniform.mvp = modelViewProjectionMatrix;
let colorElapsedOffset = elapsedTime + boxIndex * 0.15;
if (inTimeRange(colorElapsedOffset, 2, 3, 10)) {
let redBoost = middlePeakTimeRangeOffset(colorElapsedOffset, 2, 3, 10) * 0.5;
uniform.ambientColor[0] = Math.min(1.0, Math.max(0, kBoxBaseAmbientColor[0] + redBoost));
} else {
uniform.ambientColor[0] = kBoxBaseAmbientColor[0];
}
colorElapsedOffset = elapsedTime + (kNumberOfBoxes - boxIndex) * 0.1;
if (inTimeRange(colorElapsedOffset, 7, 11, 21)) {
let greenBoost = middlePeakTimeRangeOffset(colorElapsedOffset, 7, 11, 21) * 0.3;
uniform.ambientColor[1] = Math.min(1.0, Math.max(0, kBoxBaseAmbientColor[1] + greenBoost));
} else {
uniform.ambientColor[1] = kBoxBaseAmbientColor[1];
}
}
}
}