Glen Keybit
Glen Keybit

Reputation: 326

Saving a three.js generated canvas to server - no file being saved

I am trying to save an image to the server that is being created with the following three.js script..

actualCode(THREE);

function actualCode(THREE) {
    //Variables for rendering
    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);

    //Object variables
    let texture;
    let paintedMug;

    //Preload image, then trigger rendering
    const loader = new THREE.TextureLoader();
    texture = loader.load("images/djmug2.jpg", function (_tex) {
        // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
        init();

        //views 17.5=front | 355=side | 139.6=back
        renderImageSolo(17.5);

    });

    function init() {
        //Init scene and camera
        camera.position.set(0, 1.3, 11);
        camera.lookAt(scene.position);
        renderer.setSize(400, 400);
        
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        scene.add(light);

        //Draw white mug
        const muggeom = new THREE.CylinderGeometry(1.5, 1.5, 3.5, 240, 1);
        const mugmaterial = new THREE.MeshStandardMaterial({
            color: "#fff",
        });
        const mug = new THREE.Mesh(muggeom, mugmaterial);

        //Draw painting on mug with slightly larger radius
        const paintgeom = new THREE.CylinderGeometry(1.5001, 1.5001, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);

        //Define a group as mug + paint
        paintedMug = new THREE.Group();
        paintedMug.add(mug);
        paintedMug.add(paint);
        //Add group to scene
        scene.add(paintedMug);
    }


    function renderImageSolo(angle) {
        //Init just like main renderer / scene, will use same camera
        const solo_renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        solo_renderer.setSize(renderer.domElement.width, renderer.domElement.height);
        solo_renderer.domElement.style.marginTop = "0em"; //Space out canvas
        solo_renderer.domElement.id = "canvas"; //give canvas id
        document.body.appendChild(solo_renderer.domElement);
        const solo_scene = new THREE.Scene();
        //Set an ambient light
        const light = new THREE.AmbientLight(0xffffff); // soft white light
        solo_scene.add(light);

        //Draw painting alone
        const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
        const paintmaterial = new THREE.MeshStandardMaterial({
            map: texture,
        });
        const paint = new THREE.Mesh(paintgeom, paintmaterial);
        //Add paint to scene
        solo_scene.add(paint);
        //Rotate paint by angle
        paint.rotation.y = angle
        //Draw result with green screen bg
        solo_scene.background = new THREE.Color(0x04F404);
        //Draw result with trans bg (not working showing as black atm)
        //solo_scene.background = new THREE.WebGLRenderer( { alpha: true } );

        solo_renderer.render(solo_scene, camera);
        saveit();
    }
}

I then attempt to save the generated image with ajax as follows..

function saveit() {
    const canvas = document.getElementById('canvas');
    var photo = canvas.toDataURL('image/jpeg');
    $.ajax({
        method: 'POST',
        url: 'photo_upload.php',
        data: {
            photo: photo
        }
    });
}

photo_upload.php contents..

$data = $_POST['photo'];
    list($type, $data) = explode(';', $data);
    list(, $data)      = explode(',', $data);
    $data = base64_decode($data);

    mkdir($_SERVER['DOCUMENT_ROOT'] . "/photos");

    file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/photos/".time().'.png', $data);
    die;

but nothing gets saved and /photos on the server remains empty, also, as a seperate issue if i right click and "save image" the saved image is just a black square and not what is shown on the screen.

Upvotes: 2

Views: 1109

Answers (2)

julien.giband
julien.giband

Reputation: 2619

Code for saving to PHP server re-written with modern javascript and tested:

  1. Keep only the relevant part of js and add the saving function using fetch
import * as THREE from 'https://cdn.skypack.dev/three';

document.addEventListener("DOMContentLoaded", _e => {

  //Create a div to receive results
  const messDiv = document.createElement('div');
  messDiv.classList.add('message');
  document.body.appendChild(messDiv);

  //Object variables
  let texture;

  //Preload image, then trigger rendering
  const loader = new THREE.TextureLoader();
  //Example with image hosted from Imgur:
  messDiv.textContent = "Loading texture...";
  texture = loader.load("https://i.imgur.com/TQZrUSP.jpeg", function(_tex) {
    console.log("texture loaded");
    // /*Debugging:*/ setTimeout(() => document.body.appendChild(texture.image), 100);
    renderImageSolo(60);
  });

  function renderImageSolo(angle) {
    messDiv.textContent = "Rendering 3D projection...";
    //Init just main renderer / scene
    const solo_renderer = new THREE.WebGLRenderer({
      antialias: true,
      preserveDrawingBuffer: true // <-- avoid plain black image
    });
    solo_renderer.setSize(400, 400);
    document.body.appendChild(solo_renderer.domElement);
    const solo_scene = new THREE.Scene();
    //Init camera
    const camera = new THREE.PerspectiveCamera(30, 400.0 / 400, 1, 1000);
    camera.position.set(0, 1.3, 11);
    camera.lookAt(solo_scene.position);
    //Set an ambient light
    const light = new THREE.AmbientLight(0xffffff); // soft white light
    solo_scene.add(light);

    //Draw painting alone
    const paintgeom = new THREE.CylinderGeometry(1.5, 1.5, 3.3, 240, 1, true);
    const paintmaterial = new THREE.MeshStandardMaterial({
      //color: "#ddd",
      map: texture,
    });
    const paint = new THREE.Mesh(paintgeom, paintmaterial);
    //Add paint to scene
    solo_scene.add(paint);
    //Rotate paint by angle
    paint.rotation.y = angle
    //Draw result
    solo_scene.background = new THREE.Color(0xffffff);
    solo_renderer.render(solo_scene, camera);
    //Save result
    saveImage(solo_renderer.domElement, "photo.jpeg")
  }

  //Save canvas as image by posting it to special url on server
  function saveImage(canvas, filename) {
    messDiv.textContent = "Uploading result...";

    canvas.toBlob(imgBlob => { //Specifying image/jpeg, otherwise you'd get a png
      const fileform = new FormData();
      fileform.append('filename', filename);
      fileform.append('data', imgBlob);
      fetch('./photo_upload.php', {
        method: 'POST',
        body: fileform,
      })
      .then(response => {
        return response.json();
      })
      .then(data => {
        if (data.error) { //Show server errors
          messDiv.classList.add('error');
          messDiv.textContent = data.error;
        } else { //Show success message
          messDiv.classList.add('success');
          messDiv.textContent = data.message;
        }
      })
      .catch(err => { //Handle js errors
        console.log(err);
        messDiv.classList.add('error');
        messDiv.textContent = err.message;
      });
    }, 'image/jpeg'); //<- image type for canvas.toBlob (defaults to png)
  }
});
  1. Write code to save on PHP server
<?php
//photo_upload.php

try {

  header('Content-type: application/json');

  //get file name
  $filename = $_POST['filename'];
  if (!$filename) {
    die(json_encode([
      'error' => "Could not read filename from request"
    ]));
  }
  //get image data
  $img = $_FILES['data'];
  if (!$filename) {
    die(json_encode([
      'error' => "No image data in request"
    ]));
  }
  //Create save dir
  $savePath = $_SERVER['DOCUMENT_ROOT'] . "/photos/";
  if (!file_exists($savePath)) {
    if (!mkdir($savePath)) {
      die(json_encode([
        'error' => "Could not create dir $savePath"
      ]));
    }
  }
  //Save file
  $savePath .= $filename;
  if (!move_uploaded_file($img['tmp_name'], $savePath)) {
    echo json_encode([
      'error' => "Could not write to $savePath"
    ]);
  } else {
    $bytes = filesize($savePath);
    echo json_encode([
      'message' => "Image uploaded and saved to $savePath ($bytes bytes)"
    ]);
  }

} catch (Exception $err) {
  echo json_encode([
    'error' => $err->getMessage()
  ]);
}
  1. A little CSS to make messages more readable
body {
  font-family: Arial, Helvetica, sans-serif;
}
.message {
  text-align: center;
  padding: 1em;
  font-style: italic;
  color: dimgray;
}
.message.success {
  font-style: normal;
  font-weight: bold;
  color: forestgreen;
}
.message.error {
  font-style: normal;
  font-family: 'Courier New', Courier, monospace;
  white-space: pre-wrap;
  color: darkred;
}

2021-09-07 - I've edited the code to use js FormData and PHP $_FILES for better efficiency and readbility

Upvotes: 1

Mugen87
Mugen87

Reputation: 31046

You should be able to solve this issue by creating the renderer like so:

const solo_renderer = new THREE.WebGLRenderer({
    antialias: true,
    preserveDrawingBuffer: true // FIX
});

I also suggest you study existing resources that explain how to save a screenshot of your canvas with. Try it with:

Three.js: How can I make a 2D SnapShot of a Scene as a JPG Image?

Upvotes: 0

Related Questions